[ARVADOS] updated: 8c7fb5f13cb167740b7b228c7fab40f2980d1978

git at public.curoverse.com git at public.curoverse.com
Tue Mar 31 11:16:11 EDT 2015


Summary of changes:
 .../app/assets/javascripts/add_repository.js       |   3 +-
 doc/_config.yml                                    |   1 +
 .../install-arv-git-httpd.html.textile.liquid      |  73 +++++++++
 ...nstall-manual-prerequisites.html.textile.liquid |  17 ++-
 .../controllers/arvados/v1/schema_controller.rb    |   1 +
 services/api/app/models/arvados_model.rb           |   9 +-
 services/api/app/models/repository.rb              |  12 ++
 services/api/config/application.default.yml        |   5 +
 services/api/test/fixtures/repositories.yml        |  12 ++
 services/api/test/unit/repository_test.rb          |  27 +++-
 services/arv-git-httpd/.gitignore                  |   1 +
 services/arv-git-httpd/auth_handler.go             | 162 ++++++++++++++++++++
 services/arv-git-httpd/basic_auth_go13.go          |  28 ++++
 services/arv-git-httpd/basic_auth_go14.go          |  11 ++
 services/arv-git-httpd/basic_auth_test.go          |  30 ++++
 services/arv-git-httpd/doc.go                      |  29 ++++
 services/arv-git-httpd/main.go                     |  49 ++++++
 services/arv-git-httpd/server.go                   | 105 +++++++++++++
 services/arv-git-httpd/server_test.go              | 169 +++++++++++++++++++++
 19 files changed, 730 insertions(+), 14 deletions(-)
 create mode 100644 doc/install/install-arv-git-httpd.html.textile.liquid
 create mode 100644 services/arv-git-httpd/.gitignore
 create mode 100644 services/arv-git-httpd/auth_handler.go
 create mode 100644 services/arv-git-httpd/basic_auth_go13.go
 create mode 100644 services/arv-git-httpd/basic_auth_go14.go
 create mode 100644 services/arv-git-httpd/basic_auth_test.go
 create mode 100644 services/arv-git-httpd/doc.go
 create mode 100644 services/arv-git-httpd/main.go
 create mode 100644 services/arv-git-httpd/server.go
 create mode 100644 services/arv-git-httpd/server_test.go

  discards  d66803ae05f4e4ac60c7bc8fb2e22b3a825bd95c (commit)
  discards  ea103bd3ffbb3a9b342b7f6eabf3df818f914572 (commit)
  discards  585a8655a91f8410e34ca1c75acbf4bc7e6f94c3 (commit)
  discards  ebd5b4a23e1d782946c6d93a77cd266c0df795d1 (commit)
  discards  0ce4725953f601a54f0505ceced900133f5de81b (commit)
  discards  7ca7892c32d6de77729c43ac09b935d738a9cbbe (commit)
  discards  5ec73064459a57d987226cdc29b3e0233e8c33cd (commit)
  discards  271063891be573762ad2e8e48d3ba69368d34883 (commit)
  discards  6c6a37bf3d37e3116ae39536b1ea56b077de9dd1 (commit)
  discards  2c65feefb8c3fc8539e18939b444050435f58a80 (commit)
  discards  2013ac2400a36ea6154e84e1991944afc536c9a0 (commit)
  discards  dd2749162fb95d7a975d664baa01d24f529bd2ae (commit)
  discards  f71727b4febac13b77141a62afb6f5313f589399 (commit)
  discards  75feaad81d9919387c88042ec2b6d61546c4164d (commit)
       via  8c7fb5f13cb167740b7b228c7fab40f2980d1978 (commit)
       via  8be6352fa0ee445d1c6dda862f659cc015e4865b (commit)
       via  cd28c9db4897c6d18936250fba5981018113cd49 (commit)
       via  9a6c3a5b18479c0f618457e96cd594948aa9db29 (commit)
       via  b68feeee5d07f4ad5964c5fdfc4f31a2033871e8 (commit)
       via  dbf4d66b26dd1694faea8c26502efcd59d3f98c3 (commit)
       via  920a201e05cc5fda3fbb82d82150f30b2263f7ec (commit)
       via  db36b8ad5184c46c0bcbb42a384f298a6c45dee5 (commit)
       via  d843787b4ece9952597d7814cbf10fb383c72625 (commit)
       via  ce4e75d03a38fc7e4a644a08e3c6044e3a7df390 (commit)
       via  38bc4c0e294817a70b61e7b545cdcaab5737eebd (commit)
       via  24584338a1937280ac6400f8d5371972185191cd (commit)
       via  452dace2a30db53753d98baa21905b32aac4b78d (commit)
       via  d1ad9601654a26643b73f871ac58b47ee04e6479 (commit)
       via  1f3874e876617406da51ed61aded11eb7166bc49 (commit)
       via  f2d1eec06a7f44fcd2ded1939d6a2fcfad50840a (commit)
       via  25520da76bbd3c00223cf401a4b48de4681f02e8 (commit)
       via  b4f4cbf7ef41492d36c81eb83239f6e4c6744a6a (commit)
       via  ad69cab04c31db7ed78ced64e3e1fa814775d6d7 (commit)
       via  7521d023fbeb1cabc9e89953e29cd55183af3753 (commit)
       via  b93018f033f37a7a7b719fd7dc6d5b3ad4a0f797 (commit)
       via  c106899198b67d1071ccf164ce743b651981f8cd (commit)
       via  eb68ddf0ab91faf6ce1dbab4b8db81b56ffeccaf (commit)
       via  16421a1aa8d420728b51d7cbb1b006a5613f8697 (commit)

This update added new revisions after undoing existing revisions.  That is
to say, the old revision is not a strict subset of the new revision.  This
situation occurs when you --force push a change and generate a repository
containing something like this:

 * -- * -- B -- O -- O -- O (d66803ae05f4e4ac60c7bc8fb2e22b3a825bd95c)
            \
             N -- N -- N (8c7fb5f13cb167740b7b228c7fab40f2980d1978)

When this happens we assume that you've already had alert emails for all
of the O revisions, and so we here report only the revisions in the N
branch from the common base, B.

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 8c7fb5f13cb167740b7b228c7fab40f2980d1978
Author: Brett Smith <brett at curoverse.com>
Date:   Tue Mar 31 11:13:54 2015 -0400

    4253: Merge Repository permission requirements of #4253 and #5416.

diff --git a/services/api/app/models/arvados_model.rb b/services/api/app/models/arvados_model.rb
index 1fe5808..02e9386 100644
--- a/services/api/app/models/arvados_model.rb
+++ b/services/api/app/models/arvados_model.rb
@@ -308,8 +308,13 @@ class ArvadosModel < ActiveRecord::Base
     # Verify "write" permission on new owner
     # default fail unless one of:
     # current_user is this object
-    # current user can_write new owner
-    unless current_user == self or current_user.can? write: owner_uuid
+    # current user can_write new owner, or this object if owner unchanged
+    if new_record? or owner_uuid_changed? or is_a?(ApiClientAuthorization)
+      write_target = owner_uuid
+    else
+      write_target = uuid
+    end
+    unless current_user == self or current_user.can? write: write_target
       logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{uuid} but does not have permission to write new owner_uuid #{owner_uuid}"
       errors.add :owner_uuid, "cannot be changed without write permission on new owner"
       raise PermissionDeniedError
diff --git a/services/api/app/models/repository.rb b/services/api/app/models/repository.rb
index 5bc467c..0cab4dc 100644
--- a/services/api/app/models/repository.rb
+++ b/services/api/app/models/repository.rb
@@ -47,14 +47,15 @@ class Repository < ArvadosModel
   protected
 
   def permission_to_update
-    return false if not current_user
-    return true if current_user.is_admin
-    # For normal objects, this is a way to check whether you have
-    # write permission. Repositories should be brought closer to the
-    # normal permission model during #4253. Meanwhile, we'll
-    # special-case this so arv-git-httpd can detect write permission:
-    return super if changed_attributes.keys - ['modified_at', 'updated_at'] == []
-    false
+    if not super
+      false
+    elsif current_user.is_admin
+      true
+    elsif name_changed?
+      current_user.uuid == owner_uuid
+    else
+      true
+    end
   end
 
   def owner
diff --git a/services/api/test/unit/repository_test.rb b/services/api/test/unit/repository_test.rb
index a6b0be5..5acef1b 100644
--- a/services/api/test/unit/repository_test.rb
+++ b/services/api/test/unit/repository_test.rb
@@ -238,19 +238,6 @@ class RepositoryTest < ActiveSupport::TestCase
     end
   end
 
-  test 'write permission not sufficient for changing name' do
-    act_as_user users(:active) do
-      r = repositories(:foo)
-      name_was = r.name
-      r.name = 'newname'
-      assert_raises ArvadosModel::PermissionDeniedError do
-        r.save!
-      end
-      r.reload
-      assert_equal name_was, r.name
-    end
-  end
-
   test 'write permission necessary for changing modified_at' do
     act_as_user users(:spectator) do
       r = repositories(:foo)

commit 8be6352fa0ee445d1c6dda862f659cc015e4865b
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Mar 30 14:37:30 2015 -0400

    4253: Use same repo name logic whether or not user already existed before setup.

diff --git a/services/api/app/controllers/arvados/v1/users_controller.rb b/services/api/app/controllers/arvados/v1/users_controller.rb
index b5ac195..345cd46 100644
--- a/services/api/app/controllers/arvados/v1/users_controller.rb
+++ b/services/api/app/controllers/arvados/v1/users_controller.rb
@@ -96,24 +96,24 @@ class Arvados::V1::UsersController < ApplicationController
       end
     end
 
-    # It's not always possible to know the user's username when submitting
-    # this request.  If it included a plain repository name, expand that to a
-    # user-owned name now.
+    # It's not always possible for the client to know the user's
+    # username when submitting this request: the username might have
+    # been assigned automatically in create!() above. If client
+    # provided a plain repository name, prefix it with the username
+    # now that we know what it is.
     if params[:repo_name].nil?
       full_repo_name = nil
+    elsif @object.username.nil?
+      raise ArgumentError.
+        new("cannot setup a repository because user has no username")
+    elsif params[:repo_name].include?("/")
+      full_repo_name = params[:repo_name]
     else
       full_repo_name = "#{@object.username}/#{params[:repo_name]}"
     end
+
     if object_found
-      if params[:repo_name].andand.include?("/")
-        repo_name = params[:repo_name]
-      elsif @object.username.nil?
-        raise ArgumentError.
-          new("can't setup a user without a username with a repository")
-      else
-        repo_name = full_repo_name
-      end
-      @response = @object.setup_repo_vm_links repo_name,
+      @response = @object.setup_repo_vm_links full_repo_name,
                     params[:vm_uuid], params[:openid_prefix]
     else
       @response = User.setup @object, params[:openid_prefix],

commit cd28c9db4897c6d18936250fba5981018113cd49
Author: Brett Smith <brett at curoverse.com>
Date:   Tue Mar 31 09:24:42 2015 -0400

    4253: Add tests for renaming repos.

diff --git a/services/api/test/test_helper.rb b/services/api/test/test_helper.rb
index f155fc0..bf5afea 100644
--- a/services/api/test/test_helper.rb
+++ b/services/api/test/test_helper.rb
@@ -62,6 +62,15 @@ class ActiveSupport::TestCase
     end
   end
 
+  def add_permission_link from_who, to_what, perm_type
+    act_as_system_user do
+      Link.create!(tail_uuid: from_who.uuid,
+                   head_uuid: to_what.uuid,
+                   link_class: 'permission',
+                   name: perm_type)
+    end
+  end
+
   def restore_configuration
     # Restore configuration settings changed during tests
     $application_config.each do |k,v|
diff --git a/services/api/test/unit/repository_test.rb b/services/api/test/unit/repository_test.rb
index ef780f8..a6b0be5 100644
--- a/services/api/test/unit/repository_test.rb
+++ b/services/api/test/unit/repository_test.rb
@@ -263,4 +263,26 @@ class RepositoryTest < ActiveSupport::TestCase
       assert_equal modtime_was, r.modified_at
     end
   end
+
+  ### Renaming
+
+  test "non-admin can rename own repo" do
+    act_as_user users(:active) do
+      assert repositories(:foo).update_attributes(name: 'active/foo12345')
+    end
+  end
+
+  test "top level repo can be touched by non-admin with can_manage" do
+    add_permission_link users(:active), repositories(:arvados), 'can_manage'
+    act_as_user users(:active) do
+      assert changed_repo(:arvados, modified_at: Time.now).save
+    end
+  end
+
+  test "top level repo cannot be renamed by non-admin with can_manage" do
+    add_permission_link users(:active), repositories(:arvados), 'can_manage'
+    act_as_user users(:active) do
+      assert_not_allowed { changed_repo(:arvados, name: 'xarvados').save }
+    end
+  end
 end

commit 9a6c3a5b18479c0f618457e96cd594948aa9db29
Author: Brett Smith <brett at curoverse.com>
Date:   Mon Mar 30 16:06:38 2015 -0400

    4253: Add Workbench interface to create repositories.

diff --git a/apps/workbench/app/assets/javascripts/add_repository.js b/apps/workbench/app/assets/javascripts/add_repository.js
new file mode 100644
index 0000000..9594f9c
--- /dev/null
+++ b/apps/workbench/app/assets/javascripts/add_repository.js
@@ -0,0 +1,38 @@
+$(document).on('shown.bs.modal', '#add-repository-modal', function(event) {
+    $('input[type=text]', event.target).val('');
+    $('#add-repository-error', event.target).hide();
+}).on('submit', '#add-repository-form', function(event) {
+    var $form = $(event.target),
+    $submit = $(':submit', $form),
+    $error = $('#add-repository-error', $form),
+    repo_owner_uuid = $('input[name="add_repo_owner_uuid"]', $form).val(),
+    repo_prefix = $('input[name="add_repo_prefix"]', $form).val(),
+    repo_basename = $('input[name="add_repo_basename"]', $form).val();
+
+    $submit.prop('disabled', true);
+    $error.hide();
+    $.ajax('/repositories',
+           {method: 'POST',
+            dataType: 'json',
+            data: {repository: {owner_uuid: repo_owner_uuid,
+                                name: repo_prefix + repo_basename}},
+            context: $form}).
+        done(function(data, status, jqxhr) {
+            location.reload();
+        }).
+        fail(function(jqxhr, status, error) {
+            var errlist = jqxhr.responseJSON.errors;
+            var errmsg;
+            if (Array.isArray(errlist)) {
+                errmsg = errlist.join();
+            } else {
+                errmsg = ("The server returned an error when making " +
+                          "this repository (status " + jqxhr.status +
+                          ": " + errlist + ").");
+            }
+            $error.text(errmsg);
+            $error.show();
+            $submit.prop('disabled', false);
+        });
+    return false;
+});
diff --git a/apps/workbench/app/views/users/_add_repository_modal.html.erb b/apps/workbench/app/views/users/_add_repository_modal.html.erb
new file mode 100644
index 0000000..db74ec5
--- /dev/null
+++ b/apps/workbench/app/views/users/_add_repository_modal.html.erb
@@ -0,0 +1,41 @@
+<%
+   if current_user.uuid.ends_with?("-000000000000000")
+     repo_prefix = ""
+   else
+     repo_prefix = current_user.username + "/"
+   end
+-%>
+<div class="modal" id="add-repository-modal" tabindex="-1" role="dialog" aria-labelledby="add-repository-label" aria-hidden="true">
+  <div class="modal-dialog">
+    <div class="modal-content">
+      <form id="add-repository-form">
+        <input type="hidden" id="add_repo_owner_uuid" name="add_repo_owner_uuid" value="<%= current_user.uuid %>">
+        <input type="hidden" id="add_repo_prefix" name="add_repo_prefix" value="<%= repo_prefix %>">
+        <div class="modal-header">
+          <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
+          <h4 class="modal-title" id="add-repository-label">Add new repository</h4>
+        </div>
+        <div class="modal-body form-horizontal">
+          <div class="form-group">
+            <label for="add_repo_basename" class="col-sm-2 control-label">Name</label>
+            <div class="col-sm-10">
+              <div class="input-group arvados-uuid">
+                <% unless repo_prefix.empty? %>
+                  <span class="input-group-addon"><%= repo_prefix %></span>
+                <% end %>
+                <input type="text" class="form-control" id="add_repo_basename" name="add_repo_basename">
+                <span class="input-group-addon">.git</span>
+              </div>
+            </div>
+          </div>
+          <p class="alert alert-info">It may take a minute or two before you can clone your new repository.</p>
+          <p id="add-repository-error" class="alert alert-danger"></p>
+        </div>
+        <div class="modal-footer">
+          <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+          <input type="submit" class="btn btn-primary" id="add-repository-submit" name="submit" value="Create">
+        </div>
+      </form>
+    </div>
+  </div>
+</div>
diff --git a/apps/workbench/app/views/users/_manage_account.html.erb b/apps/workbench/app/views/users/_manage_account.html.erb
index 2d36b30..4a362cd 100644
--- a/apps/workbench/app/views/users/_manage_account.html.erb
+++ b/apps/workbench/app/views/users/_manage_account.html.erb
@@ -18,4 +18,5 @@
     </div>
   </div>
   <div id="add-ssh-key-modal-window" class="modal fade" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"></div>
