X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/b89fda1904ec63d637c43831df66b334f6a78377..9af03429f40bc8c9fcf8f680855c5c5c1fc4b2ad:/services/api/app/models/user.rb diff --git a/services/api/app/models/user.rb b/services/api/app/models/user.rb index bca1eef726..8ed97e6b14 100644 --- a/services/api/app/models/user.rb +++ b/services/api/app/models/user.rb @@ -1,4 +1,9 @@ +# Copyright (C) The Arvados Authors. All rights reserved. +# +# SPDX-License-Identifier: AGPL-3.0 + require 'can_be_an_owner' +require 'refresh_permission_view' class User < ArvadosModel include HasUuid @@ -25,7 +30,9 @@ class User < ArvadosModel before_create :set_initial_username, :if => Proc.new { |user| user.username.nil? and user.email } + after_create :setup_on_activate after_create :add_system_group_permission_link + after_create :invalidate_permissions_cache after_create :auto_setup_new_user, :if => Proc.new { |user| Rails.configuration.auto_setup_new_users and (user.uuid != system_user_uuid) and @@ -42,6 +49,8 @@ class User < ArvadosModel 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 @@ -132,27 +141,24 @@ class User < ArvadosModel true end - def self.invalidate_permissions_cache(timestamp=nil) - if Rails.configuration.async_permissions_update - timestamp = DbCurrentTime::db_current_time.to_i if timestamp.nil? - connection.execute "NOTIFY invalidate_permissions_cache, '#{timestamp}'" - else - Rails.cache.delete_matched(/^groups_for_user_/) - end + def self.invalidate_permissions_cache(async=false) + refresh_permission_view(async) + end + + def invalidate_permissions_cache + User.invalidate_permissions_cache end # Return a hash of {user_uuid: group_perms} def self.all_group_permissions - install_view('permission') all_perms = {} ActiveRecord::Base.connection. - exec_query('SELECT user_uuid, target_owner_uuid, max(perm_level) - FROM permission_view - WHERE target_owner_uuid IS NOT NULL - GROUP BY user_uuid, target_owner_uuid', + exec_query("SELECT user_uuid, target_owner_uuid, perm_level, trashed + FROM #{PERMISSION_VIEW} + WHERE target_owner_uuid IS NOT NULL", # "name" arg is a query label that appears in logs: "all_group_permissions", - ).rows.each do |user_uuid, group_uuid, max_p_val| + ).rows.each do |user_uuid, group_uuid, max_p_val, trashed| all_perms[user_uuid] ||= {} all_perms[user_uuid][group_uuid] = PERMS_FOR_VAL[max_p_val.to_i] end @@ -162,45 +168,23 @@ class User < ArvadosModel # Return a hash of {group_uuid: perm_hash} where perm_hash[:read] # and perm_hash[:write] are true if this user can read and write # objects owned by group_uuid. - def calculate_group_permissions - self.class.install_view('permission') - - group_perms = {} + def group_permissions + group_perms = {self.uuid => {:read => true, :write => true, :manage => true}} ActiveRecord::Base.connection. - exec_query('SELECT target_owner_uuid, max(perm_level) - FROM permission_view + exec_query("SELECT target_owner_uuid, perm_level, trashed + FROM #{PERMISSION_VIEW} WHERE user_uuid = $1 - AND target_owner_uuid IS NOT NULL - GROUP BY target_owner_uuid', + AND target_owner_uuid IS NOT NULL", # "name" arg is a query label that appears in logs: "group_permissions for #{uuid}", # "binds" arg is an array of [col_id, value] for '$1' vars: [[nil, uuid]], - ).rows.each do |group_uuid, max_p_val| + ).rows.each do |group_uuid, max_p_val, trashed| group_perms[group_uuid] = PERMS_FOR_VAL[max_p_val.to_i] end - Rails.cache.write "groups_for_user_#{self.uuid}", group_perms group_perms end - # Return a hash of {group_uuid: perm_hash} where perm_hash[:read] - # and perm_hash[:write] are true if this user can read and write - # objects owned by group_uuid. - def group_permissions - r = Rails.cache.read "groups_for_user_#{self.uuid}" - if r.nil? - if Rails.configuration.async_permissions_update - while r.nil? - sleep(0.1) - r = Rails.cache.read "groups_for_user_#{self.uuid}" - end - else - r = calculate_group_permissions - end - end - r - end - # create links def setup(openid_prefix:, repo_name: nil, vm_uuid: nil) oid_login_perm = create_oid_login_perm openid_prefix @@ -268,14 +252,100 @@ 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) + change_all_uuid_refs(old_uuid: old_uuid, new_uuid: new_uuid) + end + end + + # Move this user's (i.e., self's) owned items into new_owner_uuid. + # Also redirect future uses of this account to + # redirect_to_user_uuid, i.e., when a caller authenticates to this + # account in the future, the account redirect_to_user_uuid account + # will be used instead. + # + # current_user must have admin privileges, i.e., the caller is + # responsible for checking permission to do this. + 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 + raise "cannot merge an already merged user" if self.redirect_to_user_uuid + + new_user = User.where(uuid: redirect_to_user_uuid).first + raise "user does not exist" if !new_user + raise "cannot merge to an already merged user" if new_user.redirect_to_user_uuid + + # Existing API tokens are updated to authenticate to the new + # user. + ApiClientAuthorization. + where(user_id: id). + update_all(user_id: new_user.id) + + # References to the old user UUID in the context of a user ID + # (rather than a "home project" in the project hierarchy) are + # updated to point to the new user. + [ + [AuthorizedKey, :owner_uuid], + [AuthorizedKey, :authorized_user_uuid], + [Repository, :owner_uuid], + [Link, :owner_uuid], + [Link, :tail_uuid], + [Link, :head_uuid], + ].each do |klass, column| + klass.where(column => uuid).update_all(column => new_user.uuid) + 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) + next if !klass.columns.collect(&:name).include?('owner_uuid') + klass.where(owner_uuid: uuid).update_all(owner_uuid: new_owner_uuid) + end + + update_attributes!(redirect_to_user_uuid: new_user.uuid) + invalidate_permissions_cache + 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 @@ -286,7 +356,8 @@ class User < ArvadosModel 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 @@ -316,7 +387,7 @@ class User < ArvadosModel end 0.upto(6).each do |suffix_len| pattern = "%s%s" % [quoted_name, "_" * suffix_len] - self.class. + self.class.unscoped. where("username like '#{pattern}'"). select(:username). order('username asc'). @@ -339,7 +410,7 @@ class User < ArvadosModel end if self.is_active_changed? if self.is_active != self.is_active_was - logger.warn "User #{current_user.uuid} tried to change is_active from #{self.is_admin_was} to #{self.is_admin} for #{self.uuid}" + logger.warn "User #{current_user.uuid} tried to change is_active from #{self.is_active_was} to #{self.is_active} for #{self.uuid}" self.is_active = self.is_active_was end end @@ -388,7 +459,7 @@ class User < ArvadosModel if !oid_login_perms.any? # create openid login permission - oid_login_perm = Link.create(link_class: 'permission', + oid_login_perm = Link.create!(link_class: 'permission', name: 'can_login', tail_uuid: self.email, head_uuid: self.uuid,