16476: Merge branch 'master' into 16476-upgrade-arvados-jobs-to-buster
[arvados.git] / services / api / app / models / user.rb
index 7a3a854b3a17826117a6ff913ff5f20743f86483..64facaa98e84c2eacfdc6fed38372f2dff22fdde 100644 (file)
@@ -3,7 +3,6 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 require 'can_be_an_owner'
-require 'refresh_permission_view'
 
 class User < ArvadosModel
   include HasUuid
@@ -21,31 +20,38 @@ class User < ArvadosModel
             },
             uniqueness: true,
             allow_nil: true)
+  validate :must_unsetup_to_deactivate
   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_update :setup_on_activate
+
   before_create :check_auto_admin
   before_create :set_initial_username, :if => Proc.new { |user|
     user.username.nil? and user.email
   }
+  after_create :after_ownership_change
   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.Users.AutoSetupNewUsers and
     (user.uuid != system_user_uuid) and
     (user.uuid != anonymous_user_uuid)
   }
   after_create :send_admin_notifications
+
+  before_update :before_ownership_change
+  after_update :after_ownership_change
   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?)
   }
+  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
@@ -76,6 +82,12 @@ class User < ArvadosModel
      {read: true, write: true},
      {read: true, write: true, manage: true}]
 
+  VAL_FOR_PERM =
+    {:read => 1,
+     :write => 2,
+     :manage => 3}
+
+
   def full_name
     "#{first_name} #{last_name}".strip
   end
@@ -87,7 +99,7 @@ class User < ArvadosModel
   end
 
   def groups_i_can(verb)
-    my_groups = self.group_permissions.select { |uuid, mask| mask[verb] }.keys
+    my_groups = self.group_permissions(VAL_FOR_PERM[verb]).keys
     if verb == :read
       my_groups << anonymous_group_uuid
     end
@@ -106,60 +118,68 @@ class User < ArvadosModel
         end
       end
       next if target_uuid == self.uuid
