accept_attribute_as_json :prefs, Hash
skip_before_filter :find_object_by_uuid, only:
- [:activate, :current, :system, :setup]
+ [:activate, :current, :system, :setup, :merge]
skip_before_filter :render_404_if_no_object, only:
- [:activate, :current, :system, :setup]
+ [:activate, :current, :system, :setup, :merge]
before_filter :admin_required, only: [:setup, :unsetup, :update_uuid]
def current
show
end
+ def merge
+ if !Thread.current[:api_client].andand.is_trusted
+ return send_error("supplied API token is not from a trusted client", status: 403)
+ end
+
+ dst_auth = ApiClientAuthorization.validate(token: params[:new_user_token])
+ if !dst_auth
+ return send_error("invalid new_user_token", status: 401)
+ end
+ if !dst_auth.api_client.andand.is_trusted
+ return send_error("supplied new_user_token is not from a trusted client", status: 403)
+ end
+ dst_user = dst_auth.user
+
+ if current_user.uuid == dst_user.uuid
+ return send_error("cannot merge user to self", status: 422)
+ end
+
+ if !dst_user.can?(write: params[:new_owner_uuid])
+ return send_error("new_owner_uuid is not writable", status: 403)
+ end
+
+ redirect = params[:redirect_to_new_user]
+ if !redirect
+ return send_error("merge with redirect_to_new_user=false is not yet supported", status: 422)
+ end
+
+ @object = current_user
+ act_as_system_user do
+ @object.merge(new_owner_uuid: params[:new_owner_uuid], redirect_to_user_uuid: redirect && dst_user.uuid)
+ end
+ show
+ end
+
protected
def self._setup_requires_parameters
# Only local users can create sessions, hence uuid_like_pattern
# here.
- user = User.where('identity_url = ? and uuid like ?',
- omniauth['info']['identity_url'],
- User.uuid_like_pattern).first
+ user = User.unscoped.where('identity_url = ? and uuid like ?',
+ omniauth['info']['identity_url'],
+ User.uuid_like_pattern).first
if not user
# Check for permission to log in to an existing User record with
# a different identity_url
end
end
end
+
+ while (uuid = user.andand.redirect_to_user_uuid)
+ user = User.where(uuid: uuid).first
+ if !user
+ raise Exception.new("identity_url #{omniauth['info']['identity_url']} redirects to nonexistent uuid #{uuid}")
+ end
+ end
+
if not user
# New user registration
user = User.new(:email => omniauth['info']['email'],
uuid_prefix+".arvadosapi.com")
end
- def self.validate(token:, remote:)
+ def self.validate(token:, remote: nil)
return nil if !token
remote ||= Rails.configuration.uuid_prefix
has_many :authorized_keys, :foreign_key => :authorized_user_uuid, :primary_key => :uuid
has_many :repositories, foreign_key: :owner_uuid, primary_key: :uuid
+ default_scope { where('redirect_to_user_uuid is null') }
+
api_accessible :user, extend: :common do |t|
t.add :email
t.add :username
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
+ change_all_uuid_refs(old_uuid: old_uuid, new_uuid: new_uuid)
+ end
+ end
+
+ # Merge this user's owned items into dst_user.
+ def merge(new_owner_uuid:, redirect_to_user_uuid:)
+ raise PermissionDeniedError if !current_user.andand.is_admin
+ raise "not implemented" if !redirect_to_user_uuid
+ transaction(requires_new: true) do
+ reload
+ new_user = User.where(uuid: redirect_to_user_uuid).first
+ raise "user does not exist" if !new_user
+ if User.where('uuid in (?) and redirect_to_user_uuid is not null',
+ [new_owner_uuid, redirect_to_user_uuid]).any?
+ raise "cannot merge to/from an already merged user"
end
+ ApiClientAuthorization.
+ where(user_id: id).
+ update_all(user_id: new_user.id)
+ [
+ [AuthorizedKey, :owner_uuid],
+ [AuthorizedKey, :authorized_user_uuid],
+ [Repository, :owner_uuid],
+ [Link, :tail_uuid],
+ [Link, :head_uuid],
+ ].each do |klass, column|
+ klass.where(column => uuid).update_all(column => new_user.uuid)
+ end
+ change_all_uuid_refs(old_uuid: uuid, new_uuid: new_owner_uuid)
+ update_attributes!(redirect_to_user_uuid: new_user.uuid)
end
end
protected
+ def change_all_uuid_refs(old_uuid:, new_uuid:)
+ 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
+
def ensure_ownership_path_leads_to_user
true
end
def permission_to_update
- if username_changed?
+ if username_changed? || redirect_to_user_uuid_changed?
current_user.andand.is_admin
else
# users must be able to update themselves (even if they are
def permission_to_create
current_user.andand.is_admin or
- (self == current_user and
+ (self == current_user &&
+ self.redirect_to_user_uuid.nil? &&
self.is_active == Rails.configuration.new_users_are_active)
end
post 'setup', on: :collection
post 'unsetup', on: :member
post 'update_uuid', on: :member
+ post 'merge', on: :collection
end
resources :virtual_machines do
get 'logins', on: :member
--- /dev/null
+class AddRedirectToUserUuidToUsers < ActiveRecord::Migration
+ def up
+ add_column :users, :redirect_to_user_uuid, :string
+ User.reset_column_information
+ remove_index :users, name: 'users_search_index'
+ add_index :users, User.searchable_columns('ilike') - ['prefs'], name: 'users_search_index'
+ end
+
+ def down
+ remove_index :users, name: 'users_search_index'
+ remove_column :users, :redirect_to_user_uuid
+ User.reset_column_information
+ add_index :users, User.searchable_columns('ilike') - ['prefs'], name: 'users_search_index'
+ end
+end
updated_at timestamp without time zone NOT NULL,
default_owner_uuid character varying(255),
is_active boolean DEFAULT false,
- username character varying(255)
+ username character varying(255),
+ redirect_to_user_uuid character varying
);
-- Name: users_search_index; Type: INDEX; Schema: public; Owner: -
--
-CREATE INDEX users_search_index ON users USING btree (uuid, owner_uuid, modified_by_client_uuid, modified_by_user_uuid, email, first_name, last_name, identity_url, default_owner_uuid, username);
+CREATE INDEX users_search_index ON users USING btree (uuid, owner_uuid, modified_by_client_uuid, modified_by_user_uuid, email, first_name, last_name, identity_url, default_owner_uuid, username, redirect_to_user_uuid);
--
INSERT INTO schema_migrations (version) VALUES ('20180313180114');
+INSERT INTO schema_migrations (version) VALUES ('20180501182859');
+
end
end
+ test "refuse to merge with redirect_to_user_uuid=false (not yet supported)" do
+ authorize_with :project_viewer_trustedclient
+ post :merge, {
+ new_user_token: api_client_authorizations(:active_trustedclient).api_token,
+ new_owner_uuid: users(:active).uuid,
+ redirect_to_new_user: false,
+ }
+ assert_response(422)
+ end
+
+ test "refuse to merge user into self" do
+ authorize_with(:active_trustedclient)
+ post(:merge, {
+ new_user_token: api_client_authorizations(:active_trustedclient).api_token,
+ new_owner_uuid: users(:active).uuid,
+ redirect_to_new_user: true,
+ })
+ assert_response(422)
+ end
+
+ [[:active, :project_viewer_trustedclient],
+ [:active_trustedclient, :project_viewer]].each do |src, dst|
+ test "refuse to merge with untrusted token (#{src} -> #{dst})" do
+ authorize_with(src)
+ post(:merge, {
+ new_user_token: api_client_authorizations(dst).api_token,
+ new_owner_uuid: api_client_authorizations(dst).user.uuid,
+ redirect_to_new_user: true,
+ })
+ assert_response(403)
+ end
+ end
+
+ [[:expired_trustedclient, :project_viewer_trustedclient],
+ [:project_viewer_trustedclient, :expired_trustedclient]].each do |src, dst|
+ test "refuse to merge with expired token (#{src} -> #{dst})" do
+ authorize_with(src)
+ post(:merge, {
+ new_user_token: api_client_authorizations(dst).api_token,
+ new_owner_uuid: api_client_authorizations(dst).user.uuid,
+ redirect_to_new_user: true,
+ })
+ assert_response(401)
+ end
+ end
+
+ test "refuse to merge if new_owner_uuid is not writable" do
+ authorize_with(:project_viewer_trustedclient)
+ post(:merge, {
+ new_user_token: api_client_authorizations(:active_trustedclient).api_token,
+ new_owner_uuid: groups(:anonymously_accessible_project).uuid,
+ redirect_to_new_user: true,
+ })
+ assert_response(403)
+ end
+
+ test "refuse to update redirect_to_user_uuid directly" do
+ authorize_with(:active_trustedclient)
+ patch(:update, {
+ id: users(:active).uuid,
+ user: {
+ redirect_to_user_uuid: users(:active).uuid,
+ },
+ })
+ assert_response(403)
+ end
+
+ test "merge 'project_viewer' account into 'active' account" do
+ authorize_with(:project_viewer_trustedclient)
+ post(:merge, {
+ new_user_token: api_client_authorizations(:active_trustedclient).api_token,
+ new_owner_uuid: users(:active).uuid,
+ redirect_to_new_user: true,
+ })
+ assert_response(:success)
+ assert_equal(users(:project_viewer).redirect_to_user_uuid, users(:active).uuid)
+
+ auth = ApiClientAuthorization.validate(token: api_client_authorizations(:project_viewer).api_token)
+ assert_not_nil(auth)
+ assert_not_nil(auth.user)
+ assert_equal(users(:active).uuid, auth.user.uuid)
+ end
NON_ADMIN_USER_DATA = ["uuid", "kind", "is_active", "email", "first_name",
"last_name", "username"].sort
end
nil
end
+
+ test 'merge active into project_viewer account' do
+ post('/arvados/v1/groups', {
+ group: {
+ group_class: 'project',
+ name: "active user's stuff",
+ },
+ }, auth(:project_viewer))
+ assert_response(:success)
+ project_uuid = json_response['uuid']
+
+ post('/arvados/v1/users/merge', {
+ new_user_token: api_client_authorizations(:project_viewer_trustedclient).api_token,
+ new_owner_uuid: project_uuid,
+ redirect_to_new_user: true,
+ }, auth(:active_trustedclient))
+ assert_response(:success)
+
+ get('/arvados/v1/users/current', {}, auth(:active))
+ assert_response(:success)
+ assert_equal(users(:project_viewer).uuid, json_response['uuid'])
+
+ get('/arvados/v1/authorized_keys/' + authorized_keys(:active).uuid, {}, auth(:active))
+ assert_response(:success)
+ assert_equal(users(:project_viewer).uuid, json_response['owner_uuid'])
+ assert_equal(users(:project_viewer).uuid, json_response['authorized_user_uuid'])
+
+ get('/arvados/v1/repositories/' + repositories(:foo).uuid, {}, auth(:active))
+ assert_response(:success)
+ assert_equal(users(:project_viewer).uuid, json_response['owner_uuid'])
+
+ get('/arvados/v1/groups/' + groups(:aproject).uuid, {}, auth(:active))
+ assert_response(:success)
+ assert_equal(project_uuid, json_response['owner_uuid'])
+ end
end