+  <%= render partial: "add_repository_modal" %>
 </div>
diff --git a/apps/workbench/app/views/users/_manage_repositories.html.erb b/apps/workbench/app/views/users/_manage_repositories.html.erb
index 62c8e8d..bcf0d64 100644
--- a/apps/workbench/app/views/users/_manage_repositories.html.erb
+++ b/apps/workbench/app/views/users/_manage_repositories.html.erb
@@ -1,5 +1,10 @@
 <div class="panel panel-default">
   <div class="panel-heading">
+    <div class="pull-right">
+      <%= link_to raw('<i class="fa fa-plus"></i> Add new repository'), "#",
+                   {class: 'btn btn-xs btn-primary', 'data-toggle' => "modal",
+                    'data-target' => '#add-repository-modal'}  %>
+    </div>
     <h4 class="panel-title">
       <a data-parent="#arv-adv-accordion" href="#manage_repositories">
         Repositories
diff --git a/apps/workbench/test/integration/user_manage_account_test.rb b/apps/workbench/test/integration/user_manage_account_test.rb
index 9b5e5d6..6d680e2 100644
--- a/apps/workbench/test/integration/user_manage_account_test.rb
+++ b/apps/workbench/test/integration/user_manage_account_test.rb
@@ -171,4 +171,14 @@ class UserManageAccountTest < ActionDispatch::IntegrationTest
     assert_text 'A request for shell access was sent on '
     assert_selector 'a', text: 'Send request for shell access'
   end
+
+  test "create new repository" do
+    visit page_with_token("active_trustedclient", "/manage_account")
+    click_on "Add new repository"
+    within ".modal-dialog" do
+      fill_in "Name", with: "workbenchtest"
+      click_on "Create"
+    end
+    assert_text ":active/workbenchtest.git"
+  end
 end

commit b68feeee5d07f4ad5964c5fdfc4f31a2033871e8
Author: Brett Smith <brett at curoverse.com>
Date:   Mon Mar 30 14:28:40 2015 -0400

    4253: Let Workbench Manage Account partials render their whole pane.
    
    I'm about to add another "Add" button to the Repositories pane, so I'd
    rather do it this way than try to maintain a generic loop inside
    _manage_account.html.erb.

diff --git a/apps/workbench/app/views/users/_manage_account.html.erb b/apps/workbench/app/views/users/_manage_account.html.erb
index 5024fce..2d36b30 100644
--- a/apps/workbench/app/views/users/_manage_account.html.erb
+++ b/apps/workbench/app/views/users/_manage_account.html.erb
@@ -1,52 +1,21 @@
 <div class="col-sm-6">
   <div class="panel-group" id="arv-adv-accordion">
-    <% ['Virtual Machines',
-       'Repositories'].each do |section| %>
-      <% section_id = section.gsub(" ","_").downcase %>
-      <div class="panel panel-default">
-        <div class="panel-heading">
-          <h4 class="panel-title">
-            <a data-parent="#arv-adv-accordion" href="#manage_<%=section_id%>">
-              <%= section %>
-            </a>
-          </h4>
-        </div>
-        <div id="manage_<%=section_id%>">
-          <div class="panel-body">
-            <%= render partial: "manage_#{section_id}" %>
-          </div>
-        </div>
-      </div>
-    <% end %>
+    <div class="panel panel-default">
+      <%= render partial: "manage_virtual_machines" %>
+    </div>
+    <div class="panel panel-default">
+      <%= render partial: "manage_repositories" %>
+    </div>
   </div>
 </div>
 <div class="col-sm-6">
   <div class="panel-group" id="arv-adv-accordion">
-    <% ['SSH Keys',
-      'Current Token'].each do |section| %>
-      <% section_id = section.gsub(" ","_").downcase %>
-      <div class="panel panel-default">
-        <div class="panel-heading">
-          <% if section_id == 'ssh_keys' %>
-            <div class="pull-right">
-              <%= link_to raw('<i class="fa fa-plus"></i>' " Add new SSH key"), add_ssh_key_popup_url,
-                           {class: 'btn btn-xs btn-primary', :remote => true, 'data-toggle' =>  "modal",
-                            'data-target' => '#add-ssh-key-modal-window'}  %>
-            </div>
-          <% end %>
-          <h4 class="panel-title">
-            <a data-parent="#arv-adv-accordion" href="#manage_<%=section_id%>">
-              <%= section %>
-            </a>
-          </h4>
-        </div>
-        <div id="manage_<%=section_id%>">
-          <div class="panel-body">
-            <%= render partial: "manage_#{section_id}" %>
-          </div>
-        </div>
-      </div>
-    <% end %>
+    <div class="panel panel-default">
+      <%= render partial: "manage_ssh_keys" %>
+    </div>
+    <div class="panel panel-default">
+      <%= render partial: "manage_current_token" %>
+    </div>
   </div>
   <div id="add-ssh-key-modal-window" class="modal fade" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"></div>
 </div>
diff --git a/apps/workbench/app/views/users/_manage_current_token.html.erb b/apps/workbench/app/views/users/_manage_current_token.html.erb
index 71c6bd2..f4436ba 100644
--- a/apps/workbench/app/views/users/_manage_current_token.html.erb
+++ b/apps/workbench/app/views/users/_manage_current_token.html.erb
@@ -1,3 +1,13 @@
+<div class="panel panel-default">
+  <div class="panel-heading">
+    <h4 class="panel-title">
+      <a data-parent="#arv-adv-accordion" href="#manage_current_token">
+        Current Token
+      </a>
+    </h4>
+  </div>
+
+<div id="#manage_current_token" class="panel-body">
 <p>The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions. For more information see <%= link_to raw('Getting an API token'), "#{Rails.configuration.arvados_docsite}/user/reference/api-tokens.html", target: "_blank"%>.</p>
 <p>Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your account, <b><%= current_user.email %></b></p>
 
@@ -11,3 +21,5 @@ export ARVADOS_API_HOST_INSECURE=true
 unset ARVADOS_API_HOST_INSECURE
 <% end %>
 </pre>
+</div>
+</div>
diff --git a/apps/workbench/app/views/users/_manage_repositories.html.erb b/apps/workbench/app/views/users/_manage_repositories.html.erb
index e7c6d24..62c8e8d 100644
--- a/apps/workbench/app/views/users/_manage_repositories.html.erb
+++ b/apps/workbench/app/views/users/_manage_repositories.html.erb
@@ -1,4 +1,13 @@
-<div>
+<div class="panel panel-default">
+  <div class="panel-heading">
+    <h4 class="panel-title">
+      <a data-parent="#arv-adv-accordion" href="#manage_repositories">
+        Repositories
+      </a>
+    </h4>
+  </div>
+
+<div id="manage_repositories" class="panel-body">
   <p>
     For more information see <%= link_to raw('Writing a pipeline'),
     "#{Rails.configuration.arvados_docsite}/user/tutorials/tutorial-firstscript.html", target: "_blank"%>.
@@ -45,3 +54,4 @@
     </table>
   <% end %>
 </div>
+</div>
diff --git a/apps/workbench/app/views/users/_manage_ssh_keys.html.erb b/apps/workbench/app/views/users/_manage_ssh_keys.html.erb
index 1ea8f0b..951b79c 100644
--- a/apps/workbench/app/views/users/_manage_ssh_keys.html.erb
+++ b/apps/workbench/app/views/users/_manage_ssh_keys.html.erb
@@ -1,4 +1,18 @@
-<div>
+<div class="panel panel-default">
+  <div class="panel-heading">
+    <div class="pull-right">
+      <%= link_to raw('<i class="fa fa-plus"></i>' " Add new SSH key"), add_ssh_key_popup_url,
+                   {class: 'btn btn-xs btn-primary', :remote => true, 'data-toggle' =>  "modal",
+                    'data-target' => '#add-ssh-key-modal-window'}  %>
+    </div>
+    <h4 class="panel-title">
+      <a data-parent="#arv-adv-accordion" href="#manage_ssh_keys">
+        SSH Keys
+      </a>
+    </h4>
+  </div>
+
+<div id="manage_ssh_keys" class="panel-body">
   <% if !@my_ssh_keys.any? %>
      <p> You have not yet set up an SSH public key for use with Arvados. </p>
      <p>  <%= link_to "Click here",
@@ -53,3 +67,4 @@
     </table>
   <% end %>
 </div>
+</div>
diff --git a/apps/workbench/app/views/users/_manage_virtual_machines.html.erb b/apps/workbench/app/views/users/_manage_virtual_machines.html.erb
index 43f2b77..d5d9f52 100644
--- a/apps/workbench/app/views/users/_manage_virtual_machines.html.erb
+++ b/apps/workbench/app/views/users/_manage_virtual_machines.html.erb
@@ -1,4 +1,13 @@
-<div>
+<div class="panel panel-default">
+  <div class="panel-heading">
+    <h4 class="panel-title">
+      <a data-parent="#arv-adv-accordion" href="#manage_virtual_machines">
+        Virtual Machines
+      </a>
+    </h4>
+  </div>
+
+<div id="manage_virtual_machines" class="panel-body">
   <p>
     For more information see <%= link_to raw('Arvados Docs → User Guide → SSH access'),
   "#{Rails.configuration.arvados_docsite}/user/getting_started/ssh-access-unix.html",
@@ -88,3 +97,4 @@
     </pre>
   <% end %>
 </div>
+</div>

commit dbf4d66b26dd1694faea8c26502efcd59d3f98c3
Author: Brett Smith <brett at curoverse.com>
Date:   Mon Mar 30 11:06:51 2015 -0400

    4253: Sync up Docker with our production Gitolite setup.
    
    This updates our API server Docker image to store repositories by
    UUID, with name aliases available.  .gitolite.rc enables the aliasing,
    and update-gitolite.rb generates the necessary configuration.  This
    makes it possible to test the recent repository changes in Docker.

diff --git a/docker/api/.gitolite.rc b/docker/api/.gitolite.rc
new file mode 100644
index 0000000..855e103
--- /dev/null
+++ b/docker/api/.gitolite.rc
@@ -0,0 +1,191 @@
+# configuration variables for gitolite
+
+# This file is in perl syntax.  But you do NOT need to know perl to edit it --
+# just mind the commas, use single quotes unless you know what you're doing,
+# and make sure the brackets and braces stay matched up!
+
+# (Tip: perl allows a comma after the last item in a list also!)
+
+# HELP for commands can be had by running the command with "-h".
+
+# HELP for all the other FEATURES can be found in the documentation (look for
+# "list of non-core programs shipped with gitolite" in the master index) or
+# directly in the corresponding source file.
+
+my $repo_aliases;
+my $aliases_src = "$ENV{HOME}/.gitolite/arvadosaliases.pl";
+if ($ENV{HOME} && (-e $aliases_src)) {
+    $repo_aliases = do $aliases_src;
+}
+$repo_aliases ||= {};
+
+%RC = (
+
+    # ------------------------------------------------------------------
+
+    # default umask gives you perms of '0700'; see the rc file docs for
+    # how/why you might change this
+    UMASK                           =>  0022,
+
+    # look for "git-config" in the documentation
+    GIT_CONFIG_KEYS                 =>  '',
+
+    # comment out if you don't need all the extra detail in the logfile
+    LOG_EXTRA                       =>  1,
+
+    # roles.  add more roles (like MANAGER, TESTER, ...) here.
+    #   WARNING: if you make changes to this hash, you MUST run 'gitolite
+    #   compile' afterward, and possibly also 'gitolite trigger POST_COMPILE'
+    ROLES => {
+        READERS                     =>  1,
+        WRITERS                     =>  1,
+    },
+
+    REPO_ALIASES => $repo_aliases,
+
+    # ------------------------------------------------------------------
+
+    # rc variables used by various features
+
+    # the 'info' command prints this as additional info, if it is set
+        # SITE_INFO                 =>  'Please see http://blahblah/gitolite for more help',
+
+    # the 'desc' command uses this
+        # WRITER_CAN_UPDATE_DESC    =>  1,
+    # the 'readme' command uses this
+        # WRITER_CAN_UPDATE_README  =>  1,
+
+    # the CpuTime feature uses these
+        # display user, system, and elapsed times to user after each git operation
+        # DISPLAY_CPU_TIME          =>  1,
+        # display a warning if total CPU times (u, s, cu, cs) crosses this limit
+        # CPU_TIME_WARN_LIMIT       =>  0.1,
+
+    # the Mirroring feature needs this
+        # HOSTNAME                  =>  "foo",
+
+    # if you enabled 'Shell', you need this
+        # SHELL_USERS_LIST          =>  "$ENV{HOME}/.gitolite.shell-users",
+
+    # ------------------------------------------------------------------
+
+    # suggested locations for site-local gitolite code (see cust.html)
+
+        # this one is managed directly on the server
+        # LOCAL_CODE                =>  "$ENV{HOME}/local",
+
+        # or you can use this, which lets you put everything in a subdirectory
+        # called "local" in your gitolite-admin repo.  For a SECURITY WARNING
+        # on this, see http://gitolite.com/gitolite/cust.html#pushcode
+        # LOCAL_CODE                =>  "$rc{GL_ADMIN_BASE}/local",
+
+    # ------------------------------------------------------------------
+
+    # List of commands and features to enable
+
+    ENABLE => [
+
+        # COMMANDS
+
+            # These are the commands enabled by default
+            'help',
+            'desc',
+            'info',
+            'perms',
+            'writable',
+
+            # Uncomment or add new commands here.
+            # 'create',
+            # 'fork',
+            # 'mirror',
+            # 'readme',
+            # 'sskm',
+            # 'D',
+
+        # These FEATURES are enabled by default.
+
+            # essential (unless you're using smart-http mode)
+            'ssh-authkeys',
+
+            # creates git-config enties from gitolite.conf file entries like 'config foo.bar = baz'
+            'git-config',
+
+            # creates git-daemon-export-ok files; if you don't use git-daemon, comment this out
+            'daemon',
+
+            # creates projects.list file; if you don't use gitweb, comment this out
+            'gitweb',
+
+        # These FEATURES are disabled by default; uncomment to enable.  If you
+        # need to add new ones, ask on the mailing list :-)
+
+        # user-visible behaviour
+
+            # prevent wild repos auto-create on fetch/clone
+            # 'no-create-on-read',
+            # no auto-create at all (don't forget to enable the 'create' command!)
+            # 'no-auto-create',
+
+            # access a repo by another (possibly legacy) name
+            'Alias',
+
+            # give some users direct shell access
+            # 'Shell',
+
+            # set default roles from lines like 'option default.roles-1 = ...', etc.
+            # 'set-default-roles',
+
+            # show more detailed messages on deny
+            # 'expand-deny-messages',
+
+        # system admin stuff
+
+            # enable mirroring (don't forget to set the HOSTNAME too!)
+            # 'Mirroring',
+
+            # allow people to submit pub files with more than one key in them
+            # 'ssh-authkeys-split',
+
+            # selective read control hack
+            # 'partial-copy',
+
+            # manage local, gitolite-controlled, copies of read-only upstream repos
+            # 'upstream',
+
+            # updates 'description' file instead of 'gitweb.description' config item
+            # 'cgit',
+
+            # allow repo-specific hooks to be added
+            # 'repo-specific-hooks',
+
+        # performance, logging, monitoring...
+
+            # be nice
+            # 'renice 10',
+
+            # log CPU times (user, system, cumulative user, cumulative system)
+            # 'CpuTime',
+
+        # syntactic_sugar for gitolite.conf and included files
+
+            # allow backslash-escaped continuation lines in gitolite.conf
+            # 'continuation-lines',
+
+            # create implicit user groups from directory names in keydir/
+            # 'keysubdirs-as-groups',
+
+            # allow simple line-oriented macros
+            # 'macros',
+
+    ],
+
+);
+
+# ------------------------------------------------------------------------------
+# per perl rules, this should be the last line in such a file:
+1;
+
+# Local variables:
+# mode: perl
+# End:
+# vim: set syn=perl:
diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile
index 07acb63..e0b353c 100644
--- a/docker/api/Dockerfile
+++ b/docker/api/Dockerfile
@@ -68,6 +68,7 @@ ADD keep_proxy.json /root/
 # Set up update-gitolite.rb
 RUN mkdir /usr/local/arvados/config -p
 ADD generated/arvados-clients.yml /usr/local/arvados/config/