-      next if (group_permissions[target_uuid] and
-               group_permissions[target_uuid][action])
-      if target.respond_to? :owner_uuid
-        next if target.owner_uuid == self.uuid
-        next if (group_permissions[target.owner_uuid] and
-                 group_permissions[target.owner_uuid][action])
-      end
-      sufficient_perms = case action
-                         when :manage
-                           ['can_manage']
-                         when :write
-                           ['can_manage', 'can_write']
-                         when :read
-                           ['can_manage', 'can_write', 'can_read']
-                         else
-                           # (Skip this kind of permission opportunity
-                           # if action is an unknown permission type)
-                         end
-      if sufficient_perms
-        # Check permission links with head_uuid pointing directly at
-        # the target object. If target is a Group, this is redundant
-        # and will fail except [a] if permission caching is broken or
-        # [b] during a race condition, where a permission link has
-        # *just* been added.
-        if Link.where(link_class: 'permission',
-                      name: sufficient_perms,
-                      tail_uuid: groups_i_can(action) + [self.uuid],
-                      head_uuid: target_uuid).any?
-          next
-        end
+
+      target_owner_uuid = target.owner_uuid if target.respond_to? :owner_uuid
+
+      user_uuids_subquery = USER_UUIDS_SUBQUERY_TEMPLATE % {user: "$1", perm_level: "$3"}
+
+      unless ActiveRecord::Base.connection.
+        exec_query(%{
+SELECT 1 FROM #{PERMISSION_VIEW}
+  WHERE user_uuid in (#{user_uuids_subquery}) and
+        ((target_uuid = $2 and perm_level >= $3)
+         or (target_uuid = $4 and perm_level >= $3 and traverse_owned))
+},
+                  # "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]]
+                  ).any?
+        return false
       end
-      return false
     end
     true
   end
 
-  def self.invalidate_permissions_cache(async=false)
-    refresh_permission_view(async)
+  def before_ownership_change
+    if owner_uuid_changed? and !self.owner_uuid_was.nil?
+      MaterializedPermission.where(user_uuid: owner_uuid_was, target_uuid: uuid).delete_all
+      update_permissions self.owner_uuid_was, self.uuid, REVOKE_PERM
+    end
   end
 
-  def invalidate_permissions_cache
-    User.invalidate_permissions_cache
+  def after_ownership_change
+    if owner_uuid_changed?
+      update_permissions self.owner_uuid, self.uuid, CAN_MANAGE_PERM
+    end
+  end
+
+  def clear_permissions
+    MaterializedPermission.where("user_uuid = ? and target_uuid != ?", uuid, uuid).delete_all
+  end
+
+  def remove_self_from_permissions
+    MaterializedPermission.where("target_uuid = ?", uuid).delete_all
+    check_permissions_against_full_refresh
   end
 
   # Return a hash of {user_uuid: group_perms}
+  #
+  # note: this does not account for permissions that a user gains by
+  # having can_manage on another user.
   def self.all_group_permissions
     all_perms = {}
     ActiveRecord::Base.connection.
-      exec_query("SELECT user_uuid, target_owner_uuid, perm_level, trashed
+      exec_query(%{
+SELECT user_uuid, target_uuid, perm_level
                   FROM #{PERMISSION_VIEW}
-                  WHERE target_owner_uuid IS NOT NULL",
+                  WHERE traverse_owned
+},
                   # "name" arg is a query label that appears in logs:
-                  "all_group_permissions",
-                  ).rows.each do |user_uuid, group_uuid, max_p_val, trashed|
+                 "all_group_permissions").
+      rows.each do |user_uuid, group_uuid, max_p_val|
       all_perms[user_uuid] ||= {}
       all_perms[user_uuid][group_uuid] = PERMS_FOR_VAL[max_p_val.to_i]
     end
@@ -169,36 +189,43 @@ 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 group_permissions
-    group_perms = {self.uuid => {:read => true, :write => true, :manage => true}}
+  def group_permissions(level=1)
+    group_perms = {}
+
+    user_uuids_subquery = USER_UUIDS_SUBQUERY_TEMPLATE % {user: "$1", perm_level: "$2"}
+
     ActiveRecord::Base.connection.
-      exec_query("SELECT target_owner_uuid, perm_level, trashed
-                  FROM #{PERMISSION_VIEW}
-                  WHERE user_uuid = $1
-                  AND target_owner_uuid IS NOT NULL",
+      exec_query(%{
+SELECT target_uuid, perm_level
+  FROM #{PERMISSION_VIEW}
+  WHERE user_uuid in (#{user_uuids_subquery}) and perm_level >= $2
+},
                   # "name" arg is a query label that appears in logs:
-                  "group_permissions for #{uuid}",
+                  "User.group_permissions",
                   # "binds" arg is an array of [col_id, value] for '$1' vars:
-                  [[nil, uuid]],
-                ).rows.each do |group_uuid, max_p_val, trashed|
+                  [[nil, uuid],
+                   [nil, level]]).
+      rows.each do |group_uuid, max_p_val|
       group_perms[group_uuid] = PERMS_FOR_VAL[max_p_val.to_i]
     end
     group_perms
   end
 
   # create links
-  def setup(openid_prefix:, repo_name: nil, vm_uuid: nil)
-    oid_login_perm = create_oid_login_perm openid_prefix
+  def setup(repo_name: nil, vm_uuid: nil)
     repo_perm = create_user_repo_link repo_name
     vm_login_perm = create_vm_login_permission_link(vm_uuid, username) if vm_uuid
     group_perm = create_user_group_link
 
-    return [oid_login_perm, repo_perm, vm_login_perm, group_perm, self].compact
+    return [repo_perm, vm_login_perm, group_perm, self].compact
   end
 
   # delete user signatures, login, repo, and vm perms, and mark as inactive
   def unsetup
     # 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.
     Link.where(tail_uuid: self.email,
                      link_class: 'permission',
                      name: 'can_login').destroy_all
@@ -234,6 +261,43 @@ class User < ArvadosModel
     self.save!
   end
 
+  def must_unsetup_to_deactivate
+    if !self.new_record? &&
+       self.uuid[0..4] == Rails.configuration.Login.LoginCluster &&
+       self.uuid[0..4] != Rails.configuration.ClusterID
+      # OK to update our local record to whatever the LoginCluster
+      # reports, because self-activate is not allowed.
+      return
+    elsif self.is_active_changed? &&
+       self.is_active_was &&
+       !self.is_active
+
+      group = Group.where(name: 'All users').select do |g|
+        g[:uuid].match(/-f+$/)
+      end.first
+
+      # When a user is set up, they are added to the "All users"
+      # group.  A user that is part of the "All users" group is
+      # allowed to self-activate.
+      #
+      # It doesn't make sense to deactivate a user (set is_active =
+      # false) without first removing them from the "All users" group,
+      # because they would be able to immediately reactivate
+      # themselves.
+      #
+      # The 'unsetup' method removes the user from the "All users"
+      # group (and also sets is_active = false) so send a message
+      # explaining the correct way to deactivate a user.
+      #
+      if Link.where(tail_uuid: self.uuid,
+                    head_uuid: group[:uuid],
+                    link_class: 'permission',
+                    name: 'can_read').any?
+        errors.add :is_active, "cannot be set to false directly, use the 'Deactivate' button on Workbench, or the 'unsetup' API call"
+      end
+    end
+  end
+
   def set_initial_username(requested: false)
     if !requested.is_a?(String) || requested.empty?
       email_parts = email.partition("@")
@@ -269,6 +333,18 @@ class User < ArvadosModel
       self.uuid = new_uuid
       save!(validate: false)
       change_all_uuid_refs(old_uuid: old_uuid, new_uuid: new_uuid)
+    ActiveRecord::Base.connection.exec_update %{
+update #{PERMISSION_VIEW} set user_uuid=$1 where user_uuid = $2
+},
+                                             'User.update_uuid.update_permissions_user_uuid',
+                                             [[nil, new_uuid],
+                                              [nil, old_uuid]]
+      ActiveRecord::Base.connection.exec_update %{
+update #{PERMISSION_VIEW} set target_uuid=$1 where target_uuid = $2
+},
+                                            'User.update_uuid.update_permissions_target_uuid',
+                                             [[nil, new_uuid],
+                                              [nil, old_uuid]]
     end
   end
 
@@ -294,6 +370,9 @@ class User < ArvadosModel
       raise "user does not exist" if !new_user
       raise "cannot merge to an already merged user" if new_user.redirect_to_user_uuid
 
+      self.clear_permissions
+      new_user.clear_permissions
+
       # If 'self' is a remote user, don't transfer authorizations
       # (i.e. ability to access the account) to the new user, because
       # that gives the remote site the ability to access the 'new'
@@ -368,7 +447,12 @@ class User < ArvadosModel
       if redirect_to_new_user
         update_attributes!(redirect_to_user_uuid: new_user.uuid, username: nil)
       end
-      invalidate_permissions_cache
+      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
+      end
+      update_permissions self.owner_uuid, self.uuid, CAN_MANAGE_PERM
     end
   end
 
@@ -376,10 +460,12 @@ class User < ArvadosModel
     user = self
     redirects = 0
     while (uuid = user.redirect_to_user_uuid)
-      user = User.unscoped.find_by_uuid(uuid)
-      if !user
-        raise Exception.new("user uuid #{user.uuid} redirects to nonexistent uuid #{uuid}")
+      break if uuid.empty?
+      nextuser = User.unscoped.find_by_uuid(uuid)
+      if !nextuser
+        raise Exception.new("user uuid #{user.uuid} redirects to nonexistent uuid '#{uuid}'")
       end
+      user = nextuser
       redirects += 1
       if redirects > 15
         raise "Starting from #{self.uuid} redirect_to_user_uuid exceeded maximum number of redirects"
@@ -435,7 +521,7 @@ class User < ArvadosModel
                               :is_admin => false,
                               :is_active => Rails.configuration.Users.NewUsersAreActive)
 
-      primary_user.set_initial_username(requested: info['username']) if info['username']
+      primary_user.set_initial_username(requested: info['username']) if info['username'] && !info['username'].blank?
       primary_user.identity_url = info['identity_url'] if identity_url
     end
 
@@ -577,30 +663,6 @@ class User < ArvadosModel
     merged
   end
 
-  def create_oid_login_perm(openid_prefix)
-    # Check oid_login_perm
-    oid_login_perms = Link.where(tail_uuid: self.email,
-                                 head_uuid: self.uuid,
-                                 link_class: 'permission',
-                                 name: 'can_login')
-
-    if !oid_login_perms.any?
-      # create openid login permission
-      oid_login_perm = Link.create!(link_class: 'permission',
-                                   name: 'can_login',
-                                   tail_uuid: self.email,
-                                   head_uuid: self.uuid,
-                                   properties: {
-                                     "identity_url_prefix" => openid_prefix,
-                                   })
-      logger.info { "openid login permission: " + oid_login_perm[:uuid] }
-    else
-      oid_login_perm = oid_login_perms.first
-    end
-
-    return oid_login_perm
-  end
-
   def create_user_repo_link(repo_name)
     # repo_name is optional
     if not repo_name
@@ -682,13 +744,13 @@ class User < ArvadosModel
   def setup_on_activate
     return if [system_user_uuid, anonymous_user_uuid].include?(self.uuid)
     if is_active && (new_record? || is_active_changed?)
-      setup(openid_prefix: Rails.configuration.default_openid_prefix)
+      setup
     end
   end
 
   # Automatically setup new user during creation
   def auto_setup_new_user
-    setup(openid_prefix: Rails.configuration.default_openid_prefix)
+    setup
     if username
       create_vm_login_permission_link(Rails.configuration.Users.AutoSetupNewUsersWithVmUUID,
                                       username)