When auto_admin_first_user is set to true (default false), the first
[arvados.git] / services / api / app / models / user.rb
index 6bba194c24dac1f5460f5ce82fe90e87c202919c..3b201b5babd90ee9244263737e499b8a381b35a6 100644 (file)
@@ -1,14 +1,21 @@
+require 'can_be_an_owner'
+
 class User < ArvadosModel
-  include AssignUuid
+  include HasUuid
   include KindAndEtag
   include CommonApiTemplate
+  include CanBeAnOwner
+
   serialize :prefs, Hash
   has_many :api_client_authorizations
   before_update :prevent_privilege_escalation
   before_update :prevent_inactive_admin
   before_create :check_auto_admin
   after_create :add_system_group_permission_link
-  after_create AdminNotifier
+  after_create :auto_setup_new_user
+  after_create :send_admin_notifications
+  after_update :send_profile_created_notification
+
 
   has_many :authorized_keys, :foreign_key => :authorized_user_uuid, :primary_key => :uuid
 
@@ -22,6 +29,7 @@ class User < ArvadosModel
     t.add :is_admin
     t.add :is_invited
     t.add :prefs
+    t.add :writable_by
   end
 
   ALL_PERMISSIONS = {read: true, write: true, manage: true}
@@ -37,15 +45,23 @@ class User < ArvadosModel
   end
 
   def groups_i_can(verb)
-    self.group_permissions.select { |uuid, mask| mask[verb] }.keys
+    my_groups = self.group_permissions.select { |uuid, mask| mask[verb] }.keys
+    if verb == :read
+      my_groups << anonymous_group_uuid
+    end
+    my_groups
   end
 
   def can?(actions)
     return true if is_admin
     actions.each do |action, target|
-      target_uuid = target
-      if target.respond_to? :uuid
-        target_uuid = target.uuid
+      unless target.nil?
+        if target.respond_to? :uuid
+          target_uuid = target.uuid
+        else
+          target_uuid = target
+          target = ArvadosModel.find_by_uuid(target_uuid)
+        end
       end
       next if target_uuid == self.uuid
       next if (group_permissions[target_uuid] and
@@ -55,6 +71,30 @@ class User < ArvadosModel
         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
+      end
       return false
     end
     true
@@ -67,23 +107,35 @@ 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.
+  #
+  # The permission graph is built by repeatedly enumerating all
+  # permission links reachable from self.uuid, and then calling
+  # search_permissions
   def group_permissions
     Rails.cache.fetch "groups_for_user_#{self.uuid}" do
       permissions_from = {}
       todo = {self.uuid => true}
       done = {}
+      # Build the equivalence class of permissions starting with
+      # self.uuid. On each iteration of this loop, todo contains
+      # the next set of uuids in the permission equivalence class
+      # to evaluate.
       while !todo.empty?
         lookup_uuids = todo.keys
         lookup_uuids.each do |uuid| done[uuid] = true end
         todo = {}
         newgroups = []
+        # include all groups owned by the current set of uuids.
         Group.where('owner_uuid in (?)', lookup_uuids).each do |group|
           newgroups << [group.owner_uuid, group.uuid, 'can_manage']
         end
-        Link.where('tail_uuid in (?) and link_class = ? and (head_uuid like ? or head_uuid like ?)',
-                   lookup_uuids,
+        # add any permission links from the current lookup_uuids to a Group.
+        Link.where('link_class = ? and tail_uuid in (?) and ' \
+                   '(head_uuid like ? or (name = ? and head_uuid like ?))',
                    'permission',
+                   lookup_uuids,
                    Group.uuid_like_pattern,
+                   'can_manage',
                    User.uuid_like_pattern).each do |link|
           newgroups << [link.tail_uuid, link.head_uuid, link.name]
         end
@@ -128,47 +180,35 @@ class User < ArvadosModel
   # delete user signatures, login, repo, and vm perms, and mark as inactive
   def unsetup
     # delete oid_login_perms for this user
-    oid_login_perms = Link.where(tail_uuid: self.email,
-                                 link_class: 'permission',
-                                 name: 'can_login')
-    oid_login_perms.each do |perm|
-      Link.delete perm
-    end
+    Link.destroy_all(tail_uuid: self.email,
+                     link_class: 'permission',
+                     name: 'can_login')
 
     # delete repo_perms for this user
-    repo_perms = Link.where(tail_uuid: self.uuid,
-                            link_class: 'permission',
-                            name: 'can_write')
-    repo_perms.each do |perm|
-      Link.delete perm
-    end
+    Link.destroy_all(tail_uuid: self.uuid,
+                     link_class: 'permission',
+                     name: 'can_manage')
 
     # delete vm_login_perms for this user
-    vm_login_perms = Link.where(tail_uuid: self.uuid,
-                                link_class: 'permission',
-                                name: 'can_login')
-    vm_login_perms.each do |perm|
-      Link.delete perm
-    end
+    Link.destroy_all(tail_uuid: self.uuid,
+                     link_class: 'permission',
+                     name: 'can_login')
 
-    # delete "All users' group read permissions for this user
+    # delete "All users" group read permissions for this user
     group = Group.where(name: 'All users').select do |g|
       g[:uuid].match /-f+$/
     end.first
-    group_perms = Link.where(tail_uuid: self.uuid,
-                             head_uuid: group[:uuid],
-                             link_class: 'permission',
-                             name: 'can_read')
-    group_perms.each do |perm|
-      Link.delete perm
-    end
+    Link.destroy_all(tail_uuid: self.uuid,
+                     head_uuid: group[:uuid],
+                     link_class: 'permission',
+                     name: 'can_read')
 
     # delete any signatures by this user
-    signed_uuids = Link.where(link_class: 'signature',
-                              tail_uuid: self.uuid)
-    signed_uuids.each do |sign|
-      Link.delete sign
-    end
+    Link.destroy_all(link_class: 'signature',
+                     tail_uuid: self.uuid)
+
+    # delete user preferences (including profile)
+    self.prefs = {}
 
     # mark the user as inactive
     self.is_active = false
@@ -194,8 +234,10 @@ class User < ArvadosModel
   end
 
   def check_auto_admin
-    if User.where("uuid not like '%-000000000000000'").where(:is_admin => true).count == 0 and Rails.configuration.auto_admin_user
-      if current_user.email == Rails.configuration.auto_admin_user
+    if not self.uuid.end_with?('anonymouspublic') and
+       User.where("uuid not like '%-000000000000000'").where(:is_admin => true).count == 0
+      if (Rails.configuration.auto_admin_user and self.email == Rails.configuration.auto_admin_user) or
+         Rails.configuration.auto_admin_first_user
         self.is_admin = true
         self.is_active = true
       end
@@ -289,7 +331,7 @@ class User < ArvadosModel
       repo_perms = Link.where(tail_uuid: self.uuid,
                               head_uuid: repo[:uuid],
                               link_class: 'permission',
-                              name: 'can_write')
+                              name: 'can_manage')
       if repo_perms.any?
         logger.warn "User already has repository access " +
             repo_perms.collect { |p| p[:uuid] }.inspect
@@ -304,7 +346,7 @@ class User < ArvadosModel
     repo_perm = Link.create(tail_uuid: self.uuid,
                             head_uuid: repo[:uuid],
                             link_class: 'permission',
-                            name: 'can_write')
+                            name: 'can_manage')
     logger.info { "repo permission: " + repo_perm[:uuid] }
     return repo_perm
   end