+ADD .gitolite.rc /usr/local/arvados/config/
 ADD update-gitolite.rb /usr/local/arvados/
 
 # Supervisor.
diff --git a/docker/api/setup-gitolite.sh.in b/docker/api/setup-gitolite.sh.in
index 92014f9..ec55482 100755
--- a/docker/api/setup-gitolite.sh.in
+++ b/docker/api/setup-gitolite.sh.in
@@ -13,10 +13,7 @@ su - git -c "mkdir -p ~/bin"
 su - git -c "git clone git://github.com/sitaramc/gitolite"
 su - git -c "gitolite/install -ln ~/bin"
 su - git -c "PATH=/home/git/bin:$PATH gitolite setup -pk ~git/root-authorized_keys.pub"
-
-# Make sure the repositories are created in such a way that they are readable
-# by the api server
-sed -i 's/0077/0022/g' /home/git/.gitolite.rc
+install -o git -g git -m 600 /usr/local/arvados/config/.gitolite.rc /home/git/
 
 # And make sure that the existing repos are equally readable, or the API server commit model will freak out...
 chmod 755 /home/git/repositories
diff --git a/docker/api/update-gitolite.rb b/docker/api/update-gitolite.rb
index 2c46a0d..8247931 100755
--- a/docker/api/update-gitolite.rb
+++ b/docker/api/update-gitolite.rb
@@ -3,7 +3,7 @@
 require 'rubygems'
 require 'pp'
 require 'arvados'
-require 'active_support/all'
+require 'tempfile'
 require 'yaml'
 
 # This script does the actual gitolite config management on disk.
@@ -29,9 +29,12 @@ else
 end
 
 gitolite_url = cp_config['gitolite_url']
-gitolite_tmp = cp_config['gitolite_tmp']
+gitolite_arvados_git_user_key = cp_config['gitolite_arvados_git_user_key']
 
-gitolite_admin = File.join(File.expand_path(File.dirname(__FILE__)) + '/' + gitolite_tmp + '/gitolite-admin')
+gitolite_tmpdir = File.join(File.absolute_path(File.dirname(__FILE__)),
+                            cp_config['gitolite_tmp'])
+gitolite_admin = File.join(gitolite_tmpdir, 'gitolite-admin')
+gitolite_keydir = File.join(gitolite_admin, 'keydir', 'arvados')
 
 ENV['ARVADOS_API_HOST'] = cp_config['arvados_api_host']
 ENV['ARVADOS_API_TOKEN'] = cp_config['arvados_api_token']
@@ -41,118 +44,209 @@ else
   ENV.delete('ARVADOS_API_HOST_INSECURE')
 end
 
-keys = ''
+def ensure_directory(path, mode)
+  begin
+    Dir.mkdir(path, mode)
+  rescue Errno::EEXIST
+  end
+end
 
-seen = Hash.new
+def replace_file(path, contents)
+  unlink_now = true
+  dirname, basename = File.split(path)
+  new_file = Tempfile.new([basename, ".tmp"], dirname)
+  begin
+    new_file.write(contents)
+    new_file.flush
+    File.rename(new_file, path)
+    unlink_now = false
+  ensure
+    new_file.close(unlink_now)
+  end
+end
 
-def ensure_repo(name,permissions,user_keys,gitolite_admin)
-  tmp = ''
-  # Just in case...
-  name.gsub!(/[^a-z0-9]/i,'')
+def file_has_contents?(path, contents)
+  begin
+    IO.read(path) == contents
+  rescue Errno::ENOENT
+    false
+  end
+end
 
-  keys = Hash.new()
+module TrackCommitState
+  module ClassMethods
+    # Note that all classes that include TrackCommitState will have
+    # @@need_commit = true if any of them set it.  Since this flag reports
+    # a boolean state of the underlying git repository, that's OK in the
+    # current implementation.
+    @@need_commit = false
 
-  user_keys.each do |uuid,p|
-    p.each do |k|
-      next if k[:public_key].nil?
-      keys[uuid] = Array.new() if not keys.key?(uuid)
+    def changed?
+      @@need_commit
+    end
 
