19501: Remove all sharing permissions during unsetup.
[arvados.git] / services / api / app / models / user.rb
index f8cc78c1d7f835fa5b762067d3e0f2cf8b9bad22..bbdd9c2843d4d810439e1f9ecafce1b0835b02ae 100644 (file)
@@ -21,12 +21,14 @@ class User < ArvadosModel
             uniqueness: true,
             allow_nil: true)
   validate :must_unsetup_to_deactivate
+  validate :identity_url_nil_if_empty
   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?
   }
-  before_update :setup_on_activate
+  after_update :setup_on_activate
 
   before_create :check_auto_admin
   before_create :set_initial_username, :if => Proc.new {
@@ -71,6 +73,8 @@ class User < ArvadosModel
     t.add :is_invited
     t.add :prefs
     t.add :writable_by
+    t.add :can_write
+    t.add :can_manage
   end
 
   ALL_PERMISSIONS = {read: true, write: true, manage: true}
@@ -86,6 +90,7 @@ class User < ArvadosModel
   VAL_FOR_PERM =
     {:read => 1,
      :write => 2,
+     :unfreeze => 3,
      :manage => 3}
 
 
@@ -108,7 +113,6 @@ class User < ArvadosModel
   end
 
   def can?(actions)
-    return true if is_admin
     actions.each do |action, target|
       unless target.nil?
         if target.respond_to? :uuid
@@ -120,11 +124,19 @@ class User < ArvadosModel
       end
       next if target_uuid == self.uuid
 
+      if action == :write && target && !target.new_record? &&
+         target.respond_to?(:frozen_by_uuid) &&
+         target.frozen_by_uuid_was
+        # Just an optimization to skip the PERMISSION_VIEW and
+        # FrozenGroup queries below
+        return false
+      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.
+      if !is_admin && !ActiveRecord::Base.connection.
         exec_query(%{
 SELECT 1 FROM #{PERMISSION_VIEW}
   WHERE user_uuid in (#{user_uuids_subquery}) and
@@ -140,6 +152,23 @@ SELECT 1 FROM #{PERMISSION_VIEW}
                   ).any?
         return false
       end
+
+      if action == :write
+        if FrozenGroup.where(uuid: [target_uuid, target_owner_uuid]).any?
+          # self or parent is frozen
+          return false
+        end
+      elsif action == :unfreeze
+        # "unfreeze" permission means "can write, but only if
+        # explicitly un-freezing at the same time" (see
+        # ArvadosModel#ensure_owner_uuid_is_permitted). If the
+        # permission query above passed the permission level of
+        # :unfreeze (which is the same as :manage), and the parent
+        # isn't also frozen, then un-freeze is allowed.
+        if FrozenGroup.where(uuid: target_owner_uuid).any?
+          return false
+        end
+      end
     end
     true
   end
@@ -161,6 +190,10 @@ SELECT 1 FROM #{PERMISSION_VIEW}
     MaterializedPermission.where("user_uuid = ? and target_uuid != ?", uuid, uuid).delete_all
   end
 
+  def forget_cached_group_perms
+    @group_perms = nil
+  end
+
   def remove_self_from_permissions
     MaterializedPermission.where("target_uuid = ?", uuid).delete_all
     check_permissions_against_full_refresh
@@ -191,25 +224,35 @@ SELECT user_uuid, target_uuid, perm_level
   # and perm_hash[:write] are true if this user can read and write
   # objects owned by group_uuid.
   def group_permissions(level=1)
-    group_perms = {}
-
-    user_uuids_subquery = USER_UUIDS_SUBQUERY_TEMPLATE % {user: "$1", perm_level: "$2"}
+    @group_perms ||= {}
+    if @group_perms.empty?
+      user_uuids_subquery = USER_UUIDS_SUBQUERY_TEMPLATE % {user: "$1", perm_level: 1}
 
-    ActiveRecord::Base.connection.
-      exec_query(%{
+      ActiveRecord::Base.connection.
+        exec_query(%{
 SELECT target_uuid, perm_level
   FROM #{PERMISSION_VIEW}
-  WHERE user_uuid in (#{user_uuids_subquery}) and perm_level >= $2
+  WHERE user_uuid in (#{user_uuids_subquery}) and perm_level >= 1
 },
-                  # "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],
-                   [nil, level]]).
-      rows.each do |group_uuid, max_p_val|
-      group_perms[group_uuid] = PERMS_FOR_VAL[max_p_val.to_i]
+                   # "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]]).
+        rows.each do |group_uuid, max_p_val|
+        @group_perms[group_uuid] = PERMS_FOR_VAL[max_p_val.to_i]
+      end
+    end
+
+    case level
+    when 1
+      @group_perms
+    when 2
+      @group_perms.select {|k,v| v[:write] }
+    when 3
+      @group_perms.select {|k,v| v[:manage] }
+    else
+      raise "level must be 1, 2 or 3"
     end
-    group_perms
   end
 
   # create links
@@ -219,15 +262,27 @@ SELECT target_uuid, perm_level
                               link_class: 'permission',
                               name: 'can_read').empty?
 
-    group_perm = create_user_group_link
-    repo_perm = create_user_repo_link repo_name
+    # 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
     end
 
-    vm_login_perm = create_vm_login_permission_link(vm_uuid, username) if vm_uuid
+    vm_login_perm = if vm_uuid && username
+                      create_vm_login_permission_link(vm_uuid, username)
+                    end
 
+    # Send welcome email
     if send_notification_email.nil?
       send_notification_email = Rails.configuration.Mail.SendUserSetupNotificationEmail
     end
@@ -240,38 +295,44 @@ SELECT target_uuid, perm_level
       end
     end
 
-    return [repo_perm, vm_login_perm, group_perm, self].compact
+    forget_cached_group_perms
+
+    return [repo_perm, vm_login_perm, *group_perms, self].compact
   end
 
   # delete user signatures, login, repo, and vm perms, and mark as inactive
   def unsetup
+    if self.uuid == system_user_uuid
+      raise "System root user cannot be deactivated"
+    end
+
     # 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
+               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',
-                     name: 'can_read').destroy_all
+               link_class: 'permission').destroy_all
 
     # delete any signatures by this user
     Link.where(link_class: 'signature',
-                     tail_uuid: self.uuid).destroy_all
+               tail_uuid: self.uuid).destroy_all
+
+    # delete tokens for this user
+    ApiClientAuthorization.where(user_id: self.id).destroy_all
+    # delete ssh keys for this user
+    AuthorizedKey.where(owner_uuid: self.uuid).destroy_all
+    AuthorizedKey.where(authorized_user_uuid: self.uuid).destroy_all
 
     # delete user preferences (including profile)
     self.prefs = {}
@@ -279,9 +340,15 @@ SELECT target_uuid, perm_level
     # mark the user as inactive
     self.is_admin = false  # can't be admin and inactive
     self.is_active = false
+    forget_cached_group_perms
     self.save!
   end
 
+  # Called from ArvadosModel
+  def set_default_owner
+    self.owner_uuid = system_user_uuid
+  end
+
   def must_unsetup_to_deactivate
     if !self.new_record? &&
        self.uuid[0..4] == Rails.configuration.Login.LoginCluster &&
@@ -308,8 +375,7 @@ SELECT target_uuid, perm_level
       #
       if Link.where(tail_uuid: self.uuid,
                     head_uuid: all_users_group_uuid,
-                    link_class: 'permission',
-                    name: 'can_read').any?
+                    link_class: 'permission').any?
         errors.add :is_active, "cannot be set to false directly, use the 'Deactivate' button on Workbench, or the 'unsetup' API call"
       end
     end
@@ -334,37 +400,6 @@ SELECT target_uuid, perm_level
     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)
