Merge branch '8784-dir-listings'
[arvados.git] / services / api / app / models / user.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'can_be_an_owner'
6
7 class User < ArvadosModel
8   include HasUuid
9   include KindAndEtag
10   include CommonApiTemplate
11   include CanBeAnOwner
12
13   serialize :prefs, Hash
14   has_many :api_client_authorizations
15   validates(:username,
16             format: {
17               with: /\A[A-Za-z][A-Za-z0-9]*\z/,
18               message: "must begin with a letter and contain only alphanumerics",
19             },
20             uniqueness: true,
21             allow_nil: true)
22   before_update :prevent_privilege_escalation
23   before_update :prevent_inactive_admin
24   before_update :verify_repositories_empty, :if => Proc.new { |user|
25     user.username.nil? and user.username_changed?
26   }
27   before_update :setup_on_activate
28   before_create :check_auto_admin
29   before_create :set_initial_username, :if => Proc.new { |user|
30     user.username.nil? and user.email
31   }
32   after_create :add_system_group_permission_link
33   after_create :auto_setup_new_user, :if => Proc.new { |user|
34     Rails.configuration.auto_setup_new_users and
35     (user.uuid != system_user_uuid) and
36     (user.uuid != anonymous_user_uuid)
37   }
38   after_create :send_admin_notifications
39   after_update :send_profile_created_notification
40   after_update :sync_repository_names, :if => Proc.new { |user|
41     (user.uuid != system_user_uuid) and
42     user.username_changed? and
43     (not user.username_was.nil?)
44   }
45
46   has_many :authorized_keys, :foreign_key => :authorized_user_uuid, :primary_key => :uuid
47   has_many :repositories, foreign_key: :owner_uuid, primary_key: :uuid
48
49   api_accessible :user, extend: :common do |t|
50     t.add :email
51     t.add :username
52     t.add :full_name
53     t.add :first_name
54     t.add :last_name
55     t.add :identity_url
56     t.add :is_active
57     t.add :is_admin
58     t.add :is_invited
59     t.add :prefs
60     t.add :writable_by
61   end
62
63   ALL_PERMISSIONS = {read: true, write: true, manage: true}
64
65   # Map numeric permission levels (see lib/create_permission_view.sql)
66   # back to read/write/manage flags.
67   PERMS_FOR_VAL =
68     [{},
69      {read: true},
70      {read: true, write: true},
71      {read: true, write: true, manage: true}]
72
73   def full_name
74     "#{first_name} #{last_name}".strip
75   end
76
77   def is_invited
78     !!(self.is_active ||
79        Rails.configuration.new_users_are_active ||
80        self.groups_i_can(:read).select { |x| x.match(/-f+$/) }.first)
81   end
82
83   def groups_i_can(verb)
84     my_groups = self.group_permissions.select { |uuid, mask| mask[verb] }.keys
85     if verb == :read
86       my_groups << anonymous_group_uuid
87     end
88     my_groups
89   end
90
91   def can?(actions)
92     return true if is_admin
93     actions.each do |action, target|
94       unless target.nil?
95         if target.respond_to? :uuid
96           target_uuid = target.uuid
97         else
98           target_uuid = target
99           target = ArvadosModel.find_by_uuid(target_uuid)
100         end
101       end
102       next if target_uuid == self.uuid
103       next if (group_permissions[target_uuid] and
104                group_permissions[target_uuid][action])
105       if target.respond_to? :owner_uuid
106         next if target.owner_uuid == self.uuid
107         next if (group_permissions[target.owner_uuid] and
108                  group_permissions[target.owner_uuid][action])
109       end
110       sufficient_perms = case action
111                          when :manage
112                            ['can_manage']
113                          when :write
114                            ['can_manage', 'can_write']
115                          when :read
116                            ['can_manage', 'can_write', 'can_read']
117                          else
118                            # (Skip this kind of permission opportunity
119                            # if action is an unknown permission type)
120                          end
121       if sufficient_perms
122         # Check permission links with head_uuid pointing directly at
123         # the target object. If target is a Group, this is redundant
124         # and will fail except [a] if permission caching is broken or
125         # [b] during a race condition, where a permission link has
126         # *just* been added.
127         if Link.where(link_class: 'permission',
128                       name: sufficient_perms,
129                       tail_uuid: groups_i_can(action) + [self.uuid],
130                       head_uuid: target_uuid).any?
131           next
132         end
133       end
134       return false
135     end
136     true
137   end
138
139   def self.invalidate_permissions_cache(timestamp=nil)
140     if Rails.configuration.async_permissions_update
141       timestamp = DbCurrentTime::db_current_time.to_i if timestamp.nil?
142       connection.execute "NOTIFY invalidate_permissions_cache, '#{timestamp}'"
143     else
144       Rails.cache.delete_matched(/^groups_for_user_/)
145     end
146   end
147
148   # Return a hash of {user_uuid: group_perms}
149   def self.all_group_permissions
150     install_view('permission')
151     all_perms = {}
152     ActiveRecord::Base.connection.
153       exec_query('SELECT user_uuid, target_owner_uuid, max(perm_level)
154                   FROM permission_view
155                   WHERE target_owner_uuid IS NOT NULL
156                   GROUP BY user_uuid, target_owner_uuid',
157                   # "name" arg is a query label that appears in logs:
158                   "all_group_permissions",
159                   ).rows.each do |user_uuid, group_uuid, max_p_val|
160       all_perms[user_uuid] ||= {}
161       all_perms[user_uuid][group_uuid] = PERMS_FOR_VAL[max_p_val.to_i]
162     end
163     all_perms
164   end
165
166   # Return a hash of {group_uuid: perm_hash} where perm_hash[:read]
167   # and perm_hash[:write] are true if this user can read and write
168   # objects owned by group_uuid.
169   def calculate_group_permissions
170     self.class.install_view('permission')
171
172     group_perms = {}
173     ActiveRecord::Base.connection.
174       exec_query('SELECT target_owner_uuid, max(perm_level)
175                   FROM permission_view
176                   WHERE user_uuid = $1
177                   AND target_owner_uuid IS NOT NULL
178                   GROUP BY target_owner_uuid',
179                   # "name" arg is a query label that appears in logs:
180                   "group_permissions for #{uuid}",
181                   # "binds" arg is an array of [col_id, value] for '$1' vars:
182                   [[nil, uuid]],
183                   ).rows.each do |group_uuid, max_p_val|
184       group_perms[group_uuid] = PERMS_FOR_VAL[max_p_val.to_i]
185     end
186     Rails.cache.write "groups_for_user_#{self.uuid}", group_perms
187     group_perms
188   end
189
190   # Return a hash of {group_uuid: perm_hash} where perm_hash[:read]
191   # and perm_hash[:write] are true if this user can read and write
192   # objects owned by group_uuid.
193   def group_permissions
194     r = Rails.cache.read "groups_for_user_#{self.uuid}"
195     if r.nil?
196       if Rails.configuration.async_permissions_update
197         while r.nil?
198           sleep(0.1)
199           r = Rails.cache.read "groups_for_user_#{self.uuid}"
200         end
201       else
202         r = calculate_group_permissions
203       end
204     end
205     r
206   end
207
208   # create links
209   def setup(openid_prefix:, repo_name: nil, vm_uuid: nil)
210     oid_login_perm = create_oid_login_perm openid_prefix
211     repo_perm = create_user_repo_link repo_name
212     vm_login_perm = create_vm_login_permission_link(vm_uuid, username) if vm_uuid
213     group_perm = create_user_group_link
214
215     return [oid_login_perm, repo_perm, vm_login_perm, group_perm, self].compact
216   end
217
218   # delete user signatures, login, repo, and vm perms, and mark as inactive
219   def unsetup
220     # delete oid_login_perms for this user
221     Link.destroy_all(tail_uuid: self.email,
222                      link_class: 'permission',
223                      name: 'can_login')
224
225     # delete repo_perms for this user
226     Link.destroy_all(tail_uuid: self.uuid,
227                      link_class: 'permission',
228                      name: 'can_manage')
229
230     # delete vm_login_perms for this user
231     Link.destroy_all(tail_uuid: self.uuid,
232                      link_class: 'permission',
233                      name: 'can_login')
234
235     # delete "All users" group read permissions for this user
236     group = Group.where(name: 'All users').select do |g|
237       g[:uuid].match(/-f+$/)
238     end.first
239     Link.destroy_all(tail_uuid: self.uuid,
240                      head_uuid: group[:uuid],
241                      link_class: 'permission',
242                      name: 'can_read')
243
244     # delete any signatures by this user
245     Link.destroy_all(link_class: 'signature',
246                      tail_uuid: self.uuid)
247
248     # delete user preferences (including profile)
249     self.prefs = {}
250
251     # mark the user as inactive
252     self.is_active = false
253     self.save!
254   end
255
256   def set_initial_username(requested: false)
257     if !requested.is_a?(String) || requested.empty?
258       email_parts = email.partition("@")
259       local_parts = email_parts.first.partition("+")
260       if email_parts.any?(&:empty?)
261         return
262       elsif not local_parts.first.empty?
263         requested = local_parts.first
264       else
265         requested = email_parts.first
266       end
267     end
268     requested.sub!(/^[^A-Za-z]+/, "")
269     requested.gsub!(/[^A-Za-z0-9]/, "")
270     unless requested.empty?
271       self.username = find_usable_username_from(requested)
272     end
273   end
274
275   protected
276
277   def ensure_ownership_path_leads_to_user
278     true
279   end
280
281   def permission_to_update
282     if username_changed?
283       current_user.andand.is_admin
284     else
285       # users must be able to update themselves (even if they are
286       # inactive) in order to create sessions
287       self == current_user or super
288     end
289   end
290
291   def permission_to_create
292     current_user.andand.is_admin or
293       (self == current_user and
294        self.is_active == Rails.configuration.new_users_are_active)
295   end
296
297   def check_auto_admin
298     return if self.uuid.end_with?('anonymouspublic')
299     if (User.where("email = ?",self.email).where(:is_admin => true).count == 0 and
300         Rails.configuration.auto_admin_user and self.email == Rails.configuration.auto_admin_user) or
301        (User.where("uuid not like '%-000000000000000'").where(:is_admin => true).count == 0 and
302         Rails.configuration.auto_admin_first_user)
303       self.is_admin = true
304       self.is_active = true
305     end
306   end
307
308   def find_usable_username_from(basename)
309     # If "basename" is a usable username, return that.
310     # Otherwise, find a unique username "basenameN", where N is the
311     # smallest integer greater than 1, and return that.
312     # Return nil if a unique username can't be found after reasonable
313     # searching.
314     quoted_name = self.class.connection.quote_string(basename)
315     next_username = basename
316     next_suffix = 1
317     while Rails.configuration.auto_setup_name_blacklist.include?(next_username)
318       next_suffix += 1
319       next_username = "%s%i" % [basename, next_suffix]
320     end
321     0.upto(6).each do |suffix_len|
322       pattern = "%s%s" % [quoted_name, "_" * suffix_len]
323       self.class.
324           where("username like '#{pattern}'").
325           select(:username).
326           order('username asc').
327           each do |other_user|
328         if other_user.username > next_username
329           break
330         elsif other_user.username == next_username
331           next_suffix += 1
332           next_username = "%s%i" % [basename, next_suffix]
333         end
334       end
335       return next_username if (next_username.size <= pattern.size)
336     end
337     nil
338   end
339
340   def prevent_privilege_escalation
341     if current_user.andand.is_admin
342       return true
343     end
344     if self.is_active_changed?
345       if self.is_active != self.is_active_was
346         logger.warn "User #{current_user.uuid} tried to change is_active from #{self.is_admin_was} to #{self.is_admin} for #{self.uuid}"
347         self.is_active = self.is_active_was
348       end
349     end
350     if self.is_admin_changed?
351       if self.is_admin != self.is_admin_was
352         logger.warn "User #{current_user.uuid} tried to change is_admin from #{self.is_admin_was} to #{self.is_admin} for #{self.uuid}"
353         self.is_admin = self.is_admin_was
354       end
355     end
356     true
357   end
358
359   def prevent_inactive_admin
360     if self.is_admin and not self.is_active
361       # There is no known use case for the strange set of permissions
362       # that would result from this change. It's safest to assume it's
363       # a mistake and disallow it outright.
364       raise "Admin users cannot be inactive"
365     end
366     true
367   end
368
369   def search_permissions(start, graph, merged={}, upstream_mask=nil, upstream_path={})
370     nextpaths = graph[start]
371     return merged if !nextpaths
372     return merged if upstream_path.has_key? start
373     upstream_path[start] = true
374     upstream_mask ||= ALL_PERMISSIONS
375     nextpaths.each do |head, mask|
376       merged[head] ||= {}
377       mask.each do |k,v|
378         merged[head][k] ||= v if upstream_mask[k]
379       end
380       search_permissions(head, graph, merged, upstream_mask.select { |k,v| v && merged[head][k] }, upstream_path)
381     end
382     upstream_path.delete start
383     merged
384   end
385
386   def create_oid_login_perm(openid_prefix)
387     # Check oid_login_perm
388     oid_login_perms = Link.where(tail_uuid: self.email,
389                                  head_uuid: self.uuid,
390                                  link_class: 'permission',
391                                  name: 'can_login')
392
393     if !oid_login_perms.any?
394       # create openid login permission
395       oid_login_perm = Link.create(link_class: 'permission',
396                                    name: 'can_login',
397                                    tail_uuid: self.email,
398                                    head_uuid: self.uuid,
399                                    properties: {
400                                      "identity_url_prefix" => openid_prefix,
401                                    })
402       logger.info { "openid login permission: " + oid_login_perm[:uuid] }
403     else
404       oid_login_perm = oid_login_perms.first
405     end
406
407     return oid_login_perm
408   end
409
410   def create_user_repo_link(repo_name)
411     # repo_name is optional
412     if not repo_name
413       logger.warn ("Repository name not given for #{self.uuid}.")
414       return
415     end
416
417     repo = Repository.where(owner_uuid: uuid, name: repo_name).first_or_create!
418     logger.info { "repo uuid: " + repo[:uuid] }
419     repo_perm = Link.where(tail_uuid: uuid, head_uuid: repo.uuid,
420                            link_class: "permission",
421                            name: "can_manage").first_or_create!
422     logger.info { "repo permission: " + repo_perm[:uuid] }
423     return repo_perm
424   end
425
426   # create login permission for the given vm_uuid, if it does not already exist
427   def create_vm_login_permission_link(vm_uuid, repo_name)
428     # vm uuid is optional
429     return if !vm_uuid
430
431     vm = VirtualMachine.where(uuid: vm_uuid).first
432     if !vm
433       logger.warn "Could not find virtual machine for #{vm_uuid.inspect}"
434       raise "No vm found for #{vm_uuid}"
435     end
436
437     logger.info { "vm uuid: " + vm[:uuid] }
438     login_attrs = {
439       tail_uuid: uuid, head_uuid: vm.uuid,
440       link_class: "permission", name: "can_login",
441     }
442
443     login_perm = Link.
444       where(login_attrs).
445       select { |link| link.properties["username"] == repo_name }.
446       first
447
448     login_perm ||= Link.
449       create(login_attrs.merge(properties: {"username" => repo_name}))
450
451     logger.info { "login permission: " + login_perm[:uuid] }
452     login_perm
453   end
454
455   # add the user to the 'All users' group
456   def create_user_group_link
457     return (Link.where(tail_uuid: self.uuid,
458                        head_uuid: all_users_group[:uuid],
459                        link_class: 'permission',
460                        name: 'can_read').first or
461             Link.create(tail_uuid: self.uuid,
462                         head_uuid: all_users_group[:uuid],
463                         link_class: 'permission',
464                         name: 'can_read'))
465   end
466
467   # Give the special "System group" permission to manage this user and
468   # all of this user's stuff.
469   def add_system_group_permission_link
470     return true if uuid == system_user_uuid
471     act_as_system_user do
472       Link.create(link_class: 'permission',
473                   name: 'can_manage',
474                   tail_uuid: system_group_uuid,
475                   head_uuid: self.uuid)
476     end
477   end
478
479   # Send admin notifications
480   def send_admin_notifications
481     AdminNotifier.new_user(self).deliver_now
482     if not self.is_active then
483       AdminNotifier.new_inactive_user(self).deliver_now
484     end
485   end
486
487   # Automatically setup if is_active flag turns on
488   def setup_on_activate
489     return if [system_user_uuid, anonymous_user_uuid].include?(self.uuid)
490     if is_active && (new_record? || is_active_changed?)
491       setup(openid_prefix: Rails.configuration.default_openid_prefix)
492     end
493   end
494
495   # Automatically setup new user during creation
496   def auto_setup_new_user
497     setup(openid_prefix: Rails.configuration.default_openid_prefix)
498     if username
499       create_vm_login_permission_link(Rails.configuration.auto_setup_new_users_with_vm_uuid,
500                                       username)
501       repo_name = "#{username}/#{username}"
502       if Rails.configuration.auto_setup_new_users_with_repository and
503           Repository.where(name: repo_name).first.nil?
504         repo = Repository.create!(name: repo_name, owner_uuid: uuid)
505         Link.create!(tail_uuid: uuid, head_uuid: repo.uuid,
506                      link_class: "permission", name: "can_manage")
507       end
508     end
509   end
510
511   # Send notification if the user saved profile for the first time
512   def send_profile_created_notification
513     if self.prefs_changed?
514       if self.prefs_was.andand.empty? || !self.prefs_was.andand['profile']
515         profile_notification_address = Rails.configuration.user_profile_notification_address
516         ProfileNotifier.profile_created(self, profile_notification_address).deliver_now if profile_notification_address
517       end
518     end
519   end
520
521   def verify_repositories_empty
522     unless repositories.first.nil?
523       errors.add(:username, "can't be unset when the user owns repositories")
524       false
525     end
526   end
527
528   def sync_repository_names
529     old_name_re = /^#{Regexp.escape(username_was)}\//
530     name_sub = "#{username}/"
531     repositories.find_each do |repo|
532       repo.name = repo.name.sub(old_name_re, name_sub)
533       repo.save!
534     end
535   end
536 end