-      key = k[:public_key]
-      # Handle putty-style ssh public keys
-      key.sub!(/^(Comment: "r[^\n]*\n)(.*)$/m,'ssh-rsa \2 \1')
-      key.sub!(/^(Comment: "d[^\n]*\n)(.*)$/m,'ssh-dss \2 \1')
-      key.gsub!(/\n/,'')
-      key.strip
+    def ensure_in_git(path, contents)
+      unless file_has_contents?(path, contents)
+        replace_file(path, contents)
+        system("git", "add", path)
+        @@need_commit = true
+      end
+    end
+  end
+
+  def ensure_in_git(path, contents)
+    self.class.ensure_in_git(path, contents)
+  end
 
-      keys[uuid].push(key)
+  def self.included(base)
+    base.extend(ClassMethods)
+  end
+end
+
+class UserSSHKeys
+  include TrackCommitState
+
+  def initialize(user_keys_map, key_dir)
+    @user_keys_map = user_keys_map
+    @key_dir = key_dir
+    @installed = {}
+  end
+
+  def install(filename, pubkey)
+    unless pubkey.nil?
+      key_path = File.join(@key_dir, filename)
+      ensure_in_git(key_path, pubkey)
     end
+    @installed[filename] = true
   end
 
-  cf = gitolite_admin + '/conf/auto/' + name + '.conf'
+  def ensure_keys_for_user(user_uuid)
+    return unless key_list = @user_keys_map.delete(user_uuid)
+    key_list.map { |k| k[:public_key] }.compact.each_with_index do |pubkey, ii|
+      # Handle putty-style ssh public keys
+      pubkey.sub!(/^(Comment: "r[^\n]*\n)(.*)$/m,'ssh-rsa \2 \1')
+      pubkey.sub!(/^(Comment: "d[^\n]*\n)(.*)$/m,'ssh-dss \2 \1')
+      pubkey.gsub!(/\n/,'')
+      pubkey.strip!
+      install("#{user_uuid}@#{ii}.pub", pubkey)
+    end
+  end
 
-  conf = "\nrepo #{name}\n"
+  def installed?(filename)
+    @installed[filename]
+  end
+end
 
-  commit = false
+class Repository
+  include TrackCommitState
 
-  seen = {}
-  permissions.sort.each do |uuid,v|
-    conf += "\t#{v[:gitolite_permissions]}\t= #{uuid.to_s}\n"
+  @@aliases = {}
 
-    count = 0
-    keys.include?(uuid) and keys[uuid].each do |v|
-      kf = gitolite_admin + '/keydir/arvados/' + uuid.to_s + "@#{count}.pub"
-      seen[kf] = true
-      if !File.exists?(kf) or IO::read(kf) != v then
-        commit = true
-        f = File.new(kf + ".tmp",'w')
-        f.write(v)
-        f.close()
-        # File.rename will overwrite the destination file if it exists
-        File.rename(kf + ".tmp",kf);
-      end
-      count += 1
+  def initialize(arv_repo, user_keys)
+    @arv_repo = arv_repo
+    @user_keys = user_keys
+  end
+
+  def self.ensure_system_config(conf_root)
+    ensure_in_git(File.join(conf_root, "conf", "gitolite.conf"),
+                  %Q{include "auto/*.conf"\ninclude "admin/*.conf"\n})
+    ensure_in_git(File.join(conf_root, "arvadosaliases.pl"), alias_config)
+
+    conf_path = File.join(conf_root, "conf", "admin", "arvados.conf")
+    conf_file = %Q{
+ at arvados_git_user = arvados_git_user
+
+repo gitolite-admin
+     RW           = @arvados_git_user
+
+}
+    ensure_directory(File.dirname(conf_path), 0755)
+    ensure_in_git(conf_path, conf_file)
+  end
+
+  def ensure_config(conf_root)
+    if name and (File.exist?(auto_conf_path(conf_root, name)))
+      # This gitolite installation knows the repository by name, rather than
+      # UUID.  Leave it configured that way until a separate migration is run.
+      basename = name
+    else
+      basename = uuid
+      @@aliases[name] = uuid unless name.nil?
+    end
+    conf_file = "\nrepo #{basename}\n"
+    @arv_repo[:user_permissions].sort.each do |user_uuid, perm|
+      conf_file += "\t#{perm[:gitolite_permissions]}\t= #{user_uuid}\n"
+      @user_keys.ensure_keys_for_user(user_uuid)
     end
+    ensure_in_git(auto_conf_path(conf_root, basename), conf_file)
+  end
+
+  private
+
+  def auto_conf_path(conf_root, basename)
+    File.join(conf_root, "conf", "auto", "#{basename}.conf")
+  end
+
+  def uuid
+    @arv_repo[:uuid]
   end
 
-  if !File.exists?(cf) or IO::read(cf) != conf then
-    commit = true
-    f = File.new(cf + ".tmp",'w')
-    f.write(conf)
-    f.close()
-    # this is about as atomic as we can make the replacement of the file...
-    File.unlink(cf) if File.exists?(cf)
-    File.rename(cf + ".tmp",cf);
+  def name
+    if @arv_repo[:name].nil?
+      nil
+    else
+      @clean_name ||=
+        @arv_repo[:name].sub(/^[^A-Za-z]+/, "").gsub(/[^\w\.\/]/, "")
+    end
   end
 
-  return commit,seen
+  def self.alias_config
+    conf_s = "{\n"
+    @@aliases.sort.each do |(repo_name, repo_uuid)|
+      conf_s += "\t'#{repo_name}' \t=> '#{repo_uuid}',\n"
+    end
+    conf_s += "};\n"
+    conf_s
+  end
 end
 
 begin
-
-  pwd = Dir.pwd
   # Get our local gitolite-admin repo up to snuff
-  if not File.exists?(File.dirname(__FILE__) + '/' + gitolite_tmp) then
-    Dir.mkdir(File.join(File.dirname(__FILE__) + '/' + gitolite_tmp), 0700)
-  end
   if not File.exists?(gitolite_admin) then
-    Dir.chdir(File.join(File.dirname(__FILE__) + '/' + gitolite_tmp))
+    ensure_directory(gitolite_tmpdir, 0700)
+    Dir.chdir(gitolite_tmpdir)
     `git clone #{gitolite_url}`
+    Dir.chdir(gitolite_admin)
   else
     Dir.chdir(gitolite_admin)
     `git pull`
   end
-  Dir.chdir(pwd)
-
-  arv = Arvados.new( { :suppress_ssl_warnings => false } )
 
+  arv = Arvados.new
   permissions = arv.repository.get_all_permissions
 
-  repos = permissions[:repositories]
-  user_keys = permissions[:user_keys]
-
-  @commit = false
+  ensure_directory(gitolite_keydir, 0700)
+  user_ssh_keys = UserSSHKeys.new(permissions[:user_keys], gitolite_keydir)
+  # Make sure the arvados_git_user key is installed
+  user_ssh_keys.install('arvados_git_user.pub', gitolite_arvados_git_user_key)
 
-  @seen = {}
-
-  repos.each do |r|
-    next if r[:name].nil?
-    (@c, at s) = ensure_repo(r[:name],r[:user_permissions],user_keys,gitolite_admin)
-    @seen.merge!(@s)
-    @commit = true if @c
+  permissions[:repositories].each do |repo_record|
+    repo = Repository.new(repo_record, user_ssh_keys)
+    repo.ensure_config(gitolite_admin)
   end
+  Repository.ensure_system_config(gitolite_admin)
 
   # Clean up public key files that should not be present
-  Dir.glob(gitolite_admin + '/keydir/arvados/*.pub') do |key_file|
-    next if key_file =~ /arvados_git_user.pub$/
-    next if @seen.has_key?(key_file)
-    puts "Extra file #{key_file}"
-    @commit = true
-    Dir.chdir(gitolite_admin)
-    key_file.gsub!(/^#{gitolite_admin}\//,'')
-    `git rm #{key_file}`
+  Dir.chdir(gitolite_keydir)
+  stale_keys = Dir.glob('*.pub').reject do |key_file|
+    user_ssh_keys.installed?(key_file)
+  end
+  if stale_keys.any?
+    stale_keys.each { |key_file| puts "Extra file #{key_file}" }
+    system("git", "rm", "--quiet", *stale_keys)
   end
 
-  if @commit then
+  if UserSSHKeys.changed? or Repository.changed? or stale_keys.any?
     message = "#{Time.now().to_s}: update from API"
     Dir.chdir(gitolite_admin)
     `git add --all`
@@ -160,7 +254,7 @@ begin
     `git push`
   end
 
-rescue Exception => bang
+rescue => bang
   puts "Error: " + bang.to_s
   puts bang.backtrace.join("\n")
   exit 1

commit 920a201e05cc5fda3fbb82d82150f30b2263f7ec
Author: Brett Smith <brett at curoverse.com>
Date:   Tue Mar 31 09:23:56 2015 -0400

    4253: Users can manage their own repositories.
    
    This commit allows users to create their own repositories, as long as
    the repository name starts with their own username.
    
    To support this change, we've modified our Gitolite setup to store
    repositories primarily by UUID, with a name alias for easier
    checkout.  fetch_url and push_url become generated attributes
    accordingly.  This makes it easier to rename the repository later and
    allow checkouts to continue to work.

diff --git a/apps/workbench/app/views/users/_manage_repositories.html.erb b/apps/workbench/app/views/users/_manage_repositories.html.erb
index 83ec30a..e7c6d24 100644
--- a/apps/workbench/app/views/users/_manage_repositories.html.erb
+++ b/apps/workbench/app/views/users/_manage_repositories.html.erb
@@ -32,7 +32,7 @@
               <%= writable ? 'writable' : 'read-only' %>
             </td>
             <td style="word-break:break-all;">
-              <code><%= writable ? repo[:push_url] : repo[:fetch_url] %></code>
+              <code><%= writable ? repo.push_url : repo.fetch_url %></code>
             </td>
             <td>
               <% if writable == 'can_manage' %>
diff --git a/apps/workbench/test/integration/users_test.rb b/apps/workbench/test/integration/users_test.rb
index 80e6a71..a329f51 100644
--- a/apps/workbench/test/integration/users_test.rb
+++ b/apps/workbench/test/integration/users_test.rb
@@ -56,7 +56,7 @@ class UsersTest < ActionDispatch::IntegrationTest
     within '.modal-content' do
       find 'label', text: 'Virtual Machine'
       fill_in "email", :with => "foo at example.com"
-      fill_in "repo_name", :with => "test_repo"
+      fill_in "repo_name", :with => "newtestrepo"
       click_button "Submit"
       wait_for_ajax
     end
@@ -81,7 +81,7 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     click_link 'Advanced'
     click_link 'Metadata'
-    assert page.has_text? 'Repository: test_repo'
+    assert page.has_text? 'Repository: foo/newtestrepo'
     assert !(page.has_text? 'VirtualMachine:')
   end
 
@@ -106,7 +106,7 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     within '.modal-content' do
       find 'label', text: 'Virtual Machine'
-      fill_in "repo_name", :with => "test_repo"
+      fill_in "repo_name", :with => "activetestrepo"
       click_button "Submit"
     end
 
@@ -115,7 +115,7 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     click_link 'Advanced'
     click_link 'Metadata'
-    assert page.has_text? 'Repository: test_repo'
+    assert page.has_text? 'Repository: active/activetestrepo'
     assert !(page.has_text? 'VirtualMachine:')
 
     # Click on Setup button again and this time also choose a VM
@@ -123,7 +123,7 @@ class UsersTest < ActionDispatch::IntegrationTest
     click_link 'Setup Active User'
 
     within '.modal-content' do
-      fill_in "repo_name", :with => "second_test_repo"
+      fill_in "repo_name", :with => "activetestrepo2"
       select("testvm.shell", :from => 'vm_uuid')
       click_button "Submit"
     end
@@ -133,7 +133,7 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     click_link 'Advanced'
     click_link 'Metadata'
-    assert page.has_text? 'Repository: second_test_repo'
+    assert page.has_text? 'Repository: active/activetestrepo2'
     assert page.has_text? 'VirtualMachine: testvm.shell'
   end
 
@@ -181,16 +181,15 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     click_link 'Advanced'
     click_link 'Metadata'
-    assert !(page.has_text? 'Repository: test_repo')
-    assert !(page.has_text? 'Repository: second_test_repo')
-    assert !(page.has_text? 'VirtualMachine: testvm.shell')
+    assert page.has_no_text? 'Repository: active/'
+    assert page.has_no_text? 'VirtualMachine: testvm.shell'
 
     # setup user again and verify links present
     click_link 'Admin'
     click_link 'Setup Active User'
 
     within '.modal-content' do
-      fill_in "repo_name", :with => "second_test_repo"
+      fill_in "repo_name", :with => "activetestrepo"
       select("testvm.shell", :from => 'vm_uuid')
       click_button "Submit"
     end
@@ -200,7 +199,7 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     click_link 'Advanced'
     click_link 'Metadata'
-    assert page.has_text? 'Repository: second_test_repo'
+    assert page.has_text? 'Repository: active/activetestrepo'
     assert page.has_text? 'VirtualMachine: testvm.shell'
   end
 
diff --git a/doc/_includes/_tutorial_submit_job.liquid b/doc/_includes/_tutorial_submit_job.liquid
index 57063b3..3ea7602 100644
--- a/doc/_includes/_tutorial_submit_job.liquid
+++ b/doc/_includes/_tutorial_submit_job.liquid
@@ -2,7 +2,7 @@
   "name":"My md5 pipeline",
   "components":{
     "do_hash":{
-      "repository":"$USER",
+      "repository":"$USER/$USER",
       "script":"hash.py",
       "script_version":"master",
       "runtime_constraints":{
diff --git a/doc/api/methods/jobs.html.textile.liquid b/doc/api/methods/jobs.html.textile.liquid
index ac68129..b57858e 100644
--- a/doc/api/methods/jobs.html.textile.liquid
+++ b/doc/api/methods/jobs.html.textile.liquid
@@ -76,7 +76,7 @@ Run the script "crunch_scripts/hash.py" in the repository "you" using the "maste
 {
   "job": {
     "script": "hash.py",
-    "repository": "<b>you</b>",
+    "repository": "<b>you</b>/<b>you</b>",
     "script_version": "master",
     "script_parameters": {
       "input": "c1bad4b39ca5a924e481008009d94e32+210"
@@ -92,7 +92,7 @@ Run using exactly the version "d00220fb38d4b85ca8fc28a8151702a2b9d1dec5". Arvado
 {
   "job": {
     "script": "hash.py",
-    "repository": "<b>you</b>",
+    "repository": "<b>you</b>/<b>you</b>",
     "script_version": "d00220fb38d4b85ca8fc28a8151702a2b9d1dec5",
     "script_parameters": {
       "input": "c1bad4b39ca5a924e481008009d94e32+210"
@@ -108,7 +108,7 @@ Arvados should re-use a previous job if the "script_version" of the previous job
 {
   "job": {
     "script": "hash.py",
-    "repository": "<b>you</b>",
+    "repository": "<b>you</b>/<b>you</b>",
     "script_version": "master",
     "script_parameters": {
       "input": "c1bad4b39ca5a924e481008009d94e32+210"
@@ -126,27 +126,27 @@ The same behavior, using filters:
 {
   "job": {
     "script": "hash.py",
-    "repository": "<b>you</b>",
+    "repository": "<b>you</b>/<b>you</b>",
     "script_version": "master",
     "script_parameters": {
       "input": "c1bad4b39ca5a924e481008009d94e32+210"
     }
   },
   "filters": [["script", "=", "hash.py"],
-              ["repository", "=", "<b>you</b>"],
+              ["repository", "=", "<b>you</b>/<b>you</b>"],
               ["script_version", "in git", "earlier_version_tag"],
               ["script_version", "not in git", "blacklisted_version_tag"]],
   "find_or_create": true
 }
 </pre></notextile>
 
-Run the script "crunch_scripts/monte-carlo.py" in the repository "you" using the current "master" commit. Because it is marked as "nondeterministic", this job will not be considered as a suitable candidate for future job submissions that use the "find_or_create" feature.
+Run the script "crunch_scripts/monte-carlo.py" in the repository "you/you" using the current "master" commit. Because it is marked as "nondeterministic", this job will not be considered as a suitable candidate for future job submissions that use the "find_or_create" feature.
 
 <notextile><pre>
 {
   "job": {
     "script": "monte-carlo.py",
-    "repository": "<b>you</b>",
+    "repository": "<b>you</b>/<b>you</b>",
     "script_version": "master",
     "nondeterministic": true,
     "script_parameters": {
diff --git a/doc/api/schema/PipelineTemplate.html.textile.liquid b/doc/api/schema/PipelineTemplate.html.textile.liquid
index 2b215c2..444960a 100644
--- a/doc/api/schema/PipelineTemplate.html.textile.liquid
+++ b/doc/api/schema/PipelineTemplate.html.textile.liquid
@@ -51,7 +51,7 @@ This is a pipeline named "Filter MD5 hash values" with two components, "do_hash"
   "components": {
     "do_hash": {
       "script": "hash.py",
-      "repository": "<b>you</b>",
+      "repository": "<b>you</b>/<b>you</b>",
       "script_version": "master",
       "script_parameters": {
         "input": {
@@ -64,7 +64,7 @@ This is a pipeline named "Filter MD5 hash values" with two components, "do_hash"
     },
     "filter": {
       "script": "0-filter.py",
-      "repository": "<b>you</b>",
+      "repository": "<b>you</b>/<b>you</b>",
       "script_version": "master",
       "script_parameters": {
         "input": {
@@ -84,13 +84,13 @@ This pipeline consists of three components.  The components "thing1" and "thing2
   "components": {
     "cat_in_the_hat": {
       "script": "cat.py",
-      "repository": "<b>you</b>",
+      "repository": "<b>you</b>/<b>you</b>",
       "script_version": "master",
       "script_parameters": { }
     },
     "thing1": {
       "script": "thing1.py",
-      "repository": "<b>you</b>",
+      "repository": "<b>you</b>/<b>you</b>",
       "script_version": "master",
       "script_parameters": {
         "input": {
@@ -100,7 +100,7 @@ This pipeline consists of three components.  The components "thing1" and "thing2
     },
     "thing2": {
       "script": "thing2.py",
-      "repository": "<b>you</b>",
+      "repository": "<b>you</b>/<b>you</b>",
       "script_version": "master",
       "script_parameters": {
         "input": {
@@ -120,19 +120,19 @@ This pipeline consists of three components.  The component "cleanup" depends on
   "components": {
     "thing1": {
       "script": "thing1.py",
-      "repository": "<b>you</b>",
+      "repository": "<b>you</b>/<b>you</b>",
       "script_version": "master",
       "script_parameters": { }
     },
     "thing2": {
       "script": "thing2.py",
-      "repository": "<b>you</b>",
+      "repository": "<b>you</b>/<b>you</b>",
       "script_version": "master",
       "script_parameters": { }
     },
     "cleanup": {
       "script": "cleanup.py",
-      "repository": "<b>you</b>",
+      "repository": "<b>you</b>/<b>you</b>",
       "script_version": "master",
       "script_parameters": {
         "mess1": {
diff --git a/doc/api/schema/Repository.html.textile.liquid b/doc/api/schema/Repository.html.textile.liquid
index 0308f7d..27cc711 100644
--- a/doc/api/schema/Repository.html.textile.liquid
+++ b/doc/api/schema/Repository.html.textile.liquid
@@ -18,6 +18,6 @@ Each Repository has, in addition to the usual "attributes of Arvados resources":
 
 table(table table-bordered table-condensed).
 |_. Attribute|_. Type|_. Description|_. Example|
-|name|string|||
-|fetch_url|string|||
-|push_url|string|||
+|name|string|The name of the repository on disk.  Repository names must begin with a letter and contain only alphanumerics.  Unless the repository is owned by the system user, the name must begin with the owner's username, then be separated from the base repository name with @/@.  You may not create a repository that is owned by a user without a username.|@username/project1@|
+|fetch_url|string|The git remote's fetch URL for the repository.  Read-only.||
+|push_url|string|The git remote's push URL for the repository.  Read-only.||
diff --git a/doc/user/topics/tutorial-parallel.html.textile.liquid b/doc/user/topics/tutorial-parallel.html.textile.liquid
index 9be6103..7a430a9 100644
--- a/doc/user/topics/tutorial-parallel.html.textile.liquid
+++ b/doc/user/topics/tutorial-parallel.html.textile.liquid
@@ -40,7 +40,7 @@ You should now be able to run your new script using Crunch, with "script" referr
 <pre><code>~/$USER/crunch_scripts$ <span class="userinput">cat >~/the_job <<EOF
 {
  "script": "concurrent-hash.py",
- "repository": "$USER",
+ "repository": "$USER/$USER",
  "script_version": "master",
  "script_parameters":
  {
diff --git a/doc/user/tutorials/tutorial-submit-job.html.textile.liquid b/doc/user/tutorials/tutorial-submit-job.html.textile.liquid
index fc77e5c..7cc4b88 100644
--- a/doc/user/tutorials/tutorial-submit-job.html.textile.liquid
+++ b/doc/user/tutorials/tutorial-submit-job.html.textile.liquid
@@ -20,13 +20,13 @@ All Crunch scripts are managed through the Git revision control system.  Before
 ~$ <span class="userinput">git config --global user.email $USER at example.com</span></code></pre>
 </notextile>
 
-On the Arvados Workbench, navigate to "Code repositories":https://{{site.arvados_workbench_host}}/repositories.  You should see a repository with your user name listed in the *name* column.  Next to *name* is the column *push_url*.  Copy the *push_url* value associated with your repository.  This should look like <notextile><code>git at git.{{ site.arvados_api_host }}:$USER.git</code></notextile>.
+On the Arvados Workbench, navigate to "Code repositories":https://{{site.arvados_workbench_host}}/repositories.  You should see a repository with your user name listed in the *name* column.  Next to *name* is the column *push_url*.  Copy the *push_url* value associated with your repository.  This should look like <notextile><code>git at git.{{ site.arvados_api_host }}:$USER/$USER.git</code></notextile>.
 
 Next, on the Arvados virtual machine, clone your Git repository:
 
 <notextile>
 <pre><code>~$ <span class="userinput">cd $HOME</span> # (or wherever you want to install)
-~$ <span class="userinput">git clone git at git.{{ site.arvados_api_host }}:$USER.git</span>
+~$ <span class="userinput">git clone git at git.{{ site.arvados_api_host }}:$USER/$USER.git</span>
 Cloning into '$USER'...</code></pre>
 </notextile>
 
@@ -84,7 +84,7 @@ Counting objects: 4, done.
 Compressing objects: 100% (2/2), done.
 Writing objects: 100% (4/4), 682 bytes, done.
 Total 4 (delta 0), reused 0 (delta 0)
-To git at git.qr1hi.arvadosapi.com:$USER.git
+To git at git.qr1hi.arvadosapi.com:$USER/$USER.git
  * [new branch]      master -> master</code></pre>
 </notextile>
 
diff --git a/services/api/app/controllers/arvados/v1/users_controller.rb b/services/api/app/controllers/arvados/v1/users_controller.rb
index 131ee52..b5ac195 100644
--- a/services/api/app/controllers/arvados/v1/users_controller.rb
+++ b/services/api/app/controllers/arvados/v1/users_controller.rb
@@ -96,12 +96,28 @@ class Arvados::V1::UsersController < ApplicationController
       end
     end
 
+    # It's not always possible to know the user's username when submitting
+    # this request.  If it included a plain repository name, expand that to a
+    # user-owned name now.
+    if params[:repo_name].nil?
+      full_repo_name = nil
+    else
+      full_repo_name = "#{@object.username}/#{params[:repo_name]}"
+    end
     if object_found
-      @response = @object.setup_repo_vm_links params[:repo_name],
+      if params[:repo_name].andand.include?("/")
+        repo_name = params[:repo_name]
+      elsif @object.username.nil?
+        raise ArgumentError.
+          new("can't setup a user without a username with a repository")
+      else
+        repo_name = full_repo_name
+      end
+      @response = @object.setup_repo_vm_links repo_name,
                     params[:vm_uuid], params[:openid_prefix]
     else
       @response = User.setup @object, params[:openid_prefix],
-                    params[:repo_name], params[:vm_uuid]
+                    full_repo_name, params[:vm_uuid]
     end
 
     # setup succeeded. send email to user
diff --git a/services/api/app/models/commit.rb b/services/api/app/models/commit.rb
index 0f62737..0d47b63 100644
--- a/services/api/app/models/commit.rb
+++ b/services/api/app/models/commit.rb
@@ -137,20 +137,16 @@ class Commit < ActiveRecord::Base
 
   protected
 
-  def self.repositories
-    return @repositories if @repositories
-
-    @repositories = {}
-    @gitdirbase = Rails.configuration.git_repositories_dir
-    Dir.foreach @gitdirbase do |repo|
-      next if repo.match /^\./
-      git_dir = File.join(@gitdirbase,
-                          repo.match(/\.git$/) ? repo : File.join(repo, '.git'))
-      next if git_dir == Rails.configuration.git_internal_dir
-      repo_name = repo.sub(/\.git$/, '')
-      @repositories[repo_name] = {git_dir: git_dir}
-    end
-
-    @repositories
-  end
+ def self.repositories
+   return @repositories if @repositories
+
+   @repositories = {}
+   Repository.find_each do |repo|
+     if git_dir = repo.server_path
+       @repositories[repo.name] = {git_dir: git_dir}
+     end
+   end
+
+   @repositories
+ end
 end
diff --git a/services/api/app/models/repository.rb b/services/api/app/models/repository.rb
index 0189ee7..5bc467c 100644
--- a/services/api/app/models/repository.rb
+++ b/services/api/app/models/repository.rb
@@ -3,26 +3,49 @@ class Repository < ArvadosModel
   include KindAndEtag
   include CommonApiTemplate
 
+  # Order is important here.  We must validate the owner before we can
+  # validate the name.
+  validate :valid_owner
+  validate :name_format, :if => Proc.new { |r| r.errors[:owner_uuid].empty? }
+  validates(:name, uniqueness: true, allow_nil: false)
+
   api_accessible :user, extend: :common do |t|
     t.add :name
     t.add :fetch_url
     t.add :push_url
   end
 
+  def self.attributes_required_columns
+    super.merge({"push_url" => ["name"], "fetch_url" => ["name"]})
+  end
+
   def push_url
-    super || self.name && "git at git.#{Rails.configuration.uuid_prefix}.arvadosapi.com:#{self.name}.git"
+    "git at git.%s.arvadosapi.com:%s.git" % [Rails.configuration.uuid_prefix, name]
   end
 
   def fetch_url
-    super || push_url
+    push_url
   end
 
-  protected
-
-  def permission_to_create
-    current_user and current_user.is_admin
+  def server_path
+    # Find where the repository is stored on the API server's filesystem,
+    # and return that path, or nil if not found.
+    # This method is only for the API server's internal use, and should not
+    # be exposed through the public API.  Following our current gitolite
+    # setup, it searches for repositories stored by UUID, then name; and it
+    # prefers bare repositories over checkouts.
+    [["%s.git"], ["%s", ".git"]].each do |repo_base, *join_args|
+      [:uuid, :name].each do |path_attr|
+        git_dir = File.join(Rails.configuration.git_repositories_dir,
+                            repo_base % send(path_attr), *join_args)
+        return git_dir if File.exist?(git_dir)
+      end
+    end
+    nil
   end
 
+  protected
+
   def permission_to_update
     return false if not current_user
     return true if current_user.is_admin
@@ -33,4 +56,30 @@ class Repository < ArvadosModel
     return super if changed_attributes.keys - ['modified_at', 'updated_at'] == []
     false
   end
+
+  def owner
+    User.find_by_uuid(owner_uuid)
+  end
+
+  def valid_owner
+    if owner.nil? or (owner.username.nil? and (owner.uuid != system_user_uuid))
+      errors.add(:owner_uuid, "must refer to a user with a username")
+      false
+    end
+  end
+
+  def name_format
+    if owner.uuid == system_user_uuid
+      prefix_match = ""
+      errmsg_start = "must be"
+    else
+      prefix_match = Regexp.escape(owner.username + "/")
+      errmsg_start = "must be the owner's username, then '/', then"
+    end
+    if not /^#{prefix_match}[A-Za-z][A-Za-z0-9]*$/.match(name)
+      errors.add(:name,
+                 "#{errmsg_start} a letter followed by alphanumerics")
+      false
+    end
+  end
 end
diff --git a/services/api/app/models/user.rb b/services/api/app/models/user.rb
index c395d23..fe5e07b 100644
--- a/services/api/app/models/user.rb
+++ b/services/api/app/models/user.rb
@@ -17,6 +17,9 @@ class User < ArvadosModel
             allow_nil: true)
   before_update :prevent_privilege_escalation
   before_update :prevent_inactive_admin
+  before_update :verify_repositories_empty, :if => Proc.new { |user|
+    user.username.nil? and user.username_changed?
+  }
   before_create :check_auto_admin
   before_create :set_initial_username, :if => Proc.new { |user|
     user.username.nil? and user.email
@@ -29,8 +32,14 @@ class User < ArvadosModel
   }
   after_create :send_admin_notifications
   after_update :send_profile_created_notification
+  after_update :sync_repository_names, :if => Proc.new { |user|
+    (user.uuid != system_user_uuid) and
+    user.username_changed? and
+    (not user.username_was.nil?)
+  }
 
   has_many :authorized_keys, :foreign_key => :authorized_user_uuid, :primary_key => :uuid
+  has_many :repositories, foreign_key: :owner_uuid, primary_key: :uuid
 
   api_accessible :user, extend: :common do |t|
     t.add :email
@@ -388,7 +397,7 @@ class User < ArvadosModel
       return
     end
 
-    repo = Repository.where(name: repo_name).first_or_create!
+    repo = Repository.where(owner_uuid: uuid, name: repo_name).first_or_create!
     logger.info { "repo uuid: " + repo[:uuid] }
     repo_perm = Link.where(tail_uuid: uuid, head_uuid: repo.uuid,
                            link_class: "permission",
@@ -467,9 +476,10 @@ class User < ArvadosModel
     if username
       create_vm_login_permission_link(Rails.configuration.auto_setup_new_users_with_vm_uuid,
                                       username)
+      repo_name = "#{username}/#{username}"
       if Rails.configuration.auto_setup_new_users_with_repository and
-          Repository.where(name: username).first.nil?
-        repo = Repository.create!(name: username)
+          Repository.where(name: repo_name).first.nil?
+        repo = Repository.create!(name: repo_name, owner_uuid: uuid)
         Link.create!(tail_uuid: uuid, head_uuid: repo.uuid,
                      link_class: "permission", name: "can_manage")
       end
@@ -486,4 +496,19 @@ class User < ArvadosModel
     end
   end
 
+  def verify_repositories_empty
+    unless repositories.first.nil?
+      errors.add(:username, "can't be unset when the user owns repositories")
+      false
+    end
+  end
+
+  def sync_repository_names
+    old_name_re = /^#{Regexp.escape(username_was)}\//
+    name_sub = "#{username}/"
+    repositories.find_each do |repo|
+      repo.name = repo.name.sub(old_name_re, name_sub)
+      repo.save!
+    end
+  end
 end
diff --git a/services/api/db/migrate/20150324152204_backward_compatibility_for_user_repositories.rb b/services/api/db/migrate/20150324152204_backward_compatibility_for_user_repositories.rb
new file mode 100644
index 0000000..12b888d
--- /dev/null
+++ b/services/api/db/migrate/20150324152204_backward_compatibility_for_user_repositories.rb
@@ -0,0 +1,89 @@
+require 'has_uuid'
+require 'kind_and_etag'
+
+class BackwardCompatibilityForUserRepositories < ActiveRecord::Migration
+  include CurrentApiClient
+
+  class ArvadosModel < ActiveRecord::Base
+    self.abstract_class = true
+    extend HasUuid::ClassMethods
+    include CurrentApiClient
+    include KindAndEtag
+    before_create do |record|
+      record.uuid ||= record.class.generate_uuid
+      record.owner_uuid ||= system_user_uuid
+    end
+    serialize :properties, Hash
+
+    def self.to_s
+      # Clean up the name of the stub model class so we generate correct UUIDs.
+      super.rpartition("::").last
+    end
+  end
+
+  class Log < ArvadosModel
+    def self.log_for(thing, age="old")
+      { "#{age}_etag" => thing.etag,
+        "#{age}_attributes" => thing.attributes,
+      }
+    end
+
+    def self.log_create(thing)
+      new_log("create", thing, log_for(thing, "new"))
+    end
+
+    def self.log_update(thing, start_state)
+      new_log("update", thing, start_state.merge(log_for(thing, "new")))
+    end
+
+    def self.log_destroy(thing)
+      new_log("destroy", thing, log_for(thing, "old"))
+    end
+
+    private
+
+    def self.new_log(event_type, thing, properties)
+      create!(event_type: event_type,
+              event_at: Time.now,
+              object_uuid: thing.uuid,
+              object_owner_uuid: thing.owner_uuid,
+              properties: properties)
+    end
+  end
+
+  class Link < ArvadosModel
+  end
+
+  class Repository < ArvadosModel
+  end
+
+  def up
+    remove_index :repositories, name: "repositories_search_index"
+    add_index(:repositories, %w(uuid owner_uuid modified_by_client_uuid
+                                modified_by_user_uuid name),
+              name: "repositories_search_index")
+    remove_column :repositories, :fetch_url
+    remove_column :repositories, :push_url
+
+    [Link, Log, Repository].each { |m| m.reset_column_information }
+    Repository.where("owner_uuid != ?", system_user_uuid).find_each do |repo|
+      link_attrs = {
+        tail_uuid: repo.owner_uuid,
+        link_class: "permission", name: "can_manage", head_uuid: repo.uuid,
+      }
+      if Link.where(link_attrs).first.nil?
+        manage_link = Link.create!(link_attrs)
+        Log.log_create(manage_link)
+      end
+      start_log = Log.log_for(repo)
+      repo.owner_uuid = system_user_uuid
+      repo.save!
+      Log.log_update(repo, start_log)
+    end
+  end
+
+  def down
+    raise ActiveRecord::IrreversibleMigration.
+      new("can't restore prior fetch and push URLs")
+  end
+end
diff --git a/services/api/db/structure.sql b/services/api/db/structure.sql
index 40861f4..61fc1ae 100644
--- a/services/api/db/structure.sql
+++ b/services/api/db/structure.sql
@@ -760,8 +760,6 @@ CREATE TABLE repositories (
     modified_by_user_uuid character varying(255),
     modified_at timestamp without time zone,
     name character varying(255),
-    fetch_url character varying(255),
-    push_url character varying(255),
     created_at timestamp without time zone NOT NULL,
     updated_at timestamp without time zone NOT NULL
 );
@@ -2102,7 +2100,7 @@ CREATE INDEX pipeline_templates_search_index ON pipeline_templates USING btree (
 -- Name: repositories_search_index; Type: INDEX; Schema: public; Owner: -; Tablespace: 
 --
 
-CREATE INDEX repositories_search_index ON repositories USING btree (uuid, owner_uuid, modified_by_client_uuid, modified_by_user_uuid, name, fetch_url, push_url);
+CREATE INDEX repositories_search_index ON repositories USING btree (uuid, owner_uuid, modified_by_client_uuid, modified_by_user_uuid, name);
 
 
 --
@@ -2374,4 +2372,6 @@ INSERT INTO schema_migrations (version) VALUES ('20150303210106');
 
 INSERT INTO schema_migrations (version) VALUES ('20150312151136');
 
-INSERT INTO schema_migrations (version) VALUES ('20150317132720');
\ No newline at end of file
+INSERT INTO schema_migrations (version) VALUES ('20150317132720');
+
+INSERT INTO schema_migrations (version) VALUES ('20150324152204');
\ No newline at end of file
diff --git a/services/api/script/crunch-dispatch.rb b/services/api/script/crunch-dispatch.rb
index ab4f70e..249582e 100755
--- a/services/api/script/crunch-dispatch.rb
+++ b/services/api/script/crunch-dispatch.rb
@@ -66,6 +66,7 @@ class Dispatcher
     end
 
     @repo_root = Rails.configuration.git_repositories_dir
+    @arvados_repo_path = Repository.where(name: "arvados").first.server_path
     @authorizations = {}
     @did_recently = {}
     @fetched_commits = {}
@@ -276,19 +277,10 @@ class Dispatcher
     @authorizations[job.uuid]
   end
 
-  def get_commit(repo_name, commit_hash)
+  def get_commit(src_repo, commit_hash)
     # @fetched_commits[V]==true if we know commit V exists in the
     # arvados_internal git repository.
     if !@fetched_commits[commit_hash]
-      src_repo = File.join(@repo_root, "#{repo_name}.git")
-      if not File.exists? src_repo
-        src_repo = File.join(@repo_root, repo_name, '.git')
-        if not File.exists? src_repo
-          fail_job job, "No #{repo_name}.git or #{repo_name}/.git at #{@repo_root}"
-          return nil
-        end
-      end
-
       # check if the commit needs to be fetched or not
       commit_rev = stdout_s(git_cmd("rev-list", "-n1", commit_hash),
                             err: "/dev/null")
@@ -383,11 +375,17 @@ class Dispatcher
                          "GEM_PATH=#{ENV['GEM_PATH']}")
       end
 
+      repo = Repository.where(name: job.repository).first
+      if repo.nil? or repo.server_path.nil?
+        fail_job "Repository #{job.repository} not found under #{@repo_root}"
+        next
+      end
+
       ready = (get_authorization(job) and
-               get_commit(job.repository, job.script_version) and
+               get_commit(repo.server_path, job.script_version) and
                tag_commit(job.script_version, job.uuid))
       if ready and job.arvados_sdk_version
-        ready = (get_commit("arvados", job.arvados_sdk_version) and
+        ready = (get_commit(@arvados_repo_path, job.arvados_sdk_version) and
                  tag_commit(job.arvados_sdk_version, "#{job.uuid}-arvados-sdk"))
       end
       next unless ready
diff --git a/services/api/test/fixtures/api_client_authorizations.yml b/services/api/test/fixtures/api_client_authorizations.yml
index 0b4d874..869d9ee 100644
--- a/services/api/test/fixtures/api_client_authorizations.yml
+++ b/services/api/test/fixtures/api_client_authorizations.yml
@@ -1,5 +1,11 @@
 # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
 
+system_user:
+  api_client: untrusted
+  user: system_user
+  api_token: systemusertesttoken1234567890aoeuidhtnsqjkxbmwvzpy
+  expires_at: 2038-01-01 00:00:00
+
 admin:
   api_client: untrusted
   user: admin
diff --git a/services/api/test/fixtures/jobs.yml b/services/api/test/fixtures/jobs.yml
index 7812004..ea6cbb0 100644
--- a/services/api/test/fixtures/jobs.yml
+++ b/services/api/test/fixtures/jobs.yml
@@ -77,7 +77,7 @@ foobar:
   cancelled_by_user_uuid: ~
   cancelled_by_client_uuid: ~
   script: hash
-  repository: foo
+  repository: active/foo
   script_version: 7def43a4d3f20789dda4700f703b5514cc3ed250
   script_parameters:
     input: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
@@ -113,7 +113,7 @@ barbaz:
   finished_at: <%= 2.minute.ago.to_s(:db) %>
   running: false
   success: true
-  repository: foo
+  repository: active/foo
   output: ea10d51bcf88862dbcc36eb292017dfd+45
   priority: 0
   log: d41d8cd98f00b204e9800998ecf8427e+0
@@ -141,7 +141,7 @@ runningbarbaz:
   finished_at: <%= 2.minute.ago.to_s(:db) %>
   running: true
   success: ~
-  repository: foo
+  repository: active/foo
   output: ea10d51bcf88862dbcc36eb292017dfd+45
   priority: 0
   log: d41d8cd98f00b204e9800998ecf8427e+0
@@ -158,7 +158,7 @@ previous_job_run:
   uuid: zzzzz-8i9sb-cjs4pklxxjykqqq
   created_at: <%= 14.minute.ago.to_s(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  repository: foo
+  repository: active/foo
   script: hash
   script_version: 4fe459abe02d9b365932b8f5dc419439ab4e2577
   script_parameters:
@@ -172,7 +172,7 @@ previous_docker_job_run:
   uuid: zzzzz-8i9sb-k6emstgk4kw4yhi
   created_at: <%= 14.minute.ago.to_s(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  repository: foo
+  repository: active/foo
   script: hash
   script_version: 4fe459abe02d9b365932b8f5dc419439ab4e2577
   script_parameters:
@@ -189,7 +189,7 @@ previous_job_run_with_arvados_sdk_version:
   uuid: zzzzz-8i9sb-eoo0321or2dw2jg
   created_at: <%= 14.minute.ago.to_s(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  repository: foo
+  repository: active/foo
   script: hash
   script_version: 31ce37fe365b3dc204300a3e4c396ad333ed0556
   script_parameters:
@@ -206,7 +206,7 @@ previous_job_run_no_output:
   uuid: zzzzz-8i9sb-cjs4pklxxjykppp
   created_at: <%= 14.minute.ago.to_s(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  repository: foo
+  repository: active/foo
   script: hash
   script_version: 4fe459abe02d9b365932b8f5dc419439ab4e2577
   script_parameters:
@@ -220,7 +220,7 @@ nondeterminisic_job_run:
   uuid: zzzzz-8i9sb-cjs4pklxxjykyyy
   created_at: <%= 14.minute.ago.to_s(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  repository: foo
+  repository: active/foo
   script: hash2
   script_version: 4fe459abe02d9b365932b8f5dc419439ab4e2577
   script_parameters:
@@ -311,7 +311,7 @@ job_in_subproject:
   created_at: 2014-10-15 12:00:00
   owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
   log: ~
-  repository: foo
+  repository: active/foo
   script: hash
   script_version: 4fe459abe02d9b365932b8f5dc419439ab4e2577
   state: Complete
@@ -343,7 +343,7 @@ running_will_be_completed:
 graph_stage1:
   uuid: zzzzz-8i9sb-graphstage10000
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
-  repository: foo
+  repository: active/foo
   script: hash
   script_version: 4fe459abe02d9b365932b8f5dc419439ab4e2577
   state: Complete
@@ -352,7 +352,7 @@ graph_stage1:
 graph_stage2:
   uuid: zzzzz-8i9sb-graphstage20000
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
-  repository: foo
+  repository: active/foo
   script: hash2
   script_version: 4fe459abe02d9b365932b8f5dc419439ab4e2577
   state: Complete
@@ -364,7 +364,7 @@ graph_stage2:
 graph_stage3:
   uuid: zzzzz-8i9sb-graphstage30000
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
-  repository: foo
+  repository: active/foo
   script: hash2
   script_version: 4fe459abe02d9b365932b8f5dc419439ab4e2577
   state: Complete
@@ -380,7 +380,7 @@ job_with_latest_version:
   cancelled_by_user_uuid: ~
   cancelled_by_client_uuid: ~
   script: hash
-  repository: foo
+  repository: active/foo
   script_version: 7def43a4d3f20789dda4700f703b5514cc3ed250
   supplied_script_version: master
   script_parameters:
@@ -406,7 +406,7 @@ running_job_in_publicly_accessible_project:
   uuid: zzzzz-8i9sb-n7omg50bvt0m1nf
   owner_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  repository: foo
+  repository: active/foo
   script: running_job_script
   script_version: 4fe459abe02d9b365932b8f5dc419439ab4e2577
   state: Running
@@ -418,7 +418,7 @@ completed_job_in_publicly_accessible_project:
   uuid: zzzzz-8i9sb-jyq01m7in1jlofj
   owner_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  repository: foo
+  repository: active/foo
   script: completed_job_script
   script_version: 4fe459abe02d9b365932b8f5dc419439ab4e2577
   state: Complete
diff --git a/services/api/test/fixtures/repositories.yml b/services/api/test/fixtures/repositories.yml
index b80414d..ab2e360 100644
--- a/services/api/test/fixtures/repositories.yml
+++ b/services/api/test/fixtures/repositories.yml
@@ -1,7 +1,7 @@
 crunch_dispatch_test:
   uuid: zzzzz-s0uqq-382brsig8rp3665
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz # active user
-  name: crunch_dispatch_test
+  name: active/crunchdispatchtest
   created_at: 2015-01-01T00:00:00.123456Z
   modified_at: 2015-01-01T00:00:00.123456Z
 
@@ -15,34 +15,27 @@ arvados:
 foo:
   uuid: zzzzz-s0uqq-382brsig8rp3666
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz # active user
-  name: foo
+  name: active/foo
   created_at: 2015-01-01T00:00:00.123456Z
   modified_at: 2015-01-01T00:00:00.123456Z
 
 repository2:
   uuid: zzzzz-s0uqq-382brsig8rp3667
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz # active user
-  name: foo2
+  name: active/foo2
   created_at: 2015-01-01T00:00:00.123456Z
   modified_at: 2015-01-01T00:00:00.123456Z
 
 repository3:
   uuid: zzzzz-s0uqq-38orljkqpyo1j61
   owner_uuid: zzzzz-tpzed-d9tiejq69daie8f # admin user
-  name: foo3
+  name: admin/foo3
   created_at: 2015-01-01T00:00:00.123456Z
   modified_at: 2015-01-01T00:00:00.123456Z
 
 repository4:
   uuid: zzzzz-s0uqq-38oru8hnk57ht34
   owner_uuid: zzzzz-tpzed-d9tiejq69daie8f # admin user
-  name: foo4
-  created_at: 2015-01-01T00:00:00.123456Z
-  modified_at: 2015-01-01T00:00:00.123456Z
-
-auto_setup_repository:
-  uuid: zzzzz-s0uqq-382brabc8rp3667
-  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz # active user
-  name: auto_setup_repo
+  name: admin/foo4
   created_at: 2015-01-01T00:00:00.123456Z
   modified_at: 2015-01-01T00:00:00.123456Z
diff --git a/services/api/test/fixtures/users.yml b/services/api/test/fixtures/users.yml
index 46fbd0f..4e17de3 100644
--- a/services/api/test/fixtures/users.yml
+++ b/services/api/test/fixtures/users.yml
@@ -157,6 +157,7 @@ inactive:
   identity_url: https://inactive-user.openid.local
   is_active: false
   is_admin: false
+  username: inactiveuser
   prefs: {}
 
 inactive_but_signed_user_agreement:
diff --git a/services/api/test/functional/arvados/v1/commits_controller_test.rb b/services/api/test/functional/arvados/v1/commits_controller_test.rb
index ceaebff..2b109b6 100644
--- a/services/api/test/functional/arvados/v1/commits_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/commits_controller_test.rb
@@ -29,17 +29,17 @@ class Arvados::V1::CommitsControllerTest < ActionController::TestCase
     assert_includes(a, '077ba2ad3ea24a929091a9e6ce545c93199b8e57')
 
   #test "test_branch2" do
-    a = Commit.find_commit_range(users(:active), 'foo', nil, 'b1', nil)
+    a = Commit.find_commit_range(users(:active), 'active/foo', nil, 'b1', nil)
     assert_equal ['1de84a854e2b440dc53bf42f8548afa4c17da332'], a
 
   #test "test_branch3" do
-    a = Commit.find_commit_range(users(:active), 'foo', nil, 'HEAD', nil)
+    a = Commit.find_commit_range(users(:active), 'active/foo', nil, 'HEAD', nil)
     assert_equal ['1de84a854e2b440dc53bf42f8548afa4c17da332'], a
 
   #test "test_single_revision_repo" do
-    a = Commit.find_commit_range(users(:active), "foo", nil, '31ce37fe365b3dc204300a3e4c396ad333ed0556', nil)
+    a = Commit.find_commit_range(users(:active), "active/foo", nil, '31ce37fe365b3dc204300a3e4c396ad333ed0556', nil)
     assert_equal ['31ce37fe365b3dc204300a3e4c396ad333ed0556'], a
-    a = Commit.find_commit_range(users(:active), "bar", nil, '31ce37fe365b3dc204300a3e4c396ad333ed0556', nil)
+    a = Commit.find_commit_range(users(:active), "arvados", nil, '31ce37fe365b3dc204300a3e4c396ad333ed0556', nil)
     assert_equal nil, a
 
   #test "test_multi_revision" do
diff --git a/services/api/test/functional/arvados/v1/job_reuse_controller_test.rb b/services/api/test/functional/arvados/v1/job_reuse_controller_test.rb
index 9b66851..1dd620a 100644
--- a/services/api/test/functional/arvados/v1/job_reuse_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/job_reuse_controller_test.rb
@@ -17,7 +17,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       no_reuse: false,
       script: "hash",
       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-      repository: "foo",
+      repository: "active/foo",
       script_parameters: {
         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
         an_integer: '1'
@@ -35,7 +35,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       job: {
         script: "hash",
         script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-        repository: "foo",
+        repository: "active/foo",
         script_parameters: {
           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
           an_integer: '1'
@@ -55,7 +55,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       job: {
         script: "hash",
         script_version: "tag1",
-        repository: "foo",
+        repository: "active/foo",
         script_parameters: {
           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
           an_integer: '1'
@@ -76,7 +76,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
         no_reuse: true,
         script: "hash",
         script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-        repository: "foo",
+        repository: "active/foo",
         script_parameters: {
           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
           an_integer: '1'
@@ -96,7 +96,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
         job: {
           script: "hash",
           script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-          repository: "foo",
+          repository: "active/foo",
           script_parameters: {
             input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
             an_integer: '1'
@@ -118,7 +118,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       job: {
         script: "hash",
         script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-        repository: "foo",
+        repository: "active/foo",
         script_parameters: {
           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
           an_integer: '1'
@@ -138,7 +138,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       no_reuse: false,
       script: "hash",
       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-      repository: "foo",
+      repository: "active/foo",
       script_parameters: {
         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
         an_integer: '2'
@@ -156,7 +156,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       script: "hash",
       minimum_script_version: "tag1",
       script_version: "master",
-      repository: "foo",
+      repository: "active/foo",
       script_parameters: {
         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
         an_integer: '1'
@@ -174,7 +174,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       no_reuse: false,
       script: "hash",
       script_version: "master",
-      repository: "foo",
+      repository: "active/foo",
       script_parameters: {
         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
         an_integer: '1'
@@ -192,7 +192,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       no_reuse: false,
       script: "hash",
       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-      repository: "foo",
+      repository: "active/foo",
       script_parameters: {
         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
         an_integer: '2'
@@ -210,7 +210,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       no_reuse: false,
       script: "hash",
       script_version: "master",
-      repository: "foo",
+      repository: "active/foo",
       script_parameters: {
         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
         an_integer: '2'
@@ -228,7 +228,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       no_reuse: false,
       script: "hash",
       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-      repository: "foo",
+      repository: "active/foo",
       script_parameters: {
         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
         an_integer: '1'
@@ -247,7 +247,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       no_reuse: false,
       script: "hash2",
       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-      repository: "foo",
+      repository: "active/foo",
       script_parameters: {
         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
         an_integer: '1'
@@ -266,7 +266,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       no_reuse: false,
       script: "hash",
       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-      repository: "foo",
+      repository: "active/foo",
       script_parameters: {
         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
         an_integer: '1'
@@ -285,7 +285,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       script: "hash",
       minimum_script_version: "31ce37fe365b3dc204300a3e4c396ad333ed0556",
       script_version: "master",
-      repository: "foo",
+      repository: "active/foo",
       exclude_script_versions: ["tag1"],
       script_parameters: {
         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
@@ -305,7 +305,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
       job: {
         script: "hash",
         script_version: "master",
-        repository: "foo",
+        repository: "active/foo",
         script_parameters: {
           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
           an_integer: '1'
@@ -324,7 +324,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
   end
 
   BASE_FILTERS = {
-    'repository' => ['=', 'foo'],
+    'repository' => ['=', 'active/foo'],
     'script' => ['=', 'hash'],
     'script_version' => ['in git', 'master'],
     'docker_image_locator' => ['=', nil],
@@ -342,7 +342,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
            job: {
              script: "hash",
              script_version: "master",
-             repository: "foo",
+             repository: "active/foo",
              script_parameters: {
                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
                an_integer: '1'
@@ -368,7 +368,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
            job: {
              script: "hash",
              script_version: "master",
-             repository: "foo",
+             repository: "active/foo",
              script_parameters: {
                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
                an_integer: '1'
@@ -391,7 +391,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
            job: {
              script: "hash",
              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-             repository: "foo",
+             repository: "active/foo",
              script_parameters: {
                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
                an_integer: '1'
@@ -412,7 +412,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
            job: {
              script: "hash",
              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-             repository: "foo",
+             repository: "active/foo",
              script_parameters: {
                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
                an_integer: '1'
@@ -442,7 +442,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
            job: {
              script: "hash",
              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-             repository: "foo",
+             repository: "active/foo",
              script_parameters: {
                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
                an_integer: '1'
@@ -470,7 +470,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
            job: {
              script: "hash",
              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-             repository: "foo",
+             repository: "active/foo",
              script_parameters: {
                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
                an_integer: '1'
@@ -495,7 +495,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
            job: {
              script: "hash",
              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-             repository: "foo",
+             repository: "active/foo",
              script_parameters: {
                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
                an_integer: '1'
@@ -517,7 +517,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
              job: {
                script: "hash",
                script_version: "master",
-               repository: "foo",
+               repository: "active/foo",
                script_parameters: {
                  input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
                  an_integer: '1'
@@ -532,7 +532,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
   end
 
   test "find Job with script version range" do
-    get :index, filters: [["repository", "=", "foo"],
+    get :index, filters: [["repository", "=", "active/foo"],
                           ["script", "=", "hash"],
                           ["script_version", "in git", "tag1"]]
     assert_response :success
@@ -542,7 +542,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
   end
 
   test "find Job with script version range exclusions" do
-    get :index, filters: [["repository", "=", "foo"],
+    get :index, filters: [["repository", "=", "active/foo"],
                           ["script", "=", "hash"],
                           ["script_version", "not in git", "tag1"]]
     assert_response :success
@@ -607,7 +607,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
     params[:job] = {
       script: "hash",
       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
-      repository: "foo",
+      repository: "active/foo",
       script_parameters: {
         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
         an_integer: '1',
diff --git a/services/api/test/functional/arvados/v1/jobs_controller_test.rb b/services/api/test/functional/arvados/v1/jobs_controller_test.rb
index 07e7f84..b8b061f 100644
--- a/services/api/test/functional/arvados/v1/jobs_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/jobs_controller_test.rb
@@ -10,7 +10,7 @@ class Arvados::V1::JobsControllerTest < ActionController::TestCase
     post :create, job: {
       script: "hash",
       script_version: "master",
-      repository: "foo",
+      repository: "active/foo",
       script_parameters: {}
     }
     assert_response :success
@@ -27,7 +27,7 @@ class Arvados::V1::JobsControllerTest < ActionController::TestCase
       script: "hash",
       script_version: "master",
       script_parameters: {},
-      repository: "foo",
+      repository: "active/foo",
       started_at: Time.now,
       finished_at: Time.now,
       running: false,
diff --git a/services/api/test/functional/arvados/v1/repositories_controller_test.rb b/services/api/test/functional/arvados/v1/repositories_controller_test.rb
index 5304bca..128e490 100644
--- a/services/api/test/functional/arvados/v1/repositories_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/repositories_controller_test.rb
@@ -87,4 +87,20 @@ class Arvados::V1::RepositoriesControllerTest < ActionController::TestCase
                    "response public_key does not match fixture #{u}.")
     end
   end
+
+  test "default index includes fetch_url" do
+    authorize_with :active
+    get(:index)
+    assert_response :success
+    assert_includes(json_response["items"].map { |r| r["fetch_url"] },
+                    "git at git.zzzzz.arvadosapi.com:active/foo.git")
+  end
+
+  test "can select push_url in index" do
+    authorize_with :active
+    get(:index, {select: ["uuid", "push_url"]})
+    assert_response :success
+    assert_includes(json_response["items"].map { |r| r["push_url"] },
+                    "git at git.zzzzz.arvadosapi.com:active/foo.git")
+  end
 end
diff --git a/services/api/test/functional/arvados/v1/users_controller_test.rb b/services/api/test/functional/arvados/v1/users_controller_test.rb
index 2d26370..f776ad2 100644
--- a/services/api/test/functional/arvados/v1/users_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/users_controller_test.rb
@@ -83,7 +83,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
   test "create user with user, vm and repo as input" do
     authorize_with :admin
-    repo_name = 'test_repo'
+    repo_name = 'usertestrepo'
 
     post :setup, {
       repo_name: repo_name,
@@ -113,7 +113,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
         created['uuid'], created['email'], 'arvados#user', false, 'User'
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
-        repo_name, created['uuid'], 'arvados#repository', true, 'Repository'
+        "foo/#{repo_name}", created['uuid'], 'arvados#repository', true, 'Repository'
 
     verify_link response_items, 'arvados#group', true, 'permission', 'can_read',
         'All users', created['uuid'], 'arvados#group', true, 'Group'
@@ -129,7 +129,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
     post :setup, {
       uuid: 'bogus_uuid',
-      repo_name: 'test_repo',
+      repo_name: 'usertestrepo',
       vm_uuid: @vm_uuid
     }
     response_body = JSON.parse(@response.body)
@@ -143,7 +143,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
     post :setup, {
       user: {uuid: 'bogus_uuid'},
-      repo_name: 'test_repo',
+      repo_name: 'usertestrepo',
       vm_uuid: @vm_uuid,
       openid_prefix: 'https://www.google.com/accounts/o8/id'
     }
@@ -158,7 +158,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     authorize_with :admin
 
     post :setup, {
-      repo_name: 'test_repo',
+      repo_name: 'usertestrepo',
       vm_uuid: @vm_uuid,
       openid_prefix: 'https://www.google.com/accounts/o8/id'
     }
@@ -174,7 +174,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
     post :setup, {
       user: {},
-      repo_name: 'test_repo',
+      repo_name: 'usertestrepo',
       vm_uuid: @vm_uuid,
       openid_prefix: 'https://www.google.com/accounts/o8/id'
     }
@@ -191,7 +191,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
     post :setup, {
       uuid: users(:inactive).uuid,
-      repo_name: 'test_repo',
+      repo_name: 'usertestrepo',
       vm_uuid: @vm_uuid
     }
 
@@ -207,7 +207,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
     # expect repo and vm links
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
-        'test_repo', resp_obj['uuid'], 'arvados#repository', true, 'Repository'
+        'inactiveuser/usertestrepo', resp_obj['uuid'], 'arvados#repository', true, 'Repository'
 
     verify_link response_items, 'arvados#virtualMachine', true, 'permission', 'can_login',
         @vm_uuid, resp_obj['uuid'], 'arvados#virtualMachine', false, 'VirtualMachine'
@@ -257,7 +257,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     authorize_with :admin
 
     post :setup, {
-      repo_name: 'test_repo',
+      repo_name: 'usertestrepo',
       user: {email: 'foo at example.com'},
       openid_prefix: 'https://www.google.com/accounts/o8/id'
     }
@@ -276,7 +276,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     authorize_with :admin
 
     post :setup, {
-      repo_name: 'test_repo',
+      repo_name: 'usertestrepo',
       vm_uuid: 'no_such_vm',
       user: {email: 'foo at example.com'},
       openid_prefix: 'https://www.google.com/accounts/o8/id'
@@ -293,7 +293,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     authorize_with :admin
 
     post :setup, {
-      repo_name: 'test_repo',
+      repo_name: 'usertestrepo',
       openid_prefix: 'https://www.google.com/accounts/o8/id',
       vm_uuid: @vm_uuid,
       user: {email: 'foo at example.com'}
@@ -333,7 +333,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
         'All users', response_object['uuid'], 'arvados#group', true, 'Group'
 
     verify_link response_items, 'arvados#repository', false, 'permission', 'can_manage',
-        'test_repo', response_object['uuid'], 'arvados#repository', true, 'Repository'
+        'foo/usertestrepo', response_object['uuid'], 'arvados#repository', true, 'Repository'
 
     verify_link response_items, 'arvados#virtualMachine', false, 'permission', 'can_login',
         nil, response_object['uuid'], 'arvados#virtualMachine', false, 'VirtualMachine'
@@ -344,7 +344,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
     post :setup, {
       openid_prefix: 'https://www.google.com/accounts/o8/id',
-      repo_name: 'test_repo',
+      repo_name: 'usertestrepo',
       vm_uuid: @vm_uuid,
       user: {
         first_name: 'test_first_name',
@@ -370,7 +370,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
     post :setup, {
       openid_prefix: 'https://www.google.com/accounts/o8/id',
-      repo_name: 'test_repo',
+      repo_name: 'usertestrepo',
       user: {
         email: inactive_user['email']
       }
@@ -391,7 +391,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     authorize_with :admin
 
     post :setup, {
-      repo_name: 'test_repo',
+      repo_name: 'usertestrepo',
       openid_prefix: 'http://www.example.com/account',
       user: {
         first_name: "in_create_test_first_name",
@@ -418,7 +418,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
         created['uuid'], created['email'], 'arvados#user', false, 'User'
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
-        'test_repo', created['uuid'], 'arvados#repository', true, 'Repository'
+        'foo/usertestrepo', created['uuid'], 'arvados#repository', true, 'Repository'
 
     verify_link response_items, 'arvados#group', true, 'permission', 'can_read',
         'All users', created['uuid'], 'arvados#group', true, 'Group'
@@ -431,7 +431,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     authorize_with :admin
 
     post :setup, {
-      repo_name: 'test_repo',
+      repo_name: 'usertestrepo',
       user: {
         first_name: "in_create_test_first_name",
         last_name: "test_last_name",
@@ -456,7 +456,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
         email: "foo at example.com"
       },
       vm_uuid: @vm_uuid,
-      repo_name: 'test_repo',
+      repo_name: 'usertestrepo',
       openid_prefix: 'https://www.google.com/accounts/o8/id'
     }
 
@@ -478,7 +478,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
         created['uuid'], created['email'], 'arvados#user', false, 'User'
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
-        'test_repo', created['uuid'], 'arvados#repository', true, 'Repository'
+        'foo/usertestrepo', created['uuid'], 'arvados#repository', true, 'Repository'
 
     verify_link response_items, 'arvados#group', true, 'permission', 'can_read',
         'All users', created['uuid'], 'arvados#group', true, 'Group'
@@ -522,7 +522,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
     # invoke setup with a repository
     post :setup, {
-      repo_name: 'new_repo',
+      repo_name: 'usertestrepo',
       uuid: active_user['uuid']
     }
 
@@ -538,7 +538,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
         'All users', created['uuid'], 'arvados#group', true, 'Group'
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
-        'new_repo', created['uuid'], 'arvados#repository', true, 'Repository'
+        'active/usertestrepo', created['uuid'], 'arvados#repository', true, 'Repository'
 
     verify_link response_items, 'arvados#virtualMachine', false, 'permission', 'can_login',
         nil, created['uuid'], 'arvados#virtualMachine', false, 'VirtualMachine'
@@ -547,6 +547,11 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "setup active user with vm and no repo" do
     authorize_with :admin
     active_user = users(:active)
+    repos_query = Repository.where(owner_uuid: active_user.uuid)
+    repo_link_query = Link.where(tail_uuid: active_user.uuid,
+                                 link_class: "permission", name: "can_manage")
+    repos_count = repos_query.count
+    repo_link_count = repo_link_query.count
 
     # invoke setup with a repository
     post :setup, {
@@ -566,8 +571,8 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     verify_link response_items, 'arvados#group', true, 'permission', 'can_read',
         'All users', created['uuid'], 'arvados#group', true, 'Group'
 
-    verify_link response_items, 'arvados#repository', false, 'permission', 'can_manage',
-        'new_repo', created['uuid'], 'arvados#repository', true, 'Repository'
+    assert_equal(repos_count, repos_query.count)
+    assert_equal(repo_link_count, repo_link_query.count)
 
     verify_link response_items, 'arvados#virtualMachine', true, 'permission', 'can_login',
         @vm_uuid, created['uuid'], 'arvados#virtualMachine', false, 'VirtualMachine'
diff --git a/services/api/test/integration/crunch_dispatch_test.rb b/services/api/test/integration/crunch_dispatch_test.rb
index 81767af..a6f937b 100644
--- a/services/api/test/integration/crunch_dispatch_test.rb
+++ b/services/api/test/integration/crunch_dispatch_test.rb
@@ -28,7 +28,7 @@ class CrunchDispatchTest < ActionDispatch::IntegrationTest
       format: "json",
       job: {
         script: "log",
-        repository: "crunch_dispatch_test",
+        repository: "active/crunchdispatchtest",
         script_version: "f35f99b7d32bac257f5989df02b9f12ee1a9b0d6",
         script_parameters: "{}"
       }
diff --git a/services/api/test/integration/serialized_encoding_test.rb b/services/api/test/integration/serialized_encoding_test.rb
index 8a1cb10..36c533a 100644
--- a/services/api/test/integration/serialized_encoding_test.rb
+++ b/services/api/test/integration/serialized_encoding_test.rb
@@ -12,7 +12,7 @@ class SerializedEncodingTest < ActionDispatch::IntegrationTest
     human: {properties: {eye_color: 'gray'}},
 
     job: {
-      repository: 'foo',
+      repository: 'active/foo',
       runtime_constraints: {docker_image: 'arvados/apitestfixture'},
       script: 'hash',
       script_version: 'master',
diff --git a/services/api/test/integration/users_test.rb b/services/api/test/integration/users_test.rb
index 0d6c0f3..38ac122 100644
--- a/services/api/test/integration/users_test.rb
+++ b/services/api/test/integration/users_test.rb
@@ -5,7 +5,7 @@ class UsersTest < ActionDispatch::IntegrationTest
   include UsersTestHelper
 
   test "setup user multiple times" do
-    repo_name = 'test_repo'
+    repo_name = 'usertestrepo'
 
     post "/arvados/v1/users/setup", {
       repo_name: repo_name,
@@ -35,7 +35,7 @@ class UsersTest < ActionDispatch::IntegrationTest
         created['uuid'], created['email'], 'arvados#user', false, 'arvados#user'
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
-        repo_name, created['uuid'], 'arvados#repository', true, 'Repository'
+        'foo/usertestrepo', created['uuid'], 'arvados#repository', true, 'Repository'
 
     verify_link response_items, 'arvados#group', true, 'permission', 'can_read',
         'All users', created['uuid'], 'arvados#group', true, 'Group'
@@ -71,7 +71,7 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     # arvados#user, repo link and link add user to 'All users' group
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
-        repo_name, created['uuid'], 'arvados#repository', true, 'Repository'
+        'foo/usertestrepo', created['uuid'], 'arvados#repository', true, 'Repository'
 
     verify_link response_items, 'arvados#group', true, 'permission', 'can_read',
         'All users', created['uuid'], 'arvados#group', true, 'Group'
@@ -105,16 +105,13 @@ class UsersTest < ActionDispatch::IntegrationTest
     verify_link response_items, 'arvados#group', true, 'permission', 'can_read',
         'All users', created['uuid'], 'arvados#group', true, 'Group'
 
-    verify_link response_items, 'arvados#repository', false, 'permission', 'can_manage',
-        'test_repo', created['uuid'], 'arvados#repository', true, 'Repository'
-
     verify_link response_items, 'arvados#virtualMachine', false, 'permission', 'can_login',
         nil, created['uuid'], 'arvados#virtualMachine', false, 'VirtualMachine'
 
    # invoke setup with a repository
     post "/arvados/v1/users/setup", {
       openid_prefix: 'http://www.example.com/account',
-      repo_name: 'new_repo',
+      repo_name: 'newusertestrepo',
       uuid: created['uuid']
     }, auth(:admin)
 
@@ -130,7 +127,7 @@ class UsersTest < ActionDispatch::IntegrationTest
         'All users', created['uuid'], 'arvados#group', true, 'Group'
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
-        'new_repo', created['uuid'], 'arvados#repository', true, 'Repository'
+        'foo/newusertestrepo', created['uuid'], 'arvados#repository', true, 'Repository'
 
     verify_link response_items, 'arvados#virtualMachine', false, 'permission', 'can_login',
         nil, created['uuid'], 'arvados#virtualMachine', false, 'VirtualMachine'
@@ -156,17 +153,13 @@ class UsersTest < ActionDispatch::IntegrationTest
     verify_link response_items, 'arvados#group', true, 'permission', 'can_read',
         'All users', created['uuid'], 'arvados#group', true, 'Group'
 
-    # since no repo name in input, we won't get any; even though user has one
-    verify_link response_items, 'arvados#repository', false, 'permission', 'can_manage',
-        'new_repo', created['uuid'], 'arvados#repository', true, 'Repository'
-
     verify_link response_items, 'arvados#virtualMachine', true, 'permission', 'can_login',
         virtual_machines(:testvm).uuid, created['uuid'], 'arvados#virtualMachine', false, 'VirtualMachine'
   end
 
   test "setup and unsetup user" do
     post "/arvados/v1/users/setup", {
-      repo_name: 'test_repo',
+      repo_name: 'newusertestrepo',
       vm_uuid: virtual_machines(:testvm).uuid,
       user: {email: 'foo at example.com'},
       openid_prefix: 'https://www.google.com/accounts/o8/id'
@@ -186,7 +179,7 @@ class UsersTest < ActionDispatch::IntegrationTest
         'All users', created['uuid'], 'arvados#group', true, 'Group'
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
-        'test_repo', created['uuid'], 'arvados#repository', true, 'Repository'
+        'foo/newusertestrepo', created['uuid'], 'arvados#repository', true, 'Repository'
 
     verify_link response_items, 'arvados#virtualMachine', true, 'permission', 'can_login',
         virtual_machines(:testvm).uuid, created['uuid'], 'arvados#virtualMachine', false, 'VirtualMachine'
diff --git a/services/api/test/test.git.tar b/services/api/test/test.git.tar
index ae46601..9b8e4d5 100644
Binary files a/services/api/test/test.git.tar and b/services/api/test/test.git.tar differ
diff --git a/services/api/test/test_helper.rb b/services/api/test/test_helper.rb
index 5ea6e62..f155fc0 100644
--- a/services/api/test/test_helper.rb
+++ b/services/api/test/test_helper.rb
@@ -52,6 +52,16 @@ class ActiveSupport::TestCase
     restore_configuration
   end
 
+  def assert_not_allowed
+    # Provide a block that calls a Rails boolean "true or false" success value,
+    # like model.save or model.destroy.  This method will test that it either
+    # returns false, or raises a Permission Denied exception.
+    begin
+      refute(yield)
+    rescue ArvadosModel::PermissionDeniedError
+    end
+  end
+
   def restore_configuration
     # Restore configuration settings changed during tests
     $application_config.each do |k,v|
diff --git a/services/api/test/unit/job_test.rb b/services/api/test/unit/job_test.rb
index 24bc260..1c8573e 100644
--- a/services/api/test/unit/job_test.rb
+++ b/services/api/test/unit/job_test.rb
@@ -15,7 +15,7 @@ class JobTest < ActiveSupport::TestCase
     {
       script: "hash",
       script_version: "master",
-      repository: "foo",
+      repository: "active/foo",
     }.merge(merge_me)
   end
 
diff --git a/services/api/test/unit/permission_test.rb b/services/api/test/unit/permission_test.rb
index 20cffda..4a6ddc6 100644
--- a/services/api/test/unit/permission_test.rb
+++ b/services/api/test/unit/permission_test.rb
@@ -353,18 +353,4 @@ class PermissionTest < ActiveSupport::TestCase
       ob.update_attributes!(owner_uuid: groups(:aproject).uuid)
     end
   end
-
-  test "active user cannot write admin's repo" do
-    set_user_from_auth :active
-    assert_raises ArvadosModel::PermissionDeniedError, "pwned" do
-      repositories(:repository3).update_attributes(name: "kilroy")
-    end
-  end
-
-  test "active user cannot change repo name via can_manage permission" do
-    set_user_from_auth :active
-    assert_raises ArvadosModel::PermissionDeniedError, "pwned" do
-      repositories(:foo).update_attributes(name: "arvados")
-    end
-  end
 end
diff --git a/services/api/test/unit/repository_test.rb b/services/api/test/unit/repository_test.rb
index 4e160dd..ef780f8 100644
--- a/services/api/test/unit/repository_test.rb
+++ b/services/api/test/unit/repository_test.rb
@@ -1,6 +1,233 @@
 require 'test_helper'
+require 'helpers/git_test_helper'
 
 class RepositoryTest < ActiveSupport::TestCase
+  include GitTestHelper
+
+  def new_repo(owner_key, attrs={})
+    set_user_from_auth owner_key
+    owner = users(owner_key)
+    Repository.new({owner_uuid: owner.uuid}.merge(attrs))
+  end
+
+  def changed_repo(repo_key, changes)
+    repo = repositories(repo_key)
+    changes.each_pair { |attr, value| repo.send("#{attr}=".to_sym, value) }
+    repo
+  end
+
+  def default_git_url(repo_name, user_name=nil)
+    if user_name
+      "git at git.%s.arvadosapi.com:%s/%s.git" %
+        [Rails.configuration.uuid_prefix, user_name, repo_name]
+    else
+      "git at git.%s.arvadosapi.com:%s.git" %
+        [Rails.configuration.uuid_prefix, repo_name]
+    end
+  end
+
+  def assert_server_path(path_tail, repo_sym)
+    assert_equal(File.join(Rails.configuration.git_repositories_dir, path_tail),
+                 repositories(repo_sym).server_path)
+  end
+
+  ### name validation
+
+  {active: "active/", admin: "admin/", system_user: ""}.
+      each_pair do |user_sym, name_prefix|
+    %w(a aa a0 aA Aa AA A0).each do |name|
+      test "'#{name_prefix}#{name}' is a valid name for #{user_sym} repo" do
+        repo = new_repo(user_sym, name: name_prefix + name)
+        assert(repo.valid?)
+      end
+    end
+
+    test "name is required for #{user_sym} repo" do
+      refute(new_repo(user_sym).valid?)
+    end
+
+    test "repo name beginning with numeral is invalid for #{user_sym}" do
+      repo = new_repo(user_sym, name: "#{name_prefix}0a")
+      refute(repo.valid?)
+    end
+
+    "\\.-_/!@#$%^&*()[]{}".each_char do |bad_char|
+      test "name containing #{bad_char.inspect} is invalid for #{user_sym}" do
+        repo = new_repo(user_sym, name: "#{name_prefix}bad#{bad_char}reponame")
+        refute(repo.valid?)
+      end
+    end
+  end
+
+  test "admin can create valid repo for other user with correct name prefix" do
+    owner = users(:active)
+    repo = new_repo(:admin, name: "#{owner.username}/validnametest",
+                    owner_uuid: owner.uuid)
+    assert(repo.valid?)
+  end
+
+  test "admin can create valid system repo without name prefix" do
+    repo = new_repo(:admin, name: "validnametest",
+                    owner_uuid: users(:system_user).uuid)
+    assert(repo.valid?)
+  end
+
+  test "repo name prefix must match owner_uuid username" do
+    repo = new_repo(:admin, name: "admin/badusernametest",
+                    owner_uuid: users(:active).uuid)
+    refute(repo.valid?)
+  end
+
+  test "repo name prefix must be empty for system repo" do
+    repo = new_repo(:admin, name: "root/badprefixtest",
+                    owner_uuid: users(:system_user).uuid)
+    refute(repo.valid?)
+  end
+
+  ### owner validation
+
+  test "name must be unique per user" do
+    repo = new_repo(:active, name: repositories(:foo).name)
+    refute(repo.valid?)
+  end
+
+  test "name can be duplicated across users" do
+    repo = new_repo(:active, name: "active/#{repositories(:arvados).name}")
+    assert(repo.valid?)
+  end
+
+  test "repository cannot be owned by a group" do
+    set_user_from_auth :active
+    repo = Repository.new(owner_uuid: groups(:all_users).uuid,
+                          name: "ownedbygroup")
+    refute(repo.valid?)
+    refute_empty(repo.errors[:owner_uuid] || [])
+  end
+
+  ### URL generation
+
+  test "fetch_url" do
+    repo = new_repo(:active, name: "active/fetchtest")
+    assert_equal(default_git_url("fetchtest", "active"), repo.fetch_url)
+  end
+
+  test "fetch_url owned by system user" do
+    set_user_from_auth :admin
+    repo = Repository.new(owner_uuid: users(:system_user).uuid,
+                          name: "fetchtest")
+    assert_equal(default_git_url("fetchtest"), repo.fetch_url)
+  end
+
+  test "push_url" do
+    repo = new_repo(:active, name: "active/pushtest")
+    assert_equal(default_git_url("pushtest", "active"), repo.push_url)
+  end
+
+  test "push_url owned by system user" do
+    set_user_from_auth :admin
+    repo = Repository.new(owner_uuid: users(:system_user).uuid,
+                          name: "pushtest")
+    assert_equal(default_git_url("pushtest"), repo.push_url)
+  end
+
+  ### Path generation
+
+  test "disk path stored by UUID" do
+    assert_server_path("zzzzz-s0uqq-382brsig8rp3666/.git", :foo)
+  end
+
+  test "disk path stored by name" do
+    assert_server_path("arvados/.git", :arvados)
+  end
+
+  test "disk path for repository not on disk" do
+    assert_nil(Repository.new.server_path)
+  end
+
+  ### Repository creation
+
+  test "non-admin can create a repository for themselves" do
+    repo = new_repo(:active, name: "active/newtestrepo")
+    assert(repo.save)
+  end
+
+  test "non-admin can't create a repository for another visible user" do
+    repo = new_repo(:active, name: "repoforanon",
+                    owner_uuid: users(:anonymous).uuid)
+    assert_not_allowed { repo.save }
+  end
+
+  test "admin can create a repository for themselves" do
+    repo = new_repo(:admin, name: "admin/newtestrepo")
+    assert(repo.save)
+  end
+
+  test "admin can create a repository for others" do
+    repo = new_repo(:admin, name: "active/repoforactive",
+                    owner_uuid: users(:active).uuid)
+    assert(repo.save)
+  end
+
+  test "admin can create a system repository" do
+    repo = new_repo(:admin, name: "repoforsystem",
+                    owner_uuid: users(:system_user).uuid)
+    assert(repo.save)
+  end
+
+  ### Repository destruction
+
+  test "non-admin can destroy their own repository" do
+    set_user_from_auth :active
+    assert(repositories(:foo).destroy)
+  end
+
+  test "non-admin can't destroy others' repository" do
+    set_user_from_auth :active
+    assert_not_allowed { repositories(:repository3).destroy }
+  end
+
+  test "non-admin can't destroy system repository" do
+    set_user_from_auth :active
+    assert_not_allowed { repositories(:arvados).destroy }
+  end
+
+  test "admin can destroy their own repository" do
+    set_user_from_auth :admin
+    assert(repositories(:repository3).destroy)
+  end
+
+  test "admin can destroy others' repository" do
+    set_user_from_auth :admin
+    assert(repositories(:foo).destroy)
+  end
+
+  test "admin can destroy system repository" do
+    set_user_from_auth :admin
+    assert(repositories(:arvados).destroy)
+  end
+
+  ### Changing ownership
+
+  test "non-admin can't make their repository a system repository" do
+    set_user_from_auth :active
+    repo = changed_repo(:foo, owner_uuid: users(:system_user).uuid)
+    assert_not_allowed { repo.save }
+  end
+
+  test "admin can give their repository to someone else" do
+    set_user_from_auth :admin
+    repo = changed_repo(:repository3, owner_uuid: users(:active).uuid,
+                        name: "active/foo3")
+    assert(repo.save)
+  end
+
+  test "admin can make their repository a system repository" do
+    set_user_from_auth :admin
+    repo = changed_repo(:repository3, owner_uuid: users(:system_user).uuid,
+                        name: "foo3")
+    assert(repo.save)
+  end
+
   test 'write permission allows changing modified_at' do
     act_as_user users(:active) do
       r = repositories(:foo)
diff --git a/services/api/test/unit/user_test.rb b/services/api/test/unit/user_test.rb
index 6eabdfd..45dd186 100644
--- a/services/api/test/unit/user_test.rb
+++ b/services/api/test/unit/user_test.rb
@@ -39,14 +39,10 @@ class UserTest < ActiveSupport::TestCase
   end
 
   test "non-admin can't update username" do
-    set_user_from_auth :active_trustedclient
-    user = User.find_by_uuid(users(:active).uuid)
+    set_user_from_auth :rominiadmin
+    user = User.find_by_uuid(users(:rominiadmin).uuid)
     user.username = "selfupdate"
-    begin
-      refute(user.save)
-    rescue ArvadosModel::PermissionDeniedError
-      # That works too.
-    end
+    assert_not_allowed { user.save }
   end
 
   def check_admin_username_change(fixture_name)
@@ -108,6 +104,42 @@ class UserTest < ActiveSupport::TestCase
     check_new_username_setting("_", nil)
   end
 
+  test "updating username updates repository names" do
+    set_user_from_auth :admin
+    user = users(:active)
+    user.username = "newtestname"
+    assert(user.save, "username update failed")
+    {foo: "newtestname/foo", repository2: "newtestname/foo2"}.
+        each_pair do |repo_sym, expect_name|
+      assert_equal(expect_name, repositories(repo_sym).name)
+    end
+  end
+
+  test "admin can clear username when user owns no repositories" do
+    set_user_from_auth :admin
+    user = users(:spectator)
+    user.username = nil
+    assert(user.save)
+    assert_nil(user.username)
+  end
+
+  test "admin can't clear username when user owns repositories" do
+    set_user_from_auth :admin
+    user = users(:active)
+    start_username = user.username
+    user.username = nil
+    assert_not_allowed { user.save }
+    refute_empty(user.errors[:username])
+  end
+
+  test "failed username update doesn't change repository names" do
+    set_user_from_auth :admin
+    user = users(:active)
+    user.username = users(:fuse).username
+    assert_not_allowed { user.save }
+    assert_equal("active/foo", repositories(:foo).name)
+  end
+
   [[false, 'foo at example.com', true, nil],
    [false, 'bar at example.com', nil, true],
    [true, 'foo at example.com', true, nil],
@@ -391,14 +423,7 @@ class UserTest < ActiveSupport::TestCase
 
   test "create new user as non-admin user" do
     set_user_from_auth :active
-
-    begin
-      user = User.new
-      user.save
-    rescue ArvadosModel::PermissionDeniedError => e
-    end
-    assert (e.message.include? 'PermissionDeniedError'),
-        'Expected PermissionDeniedError'
+    assert_not_allowed { User.new.save }
   end
 
   test "setup new user" do
@@ -411,7 +436,7 @@ class UserTest < ActiveSupport::TestCase
 
     vm = VirtualMachine.create
 
-    response = User.setup user, openid_prefix, 'test_repo', vm.uuid
+    response = User.setup user, openid_prefix, 'foo/testrepo', vm.uuid
 
     resp_user = find_obj_in_resp response, 'User'
     verify_user resp_user, email
@@ -453,7 +478,7 @@ class UserTest < ActiveSupport::TestCase
 
     verify_link resp_link, 'permission', 'can_login', email, bad_uuid
 
-    response = User.setup user, openid_prefix, 'test_repo', vm.uuid
+    response = User.setup user, openid_prefix, 'foo/testrepo', vm.uuid
 
     resp_user = find_obj_in_resp response, 'User'
     verify_user resp_user, email
@@ -499,7 +524,7 @@ class UserTest < ActiveSupport::TestCase
     verify_link group_perm, 'permission', 'can_read', resp_user[:uuid], nil
 
     # invoke setup again with repo_name
-    response = User.setup user, openid_prefix, 'test_repo'
+    response = User.setup user, openid_prefix, 'foo/testrepo'
     resp_user = find_obj_in_resp response, 'User', nil
     verify_user resp_user, email
     assert_equal user.uuid, resp_user[:uuid], 'expected uuid not found'
@@ -513,7 +538,7 @@ class UserTest < ActiveSupport::TestCase
     # invoke setup again with a vm_uuid
     vm = VirtualMachine.create
 
-    response = User.setup user, openid_prefix, 'test_repo', vm.uuid
+    response = User.setup user, openid_prefix, 'foo/testrepo', vm.uuid
 
     resp_user = find_obj_in_resp response, 'User', nil
     verify_user resp_user, email
@@ -582,7 +607,8 @@ class UserTest < ActiveSupport::TestCase
 
     can_setup = (Rails.configuration.auto_setup_new_users and
                  (not expect_username.nil?))
-    prior_repo = Repository.where(name: expect_username).first
+    expect_repo_name = "#{expect_username}/#{expect_username}"
+    prior_repo = Repository.where(name: expect_repo_name).first
 
     user = User.new
     user.first_name = "first_name_for_newly_created_user"
@@ -600,7 +626,7 @@ class UserTest < ActiveSupport::TestCase
                        user.uuid, user.email, "permission", "can_login")
     # Check for repository.
     if named_repo = (prior_repo or
-                     Repository.where(name: expect_username).first)
+                     Repository.where(name: expect_repo_name).first)
       verify_link_exists((can_setup and prior_repo.nil? and
                           Rails.configuration.auto_setup_new_users_with_repository),
                          named_repo.uuid, user.uuid, "permission", "can_manage")

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list