-    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
-
   # 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).
   #
@@ -559,6 +594,13 @@ update #{PERMISSION_VIEW} set target_uuid=$1 where target_uuid = $2
 
   protected
 
+  def self.attributes_required_columns
+    super.merge(
+                'can_write' => ['owner_uuid', 'uuid'],
+                'can_manage' => ['owner_uuid', 'uuid'],
+                )
+  end
+
   def change_all_uuid_refs(old_uuid:, new_uuid:)
     ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |klass|
       klass.columns.each do |col|
@@ -663,6 +705,13 @@ update #{PERMISSION_VIEW} set target_uuid=$1 where target_uuid = $2
     true
   end
 
+  def prevent_nonadmin_system_root
+    if self.uuid == system_user_uuid and self.is_admin_changed? and !self.is_admin
+      raise "System root user cannot be non-admin"
+    end
+    true
+  end
+
   def search_permissions(start, graph, merged={}, upstream_mask=nil, upstream_path={})
     nextpaths = graph[start]
     return merged if !nextpaths
@@ -725,16 +774,26 @@ update #{PERMISSION_VIEW} set target_uuid=$1 where target_uuid = $2
     login_perm
   end
 
-  # add the user to the 'All users' group
-  def create_user_group_link
-    return (Link.where(tail_uuid: self.uuid,
+  def add_to_all_users_group
+    resp = [Link.where(tail_uuid: self.uuid,
                        head_uuid: all_users_group_uuid,
                        link_class: 'permission',
-                       name: 'can_read').first or
+                       name: 'can_write').first ||
             Link.create(tail_uuid: self.uuid,
                         head_uuid: all_users_group_uuid,
                         link_class: 'permission',
-                        name: 'can_read'))
+                        name: 'can_write')]
+    if Rails.configuration.Users.ActivatedUsersAreVisibleToOthers
+      resp += [Link.where(tail_uuid: all_users_group_uuid,
+                          head_uuid: self.uuid,
+                          link_class: 'permission',
+                          name: 'can_read').first ||
+               Link.create(tail_uuid: all_users_group_uuid,
+                           head_uuid: self.uuid,
+                           link_class: 'permission',
+                           name: 'can_read')]
+    end
+    return resp
   end
 
   # Give the special "System group" permission to manage this user and
@@ -769,17 +828,6 @@ update #{PERMISSION_VIEW} set target_uuid=$1 where target_uuid = $2
   # Automatically setup new user during creation
   def auto_setup_new_user
     setup
-    if username
-      create_vm_login_permission_link(Rails.configuration.Users.AutoSetupNewUsersWithVmUUID,
-                                      username)
-      repo_name = "#{username}/#{username}"
-      if Rails.configuration.Users.AutoSetupNewUsersWithRepository and
-          Repository.where(name: repo_name).first.nil?
-        repo = Repository.create!(name: repo_name, owner_uuid: uuid)
-        Link.create!(tail_uuid: uuid, head_uuid: repo.uuid,
-                     link_class: "permission", name: "can_manage")
-      end
-    end
   end
 
   # Send notification if the user saved profile for the first time
@@ -807,4 +855,10 @@ update #{PERMISSION_VIEW} set target_uuid=$1 where target_uuid = $2
       repo.save!
     end
   end
+
+  def identity_url_nil_if_empty
+    if identity_url == ""
+      self.identity_url = nil
+    end
+  end
 end