Merge branch '15521-keepstore-logging'
[arvados.git] / services / api / app / models / user.rb
index fc5ae0a49db5b2fb05036a0587759d92378358cf..50ecc6b65df04adbf36b9b00e5f34cf985ad26b5 100644 (file)
@@ -10,6 +10,7 @@ class User < ArvadosModel
   include KindAndEtag
   include CommonApiTemplate
   include CanBeAnOwner
+  extend CurrentApiClient
 
   serialize :prefs, Hash
   has_many :api_client_authorizations
@@ -271,45 +272,87 @@ class User < ArvadosModel
     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.
+  # 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).
+  #
+  # If redirect_auth is true, also reassign auth tokens and ssh keys,
+  # and redirect 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:)
+  def merge(new_owner_uuid:, new_user_uuid:, redirect_to_new_user:)
     raise PermissionDeniedError if !current_user.andand.is_admin
-    raise "not implemented" if !redirect_to_user_uuid
+    raise "Missing new_owner_uuid" if !new_owner_uuid
+    raise "Missing new_user_uuid" if !new_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
+      new_user = User.where(uuid: new_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)
+      # 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'
+      # user account that takes over the 'self' account.
+      #
+      # If 'self' is a local user, it is okay to transfer
+      # authorizations, even if the 'new' user is a remote account,
+      # because the remote site does not gain the ability to access an
+      # account it could not before.
+
+      if redirect_to_new_user and self.uuid[0..4] == Rails.configuration.ClusterID
+        # Existing API tokens and ssh keys are updated to authenticate
+        # to the new user.
+        ApiClientAuthorization.
+          where(user_id: id).
+          update_all(user_id: new_user.id)
+
+        user_updates = [
+          [AuthorizedKey, :owner_uuid],
+          [AuthorizedKey, :authorized_user_uuid],
+          [Link, :owner_uuid],
+          [Link, :tail_uuid],
+          [Link, :head_uuid],
+        ]
+      else
+        # Destroy API tokens and ssh keys associated with the old
+        # user.
+        ApiClientAuthorization.where(user_id: id).destroy_all
+        AuthorizedKey.where(owner_uuid: uuid).destroy_all
+        AuthorizedKey.where(authorized_user_uuid: uuid).destroy_all
+        user_updates = [
+          [Link, :owner_uuid],
+          [Link, :tail_uuid]
+        ]
+      end
 
       # 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|
+      user_updates.each do |klass, column|
         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|
@@ -322,11 +365,97 @@ class User < ArvadosModel
         klass.where(owner_uuid: uuid).update_all(owner_uuid: new_owner_uuid)
       end
 
-      update_attributes!(redirect_to_user_uuid: new_user.uuid)
+      if redirect_to_new_user
+        update_attributes!(redirect_to_user_uuid: new_user.uuid, username: nil)
+      end
       invalidate_permissions_cache
     end
   end
 
+  def redirects_to
+    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}")
+      end
+      redirects += 1
+      if redirects > 15
+        raise "Starting from #{self.uuid} redirect_to_user_uuid exceeded maximum number of redirects"
+      end
+    end
+    user
+  end
+
+  def self.register info
+    # login info expected fields, all can be optional but at minimum
+    # must supply either 'identity_url' or 'email'
+    #
+    #   email
+    #   first_name
+    #   last_name
+    #   username
+    #   alternate_emails
+    #   identity_url
+
+    info = info.with_indifferent_access
+
+    primary_user = nil
+
+    # local database
+    identity_url = info['identity_url']
+
+    if identity_url && identity_url.length > 0
+      # Only local users can create sessions, hence uuid_like_pattern
+      # here.
+      user = User.unscoped.where('identity_url = ? and uuid like ?',
+                                 identity_url,
+                                 User.uuid_like_pattern).first
+      primary_user = user.redirects_to if user
+    end
+
+    if !primary_user
+      # identity url is unset or didn't find matching record.
+      emails = [info['email']] + (info['alternate_emails'] || [])
+      emails.select! {|em| !em.nil? && !em.empty?}
+
+      User.unscoped.where('email in (?) and uuid like ?',
+                          emails,
+                          User.uuid_like_pattern).each do |user|
+        if !primary_user
+          primary_user = user.redirects_to
+        elsif primary_user.uuid != user.redirects_to.uuid
+          raise "Ambiguous email address, directs to both #{primary_user.uuid} and #{user.redirects_to.uuid}"
+        end
+      end
+    end
+
+    if !primary_user
+      # New user registration
+      primary_user = User.new(:owner_uuid => system_user_uuid,
+                              :is_admin => false,
+                              :is_active => Rails.configuration.Users.NewUsersAreActive)
+
+      primary_user.set_initial_username(requested: info['username']) if info['username']
+      primary_user.identity_url = info['identity_url'] if identity_url
+    end
+
+    primary_user.email = info['email'] if info['email']
+    primary_user.first_name = info['first_name'] if info['first_name']
+    primary_user.last_name = info['last_name'] if info['last_name']
+
+    if (!primary_user.email or primary_user.email.empty?) and (!primary_user.identity_url or primary_user.identity_url.empty?)
+      raise "Must have supply at least one of 'email' or 'identity_url' to User.register"
+    end
+
+    act_as_system_user do
+      primary_user.save!
+    end
+
+    primary_user
+  end
+
   protected
 
   def change_all_uuid_refs(old_uuid:, new_uuid:)
@@ -345,7 +474,7 @@ class User < ArvadosModel
   end
 
   def permission_to_update
-    if username_changed? || redirect_to_user_uuid_changed?
+    if username_changed? || redirect_to_user_uuid_changed? || email_changed?
       current_user.andand.is_admin
     else
       # users must be able to update themselves (even if they are
@@ -381,7 +510,7 @@ class User < ArvadosModel
     quoted_name = self.class.connection.quote_string(basename)
     next_username = basename
     next_suffix = 1
-    while Rails.configuration.Users.AutoSetupUsernameBlacklist.include?(next_username)
+    while Rails.configuration.Users.AutoSetupUsernameBlacklist[next_username]
       next_suffix += 1
       next_username = "%s%i" % [basename, next_suffix]
     end