@@ -334,21 +376,21 @@ class User < ArvadosModel
 
       perm_exists = false
       login_perms.each do |perm|
-        if perm.properties[:username] == repo_name
-          perm_exists = true
+        if perm.properties['username'] == repo_name
+          perm_exists = perm
           break
         end
       end
 
-      if !perm_exists
+      if perm_exists
+        login_perm = perm_exists
+      else
         login_perm = Link.create(tail_uuid: self.uuid,
                                  head_uuid: vm[:uuid],
                                  link_class: 'permission',
                                  name: 'can_login',
-                                 properties: {username: repo_name})
+                                 properties: {'username' => repo_name})
         logger.info { "login permission: " + login_perm[:uuid] }
-      else
-        login_perm = login_perms.first
       end
 
       return login_perm
@@ -398,4 +440,66 @@ class User < ArvadosModel
                   head_uuid: self.uuid)
     end
   end
+
+  # Send admin notifications
+  def send_admin_notifications
+    AdminNotifier.new_user(self).deliver
+    if not self.is_active then
+      AdminNotifier.new_inactive_user(self).deliver
+    end
+  end
+
+  # Automatically setup new user during creation
+  def auto_setup_new_user
+    return true if !Rails.configuration.auto_setup_new_users
+    return true if !self.email
+    return true if self.uuid == system_user_uuid
+    return true if self.uuid == anonymous_user_uuid
+
+    if Rails.configuration.auto_setup_new_users_with_vm_uuid ||
+       Rails.configuration.auto_setup_new_users_with_repository
+      username = self.email.partition('@')[0] if self.email
+      return true if !username
+
+      blacklisted_usernames = Rails.configuration.auto_setup_name_blacklist
+      if blacklisted_usernames.include?(username)
+        return true
+      elsif !(/^[a-zA-Z][-._a-zA-Z0-9]{0,30}[a-zA-Z0-9]$/.match(username))
+        return true
+      else
+        return true if !(username = derive_unique_username username)
+      end
+    end
+
+    # setup user
+    setup_repo_vm_links(username,
+                        Rails.configuration.auto_setup_new_users_with_vm_uuid,
+                        Rails.configuration.default_openid_prefix)
+  end
+
+  # Find a username that starts with the given string and does not collide
+  # with any existing repository name or VM login name
+  def derive_unique_username username
+    while true
+      if Repository.where(name: username).empty?
+        login_collisions = Link.where(link_class: 'permission',
+                                      name: 'can_login').select do |perm|
+          perm.properties['username'] == username
+        end
+        return username if login_collisions.empty?
+      end
+      username = username + SecureRandom.random_number(10).to_s
+    end
+  end
+
+  # Send notification if the user saved profile for the first time
+  def send_profile_created_notification
+    if self.prefs_changed?
+      if self.prefs_was.andand.empty? || !self.prefs_was.andand['profile']
+        profile_notification_address = Rails.configuration.user_profile_notification_address
+        ProfileNotifier.profile_created(self, profile_notification_address).deliver if profile_notification_address
+      end
+    end
+  end
+
 end