Merge branch '12702-migrate-user'
authorTom Clegg <tclegg@veritasgenetics.com>
Tue, 9 Jan 2018 15:54:50 +0000 (10:54 -0500)
committerTom Clegg <tclegg@veritasgenetics.com>
Tue, 9 Jan 2018 15:54:50 +0000 (10:54 -0500)
closes #12702

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

doc/api/methods/users.html.textile.liquid
services/api/app/controllers/arvados/v1/users_controller.rb
services/api/app/models/user.rb
services/api/config/routes.rb
services/api/test/functional/arvados/v1/users_controller_test.rb
services/api/test/unit/user_test.rb

index 3a28e1e85610f4120aa3a4d4082c80c25edd8b3b..2e5dee51810f188591c7903776d1d70e3b23f289 100644 (file)
@@ -111,3 +111,16 @@ table(table table-bordered table-condensed).
 |_. Argument |_. Type |_. Description |_. Location |_. Example |
 {background:#ccffcc}.|uuid|string|The UUID of the User in question.|path||
 |user|object||query||
+
+h3. update_uuid
+
+Change the UUID of an existing user, updating all database references accordingly.
+
+This method can only be used by an admin user. It should only be used when the affected user is idle. New references to the affected user that are established _while the update_uuid operation is in progress_ might not be migrated as expected.
+
+Arguments:
+
+table(table table-bordered table-condensed).
+|_. Argument |_. Type |_. Description |_. Location |_. Example |
+{background:#ccffcc}.|uuid|string|The current UUID of the user in question.|path|@zzzzz-tpzed-12345abcde12345@|
+{background:#ccffcc}.|new_uuid|string|The desired new UUID. It is an error to use a UUID belonging to an existing user.|query|@zzzzz-tpzed-abcde12345abcde@|
index 5e1235210a2a33d6d6c6b27c9daa36cf4a11e1bd..dc7e62f3e340741bec37e1d72bbb99c7ccf797d4 100644 (file)
@@ -9,7 +9,7 @@ class Arvados::V1::UsersController < ApplicationController
     [:activate, :current, :system, :setup]
   skip_before_filter :render_404_if_no_object, only:
     [:activate, :current, :system, :setup]
-  before_filter :admin_required, only: [:setup, :unsetup]
+  before_filter :admin_required, only: [:setup, :unsetup, :update_uuid]
 
   def current
     if current_user
@@ -118,6 +118,13 @@ class Arvados::V1::UsersController < ApplicationController
     show
   end
 
+  # Change UUID to a new (unused) uuid and transfer all owned/linked
+  # objects accordingly.
+  def update_uuid
+    @object.update_uuid(new_uuid: params[:new_uuid])
+    show
+  end
+
   protected
 
   def self._setup_requires_parameters
@@ -140,6 +147,14 @@ class Arvados::V1::UsersController < ApplicationController
     }
   end
 
+  def self._update_uuid_requires_parameters
+    {
+      new_uuid: {
+        type: 'string', required: true,
+      },
+    }
+  end
+
   def apply_filters(model_class=nil)
     return super if @read_users.any?(&:is_admin)
     if params[:uuid] != current_user.andand.uuid
index f3ca75929f7da09695759a7c92643d47a04158da..9209411f1e30fbb36ab0d44769c2815550bac0a0 100644 (file)
@@ -254,6 +254,32 @@ class User < ArvadosModel
     end
   end
 
+  def update_uuid(new_uuid:)
+    if !current_user.andand.is_admin
+      raise PermissionDeniedError
+    end
+    if uuid == system_user_uuid || uuid == anonymous_user_uuid
+      raise "update_uuid cannot update system accounts"
+    end
+    if self.class != self.class.resource_class_for_uuid(new_uuid)
+      raise "invalid new_uuid #{new_uuid.inspect}"
+    end
+    transaction(requires_new: true) do
+      reload
+      old_uuid = self.uuid
+      self.uuid = new_uuid
+      save!(validate: false)
+      ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |klass|
+        klass.columns.each do |col|
+          if col.name.end_with?('_uuid')
+            column = col.name.to_sym
+            klass.where(column => old_uuid).update_all(column => new_uuid)
+          end
+        end
+      end
+    end
+  end
+
   protected
 
   def ensure_ownership_path_leads_to_user
index 5b6fe80975faf0d5a56be3e17150d600ce525bc6..fcd5c34212d16187207e64abc8372e43ecd09af8 100644 (file)
@@ -79,6 +79,7 @@ Server::Application.routes.draw do
         post 'activate', on: :member
         post 'setup', on: :collection
         post 'unsetup', on: :member
+        post 'update_uuid', on: :member
       end
       resources :virtual_machines do
         get 'logins', on: :member
index b75479ff8d145f0b11712273df0970f3f3ff6dc6..a50648617fd59aea8fb1bdb39c7066b3b4e60974 100644 (file)
@@ -794,6 +794,27 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
                     "user's writable_by should include its owner_uuid")
   end
 
+  [
+    [:admin, true],
+    [:active, false],
+  ].each do |auth_user, expect_success|
+    test "update_uuid as #{auth_user}" do
+      authorize_with auth_user
+      orig_uuid = users(:active).uuid
+      post :update_uuid, {
+             id: orig_uuid,
+             new_uuid: 'zbbbb-tpzed-abcde12345abcde',
+           }
+      if expect_success
+        assert_response :success
+        assert_empty User.where(uuid: orig_uuid)
+      else
+        assert_response 403
+        assert_not_empty User.where(uuid: orig_uuid)
+      end
+    end
+  end
+
 
   NON_ADMIN_USER_DATA = ["uuid", "kind", "is_active", "email", "first_name",
                          "last_name"].sort
index 3d8fb9da7522f29d508fade008afa4c63d2692f9..72beca6c78134dbe92bd9ce4b65d8b3e70c6d530 100644 (file)
@@ -721,4 +721,83 @@ class UserTest < ActiveSupport::TestCase
     end
   end
 
+  [
+    [:active, 'zzzzz-borkd-abcde12345abcde'],
+    [:active, 'zzzzz-j7d0g-abcde12345abcde'],
+    [:active, 'zzzzz-tpzed-borkd'],
+    [:system_user, 'zzzzz-tpzed-abcde12345abcde'],
+    [:anonymous, 'zzzzz-tpzed-abcde12345abcde'],
+  ].each do |fixture, new_uuid|
+    test "disallow update_uuid #{fixture} -> #{new_uuid}" do
+      u = users(fixture)
+      orig_uuid = u.uuid
+      act_as_system_user do
+        assert_raises do
+          u.update_uuid(new_uuid: new_uuid)
+        end
+      end
+      # "Successfully aborted orig->new" outcome looks the same as
+      # "successfully updated new->orig".
+      assert_update_success(old_uuid: new_uuid,
+                            new_uuid: orig_uuid,
+                            expect_owned_objects: fixture == :active)
+    end
+  end
+
+  [:active, :spectator, :admin].each do |target|
+    test "update_uuid on #{target} as non-admin user" do
+      act_as_user users(:active) do
+        assert_raises(ArvadosModel::PermissionDeniedError) do
+          users(target).update_uuid(new_uuid: 'zzzzz-tpzed-abcde12345abcde')
+        end
+      end
+    end
+  end
+
+  test "update_uuid to existing uuid" do
+    u = users(:active)
+    orig_uuid = u.uuid
+    new_uuid = users(:admin).uuid
+    act_as_system_user do
+      assert_raises do
+        u.update_uuid(new_uuid: new_uuid)
+      end
+    end
+    u.reload
+    assert_equal u.uuid, orig_uuid
+    assert_not_empty Collection.where(owner_uuid: orig_uuid)
+    assert_not_empty Group.where(owner_uuid: orig_uuid)
+  end
+
+  [
+    [:active, 'zbbbb-tpzed-abcde12345abcde'],
+    [:active, 'zzzzz-tpzed-abcde12345abcde'],
+    [:admin, 'zbbbb-tpzed-abcde12345abcde'],
+    [:admin, 'zzzzz-tpzed-abcde12345abcde'],
+  ].each do |fixture, new_uuid|
+    test "update_uuid #{fixture} to unused uuid #{new_uuid}" do
+      u = users(fixture)
+      orig_uuid = u.uuid
+      act_as_system_user do
+        u.update_uuid(new_uuid: new_uuid)
+      end
+      assert_update_success(old_uuid: orig_uuid,
+                            new_uuid: new_uuid,
+                            expect_owned_objects: fixture == :active)
+    end
+  end
+
+  def assert_update_success(old_uuid:, new_uuid:, expect_owned_objects: true)
+    [[User, :uuid],
+     [Link, :head_uuid],
+     [Link, :tail_uuid],
+     [Group, :owner_uuid],
+     [Collection, :owner_uuid],
+    ].each do |klass, attr|
+      assert_empty klass.where(attr => old_uuid)
+      if klass == User || expect_owned_objects
+        assert_not_empty klass.where(attr => new_uuid)
+      end
+    end
+  end
 end