X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/ee5443faad325b16047b9ad4cd588baf51e231fa..HEAD:/services/api/app/models/user.rb diff --git a/services/api/app/models/user.rb b/services/api/app/models/user.rb index 8c8039f1b8..299b20baa6 100644 --- a/services/api/app/models/user.rb +++ b/services/api/app/models/user.rb @@ -25,15 +25,13 @@ class User < ArvadosModel before_update :prevent_privilege_escalation before_update :prevent_inactive_admin before_update :prevent_nonadmin_system_root - before_update :verify_repositories_empty, :if => Proc.new { - username.nil? and username_changed? - } after_update :setup_on_activate before_create :check_auto_admin - before_create :set_initial_username, :if => Proc.new { - username.nil? and email + before_validation :set_initial_username, :if => Proc.new { + new_record? && email } + before_create :active_is_not_nil after_create :after_ownership_change after_create :setup_on_activate after_create :add_system_group_permission_link @@ -48,16 +46,10 @@ class User < ArvadosModel before_update :before_ownership_change after_update :after_ownership_change after_update :send_profile_created_notification - after_update :sync_repository_names, :if => Proc.new { - (uuid != system_user_uuid) and - saved_change_to_username? and - (not username_before_last_save.nil?) - } before_destroy :clear_permissions after_destroy :remove_self_from_permissions - has_many :authorized_keys, :foreign_key => :authorized_user_uuid, :primary_key => :uuid - has_many :repositories, foreign_key: :owner_uuid, primary_key: :uuid + has_many :authorized_keys, foreign_key: 'authorized_user_uuid', primary_key: 'uuid' default_scope { where('redirect_to_user_uuid is null') } @@ -104,6 +96,10 @@ class User < ArvadosModel self.groups_i_can(:read).select { |x| x.match(/-f+$/) }.first) end + def self.ignored_select_attributes + super + ["full_name", "is_invited"] + end + def groups_i_can(verb) my_groups = self.group_permissions(VAL_FOR_PERM[verb]).keys if verb == :read @@ -145,10 +141,10 @@ SELECT 1 FROM #{PERMISSION_VIEW} }, # "name" arg is a query label that appears in logs: "user_can_query", - [[nil, self.uuid], - [nil, target_uuid], - [nil, VAL_FOR_PERM[action]], - [nil, target_owner_uuid]] + [self.uuid, + target_uuid, + VAL_FOR_PERM[action], + target_owner_uuid] ).any? return false end @@ -237,7 +233,7 @@ SELECT target_uuid, perm_level # "name" arg is a query label that appears in logs: "User.group_permissions", # "binds" arg is an array of [col_id, value] for '$1' vars: - [[nil, uuid]]). + [uuid]). rows.each do |group_uuid, max_p_val| @group_perms[group_uuid] = PERMS_FOR_VAL[max_p_val.to_i] end @@ -256,23 +252,16 @@ SELECT target_uuid, perm_level end # create links - def setup(repo_name: nil, vm_uuid: nil, send_notification_email: nil) + def setup(vm_uuid: nil, send_notification_email: nil) newly_invited = Link.where(tail_uuid: self.uuid, head_uuid: all_users_group_uuid, - link_class: 'permission', - name: 'can_read').empty? + link_class: 'permission').empty? # Add can_read link from this user to "all users" which makes this # user "invited", and (depending on config) a link in the opposite # direction which makes this user visible to other users. group_perms = add_to_all_users_group - # Add git repo - repo_perm = if (!repo_name.nil? || Rails.configuration.Users.AutoSetupNewUsersWithRepository) and !username.nil? - repo_name ||= "#{username}/#{username}" - create_user_repo_link repo_name - end - # Add virtual machine if vm_uuid.nil? and !Rails.configuration.Users.AutoSetupNewUsersWithVmUUID.empty? vm_uuid = Rails.configuration.Users.AutoSetupNewUsersWithVmUUID @@ -284,7 +273,7 @@ SELECT target_uuid, perm_level # Send welcome email if send_notification_email.nil? - send_notification_email = Rails.configuration.Mail.SendUserSetupNotificationEmail + send_notification_email = Rails.configuration.Users.SendUserSetupNotificationEmail end if newly_invited and send_notification_email and !Rails.configuration.Users.UserSetupMailText.empty? @@ -297,10 +286,10 @@ SELECT target_uuid, perm_level forget_cached_group_perms - return [repo_perm, vm_login_perm, *group_perms, self].compact + return [vm_login_perm, *group_perms, self].compact end - # delete user signatures, login, repo, and vm perms, and mark as inactive + # delete user signatures, login, and vm perms, and mark as inactive def unsetup if self.uuid == system_user_uuid raise "System root user cannot be deactivated" @@ -308,25 +297,20 @@ SELECT target_uuid, perm_level # delete oid_login_perms for this user # - # note: these permission links are obsolete, they have no effect - # on anything and they are not created for new users. + # note: these permission links are obsolete anyway: they have no + # effect on anything and they are not created for new users. Link.where(tail_uuid: self.email, link_class: 'permission', name: 'can_login').destroy_all - # delete repo_perms for this user - Link.where(tail_uuid: self.uuid, - link_class: 'permission', - name: 'can_manage').destroy_all - - # delete vm_login_perms for this user - Link.where(tail_uuid: self.uuid, - link_class: 'permission', - name: 'can_login').destroy_all - - # delete "All users" group read permissions for this user + # Delete all sharing permissions so (a) the user doesn't + # automatically regain access to anything if re-setup in future, + # (b) the user doesn't appear in "currently shared with" lists + # shown to other users. + # + # Notably this includes the can_read -> "all users" group + # permission. Link.where(tail_uuid: self.uuid, - head_uuid: all_users_group_uuid, link_class: 'permission').destroy_all # delete any signatures by this user @@ -387,7 +371,11 @@ SELECT target_uuid, perm_level end def set_initial_username(requested: false) - if !requested.is_a?(String) || requested.empty? + if new_record? and requested == false and self.username != nil and self.username != "" + requested = self.username + end + + if (!requested.is_a?(String) || requested.empty?) and email email_parts = email.partition("@") local_parts = email_parts.first.partition("+") if email_parts.any?(&:empty?) @@ -398,13 +386,20 @@ SELECT target_uuid, perm_level requested = email_parts.first end end - requested.sub!(/^[^A-Za-z]+/, "") - requested.gsub!(/[^A-Za-z0-9]/, "") - unless requested.empty? + if requested + requested.sub!(/^[^A-Za-z]+/, "") + requested.gsub!(/[^A-Za-z0-9]/, "") + end + unless !requested || requested.empty? self.username = find_usable_username_from(requested) end end + def active_is_not_nil + self.is_active = false if self.is_active.nil? + self.is_admin = false if self.is_admin.nil? + end + # Move this user's (i.e., self's) owned items to new_owner_uuid and # new_user_uuid (for things normally owned directly by the user). # @@ -473,43 +468,26 @@ SELECT target_uuid, perm_level klass.where(column => uuid).update_all(column => new_user.uuid) end - # Need to update repository names to new username - if username - old_repo_name_re = /^#{Regexp.escape(username)}\// - Repository.where(:owner_uuid => uuid).each do |repo| - repo.owner_uuid = new_user.uuid - repo_name_sub = "#{new_user.username}/" - name = repo.name.sub(old_repo_name_re, repo_name_sub) - while (conflict = Repository.where(:name => name).first) != nil - repo_name_sub += "migrated" - name = repo.name.sub(old_repo_name_re, repo_name_sub) - end - repo.name = name - repo.save! - end - end - # References to the merged user's "home project" are updated to # point to new_owner_uuid. ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |klass| next if [ApiClientAuthorization, AuthorizedKey, Link, - Log, - Repository].include?(klass) + Log].include?(klass) next if !klass.columns.collect(&:name).include?('owner_uuid') klass.where(owner_uuid: uuid).update_all(owner_uuid: new_owner_uuid) end if redirect_to_new_user - update_attributes!(redirect_to_user_uuid: new_user.uuid, username: nil) + update!(redirect_to_user_uuid: new_user.uuid, username: nil) end skip_check_permissions_against_full_refresh do - update_permissions self.uuid, self.uuid, CAN_MANAGE_PERM - update_permissions new_user.uuid, new_user.uuid, CAN_MANAGE_PERM - update_permissions new_user.owner_uuid, new_user.uuid, CAN_MANAGE_PERM + update_permissions self.uuid, self.uuid, CAN_MANAGE_PERM, nil, true + update_permissions new_user.uuid, new_user.uuid, CAN_MANAGE_PERM, nil, true + update_permissions new_user.owner_uuid, new_user.uuid, CAN_MANAGE_PERM, nil, true end - update_permissions self.owner_uuid, self.uuid, CAN_MANAGE_PERM + update_permissions self.owner_uuid, self.uuid, CAN_MANAGE_PERM, nil, true end end @@ -597,6 +575,151 @@ SELECT target_uuid, perm_level primary_user end + def self.update_remote_user remote_user + remote_user = remote_user.symbolize_keys + remote_user_prefix = remote_user[:uuid][0..4] + + # interaction between is_invited and is_active + # + # either can flag can be nil, true or false + # + # in all cases, we create the user if they don't exist. + # + # invited nil, active nil: don't call setup or unsetup. + # + # invited nil, active false: call unsetup + # + # invited nil, active true: call setup and activate them. + # + # + # invited false, active nil: call unsetup + # + # invited false, active false: call unsetup + # + # invited false, active true: call unsetup + # + # + # invited true, active nil: call setup but don't change is_active + # + # invited true, active false: call setup but don't change is_active + # + # invited true, active true: call setup and activate them. + + should_setup = (remote_user_prefix == Rails.configuration.Login.LoginCluster or + Rails.configuration.Users.AutoSetupNewUsers or + Rails.configuration.Users.NewUsersAreActive or + Rails.configuration.RemoteClusters[remote_user_prefix].andand["ActivateUsers"]) + + should_activate = (remote_user_prefix == Rails.configuration.Login.LoginCluster or + Rails.configuration.Users.NewUsersAreActive or + Rails.configuration.RemoteClusters[remote_user_prefix].andand["ActivateUsers"]) + + remote_should_be_unsetup = (remote_user[:is_invited] == nil && remote_user[:is_active] == false) || + (remote_user[:is_invited] == false) + + remote_should_be_setup = should_setup && ( + (remote_user[:is_invited] == nil && remote_user[:is_active] == true) || + (remote_user[:is_invited] == false && remote_user[:is_active] == true) || + (remote_user[:is_invited] == true)) + + remote_should_be_active = should_activate && remote_user[:is_invited] != false && remote_user[:is_active] == true + + # Make sure blank username is nil + remote_user[:username] = nil if remote_user[:username] == "" + + begin + user = User.create_with(email: remote_user[:email], + username: remote_user[:username], + first_name: remote_user[:first_name], + last_name: remote_user[:last_name], + is_active: remote_should_be_active, + ).find_or_create_by(uuid: remote_user[:uuid]) + rescue ActiveRecord::RecordNotUnique + retry + end + + user.with_lock do + needupdate = {} + [:email, :username, :first_name, :last_name, :prefs].each do |k| + v = remote_user[k] + if !v.nil? && user.send(k) != v + needupdate[k] = v + end + end + + user.email = needupdate[:email] if needupdate[:email] + + loginCluster = Rails.configuration.Login.LoginCluster + if user.username.nil? || user.username == "" + # Don't have a username yet, try to set one + initial_username = user.set_initial_username(requested: remote_user[:username]) + needupdate[:username] = initial_username if !initial_username.nil? + elsif remote_user_prefix != loginCluster + # Upstream is not login cluster, don't try to change the + # username once set. + needupdate.delete :username + end + + if needupdate.length > 0 + begin + user.update!(needupdate) + rescue ActiveRecord::RecordInvalid + if remote_user_prefix == loginCluster && !needupdate[:username].nil? + local_user = User.find_by_username(needupdate[:username]) + # The username of this record conflicts with an existing, + # different user record. This can happen because the + # username changed upstream on the login cluster, or + # because we're federated with another cluster with a user + # by the same username. The login cluster is the source + # of truth, so change the username on the conflicting + # record and retry the update operation. + if local_user.uuid != user.uuid + new_username = "#{needupdate[:username]}#{rand(99999999)}" + Rails.logger.warn("cached username '#{needupdate[:username]}' collision with user '#{local_user.uuid}' - renaming to '#{new_username}' before retrying") + local_user.update!({username: new_username}) + retry + end + end + raise # Not the issue we're handling above + end + elsif user.new_record? + begin + user.save! + rescue => e + Rails.logger.debug "Error saving user record: #{$!}" + Rails.logger.debug "Backtrace:\n\t#{e.backtrace.join("\n\t")}" + raise + end + end + + if remote_should_be_unsetup + # Remote user is not "invited" or "active" state on their home + # cluster, so they should be unsetup, which also makes them + # inactive. + user.unsetup + else + if !user.is_invited && remote_should_be_setup + user.setup + end + + if !user.is_active && remote_should_be_active + # remote user is active and invited, we need to activate them + user.update!(is_active: true) + end + + if remote_user_prefix == Rails.configuration.Login.LoginCluster and + user.is_active and + !remote_user[:is_admin].nil? and + user.is_admin != remote_user[:is_admin] + # Remote cluster controls our user database, including the + # admin flag. + user.update!(is_admin: remote_user[:is_admin]) + end + end + end + user + end + protected def self.attributes_required_columns @@ -734,24 +857,8 @@ SELECT target_uuid, perm_level merged end - def create_user_repo_link(repo_name) - # repo_name is optional - if not repo_name - logger.warn ("Repository name not given for #{self.uuid}.") - return - end - - 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", - name: "can_manage").first_or_create! - logger.info { "repo permission: " + repo_perm[:uuid] } - return repo_perm - end - # create login permission for the given vm_uuid, if it does not already exist - def create_vm_login_permission_link(vm_uuid, repo_name) + def create_vm_login_permission_link(vm_uuid, username) # vm uuid is optional return if vm_uuid == "" @@ -769,11 +876,11 @@ SELECT target_uuid, perm_level login_perm = Link. where(login_attrs). - select { |link| link.properties["username"] == repo_name }. + select { |link| link.properties["username"] == username }. first login_perm ||= Link. - create(login_attrs.merge(properties: {"username" => repo_name})) + create(login_attrs.merge(properties: {"username" => username})) logger.info { "login permission: " + login_perm[:uuid] } login_perm @@ -815,8 +922,9 @@ SELECT target_uuid, perm_level # Send admin notifications def send_admin_notifications - AdminNotifier.new_user(self).deliver_now - if not self.is_active then + if self.is_invited then + AdminNotifier.new_user(self).deliver_now + else AdminNotifier.new_inactive_user(self).deliver_now end end @@ -845,22 +953,6 @@ SELECT target_uuid, perm_level end end - def verify_repositories_empty - unless repositories.first.nil? - errors.add(:username, "can't be unset when the user owns repositories") - throw(:abort) - end - end - - def sync_repository_names - old_name_re = /^#{Regexp.escape(username_before_last_save)}\// - name_sub = "#{username}/" - repositories.find_each do |repo| - repo.name = repo.name.sub(old_name_re, name_sub) - repo.save! - end - end - def identity_url_nil_if_empty if identity_url == "" self.identity_url = nil