Merge branch '4189-workbench-project-admin-attr-editing-wip'
[arvados.git] / services / api / app / models / user.rb
1 require 'can_be_an_owner'
2
3 class User < ArvadosModel
4   include HasUuid
5   include KindAndEtag
6   include CommonApiTemplate
7   include CanBeAnOwner
8
9   serialize :prefs, Hash
10   has_many :api_client_authorizations
11   before_update :prevent_privilege_escalation
12   before_update :prevent_inactive_admin
13   before_create :check_auto_admin
14   after_create :add_system_group_permission_link
15   after_create :auto_setup_new_user
16   after_create :send_admin_notifications
17   after_update :send_profile_created_notification
18
19
20   has_many :authorized_keys, :foreign_key => :authorized_user_uuid, :primary_key => :uuid
21
22   api_accessible :user, extend: :common do |t|
23     t.add :email
24     t.add :full_name
25     t.add :first_name
26     t.add :last_name
27     t.add :identity_url
28     t.add :is_active
29     t.add :is_admin
30     t.add :is_invited
31     t.add :prefs
32     t.add :writable_by
33   end
34
35   ALL_PERMISSIONS = {read: true, write: true, manage: true}
36
37   def full_name
38     "#{first_name} #{last_name}".strip
39   end
40
41   def is_invited
42     !!(self.is_active ||
43        Rails.configuration.new_users_are_active ||
44        self.groups_i_can(:read).select { |x| x.match /-f+$/ }.first)
45   end
46
47   def groups_i_can(verb)
48     my_groups = self.group_permissions.select { |uuid, mask| mask[verb] }.keys
49     if verb == :read
50       my_groups << anonymous_group_uuid
51     end
52     my_groups
53   end
54
55   def can?(actions)
56     return true if is_admin
57     actions.each do |action, target|
58       unless target.nil?
59         if target.respond_to? :uuid
60           target_uuid = target.uuid
61         else
62           target_uuid = target
63           target = ArvadosModel.find_by_uuid(target_uuid)
64         end
65       end
66       next if target_uuid == self.uuid
67       next if (group_permissions[target_uuid] and
68                group_permissions[target_uuid][action])
69       if target.respond_to? :owner_uuid
70         next if target.owner_uuid == self.uuid
71         next if (group_permissions[target.owner_uuid] and
72                  group_permissions[target.owner_uuid][action])
73       end
74       sufficient_perms = case action
75                          when :manage
76                            ['can_manage']
77                          when :write
78                            ['can_manage', 'can_write']
79                          when :read
80                            ['can_manage', 'can_write', 'can_read']
81                          else
82                            # (Skip this kind of permission opportunity
83                            # if action is an unknown permission type)
84                          end
85       if sufficient_perms
86         # Check permission links with head_uuid pointing directly at
87         # the target object. If target is a Group, this is redundant
88         # and will fail except [a] if permission caching is broken or
89         # [b] during a race condition, where a permission link has
90         # *just* been added.
91         if Link.where(link_class: 'permission',
92                       name: sufficient_perms,
93                       tail_uuid: groups_i_can(action) + [self.uuid],
94                       head_uuid: target_uuid).any?
95           next
96         end
97       end
98       return false
99     end
100     true
101   end
102
103   def self.invalidate_permissions_cache
104     Rails.cache.delete_matched(/^groups_for_user_/)
105   end
106
107   # Return a hash of {group_uuid: perm_hash} where perm_hash[:read]
108   # and perm_hash[:write] are true if this user can read and write
109   # objects owned by group_uuid.
110   #
111   # The permission graph is built by repeatedly enumerating all
112   # permission links reachable from self.uuid, and then calling
113   # search_permissions
114   def group_permissions
115     Rails.cache.fetch "groups_for_user_#{self.uuid}" do
116       permissions_from = {}
117       todo = {self.uuid => true}
118       done = {}
119       # Build the equivalence class of permissions starting with
120       # self.uuid. On each iteration of this loop, todo contains
121       # the next set of uuids in the permission equivalence class
122       # to evaluate.
123       while !todo.empty?
124         lookup_uuids = todo.keys
125         lookup_uuids.each do |uuid| done[uuid] = true end
126         todo = {}
127         newgroups = []
128         # include all groups owned by the current set of uuids.
129         Group.where('owner_uuid in (?)', lookup_uuids).each do |group|
130           newgroups << [group.owner_uuid, group.uuid, 'can_manage']
131         end
132         # add any permission links from the current lookup_uuids to a Group.
133         Link.where('link_class = ? and tail_uuid in (?) and ' \
134                    '(head_uuid like ? or (name = ? and head_uuid like ?))',
135                    'permission',
136                    lookup_uuids,
137                    Group.uuid_like_pattern,
138                    'can_manage',
139                    User.uuid_like_pattern).each do |link|
140           newgroups << [link.tail_uuid, link.head_uuid, link.name]
141         end
142         newgroups.each do |tail_uuid, head_uuid, perm_name|
143           unless done.has_key? head_uuid
144             todo[head_uuid] = true
145           end
146           link_permissions = {}
147           case perm_name
148           when 'can_read'
149             link_permissions = {read:true}
150           when 'can_write'
151             link_permissions = {read:true,write:true}
152           when 'can_manage'
153             link_permissions = ALL_PERMISSIONS
154           end
155           permissions_from[tail_uuid] ||= {}
156           permissions_from[tail_uuid][head_uuid] ||= {}
157           link_permissions.each do |k,v|
158             permissions_from[tail_uuid][head_uuid][k] ||= v
159           end
160         end
161       end
162       search_permissions(self.uuid, permissions_from)
163     end
164   end
165
166   def self.setup(user, openid_prefix, repo_name=nil, vm_uuid=nil)
167     return user.setup_repo_vm_links(repo_name, vm_uuid, openid_prefix)
168   end
169
170   # create links
171   def setup_repo_vm_links(repo_name, vm_uuid, openid_prefix)
172     oid_login_perm = create_oid_login_perm openid_prefix
173     repo_perm = create_user_repo_link repo_name
174     vm_login_perm = create_vm_login_permission_link vm_uuid, repo_name
175     group_perm = create_user_group_link
176
177     return [oid_login_perm, repo_perm, vm_login_perm, group_perm, self].compact
178   end
179
180   # delete user signatures, login, repo, and vm perms, and mark as inactive
181   def unsetup
182     # delete oid_login_perms for this user
183     Link.destroy_all(tail_uuid: self.email,
184                      link_class: 'permission',
185                      name: 'can_login')
186
187     # delete repo_perms for this user
188     Link.destroy_all(tail_uuid: self.uuid,
189                      link_class: 'permission',
190                      name: 'can_manage')
191
192     # delete vm_login_perms for this user
193     Link.destroy_all(tail_uuid: self.uuid,
194                      link_class: 'permission',
195                      name: 'can_login')
196
197     # delete "All users" group read permissions for this user
198     group = Group.where(name: 'All users').select do |g|
199       g[:uuid].match /-f+$/
200     end.first
201     Link.destroy_all(tail_uuid: self.uuid,
202                      head_uuid: group[:uuid],
203                      link_class: 'permission',
204                      name: 'can_read')
205
206     # delete any signatures by this user
207     Link.destroy_all(link_class: 'signature',
208                      tail_uuid: self.uuid)
209
210     # delete user preferences (including profile)
211     self.prefs = {}
212
213     # mark the user as inactive
214     self.is_active = false
215     self.save!
216   end
217
218   protected
219
220   def ensure_ownership_path_leads_to_user
221     true
222   end
223
224   def permission_to_update
225     # users must be able to update themselves (even if they are
226     # inactive) in order to create sessions
227     self == current_user or super
228   end
229
230   def permission_to_create
231     current_user.andand.is_admin or
232       (self == current_user and
233        self.is_active == Rails.configuration.new_users_are_active)
234   end
235
236   def check_auto_admin
237     if User.where("uuid not like '%-000000000000000'").where(:is_admin => true).count == 0 and Rails.configuration.auto_admin_user
238       if self.email == Rails.configuration.auto_admin_user
239         self.is_admin = true
240         self.is_active = true
241       end
242     end
243   end
244
245   def prevent_privilege_escalation
246     if current_user.andand.is_admin
247       return true
248     end
249     if self.is_active_changed?
250       if self.is_active != self.is_active_was
251         logger.warn "User #{current_user.uuid} tried to change is_active from #{self.is_admin_was} to #{self.is_admin} for #{self.uuid}"
252         self.is_active = self.is_active_was
253       end
254     end
255     if self.is_admin_changed?
256       if self.is_admin != self.is_admin_was
257         logger.warn "User #{current_user.uuid} tried to change is_admin from #{self.is_admin_was} to #{self.is_admin} for #{self.uuid}"
258         self.is_admin = self.is_admin_was
259       end
260     end
261     true
262   end
263
264   def prevent_inactive_admin
265     if self.is_admin and not self.is_active
266       # There is no known use case for the strange set of permissions
267       # that would result from this change. It's safest to assume it's
268       # a mistake and disallow it outright.
269       raise "Admin users cannot be inactive"
270     end
271     true
272   end
273
274   def search_permissions(start, graph, merged={}, upstream_mask=nil, upstream_path={})
275     nextpaths = graph[start]
276     return merged if !nextpaths
277     return merged if upstream_path.has_key? start
278     upstream_path[start] = true
279     upstream_mask ||= ALL_PERMISSIONS
280     nextpaths.each do |head, mask|
281       merged[head] ||= {}
282       mask.each do |k,v|
283         merged[head][k] ||= v if upstream_mask[k]
284       end
285       search_permissions(head, graph, merged, upstream_mask.select { |k,v| v && merged[head][k] }, upstream_path)
286     end
287     upstream_path.delete start
288     merged
289   end
290
291   def create_oid_login_perm (openid_prefix)
292     login_perm_props = { "identity_url_prefix" => openid_prefix}
293
294     # Check oid_login_perm
295     oid_login_perms = Link.where(tail_uuid: self.email,
296                                    link_class: 'permission',
297                                    name: 'can_login').where("head_uuid = ?", self.uuid)
298
299     if !oid_login_perms.any?
300       # create openid login permission
301       oid_login_perm = Link.create(link_class: 'permission',
302                                    name: 'can_login',
303                                    tail_uuid: self.email,
304                                    head_uuid: self.uuid,
305                                    properties: login_perm_props
306                                   )
307       logger.info { "openid login permission: " + oid_login_perm[:uuid] }
308     else
309       oid_login_perm = oid_login_perms.first
310     end
311
312     return oid_login_perm
313   end
314
315   def create_user_repo_link(repo_name)
316     # repo_name is optional
317     if not repo_name
318       logger.warn ("Repository name not given for #{self.uuid}.")
319       return
320     end
321
322     # Check for an existing repository with the same name we're about to use.
323     repo = Repository.where(name: repo_name).first
324
325     if repo
326       logger.warn "Repository exists for #{repo_name}: #{repo[:uuid]}."
327
328       # Look for existing repository access for this repo
329       repo_perms = Link.where(tail_uuid: self.uuid,
330                               head_uuid: repo[:uuid],
331                               link_class: 'permission',
332                               name: 'can_manage')
333       if repo_perms.any?
334         logger.warn "User already has repository access " +
335             repo_perms.collect { |p| p[:uuid] }.inspect
336         return repo_perms.first
337       end
338     end
339
340     # create repo, if does not already exist
341     repo ||= Repository.create(name: repo_name)
342     logger.info { "repo uuid: " + repo[:uuid] }
343
344     repo_perm = Link.create(tail_uuid: self.uuid,
345                             head_uuid: repo[:uuid],
346                             link_class: 'permission',
347                             name: 'can_manage')
348     logger.info { "repo permission: " + repo_perm[:uuid] }
349     return repo_perm
350   end
351
352   # create login permission for the given vm_uuid, if it does not already exist
353   def create_vm_login_permission_link(vm_uuid, repo_name)
354     begin
355
356       # vm uuid is optional
357       if vm_uuid
358         vm = VirtualMachine.where(uuid: vm_uuid).first
359
360         if not vm
361           logger.warn "Could not find virtual machine for #{vm_uuid.inspect}"
362           raise "No vm found for #{vm_uuid}"
363         end
364       else
365         return
366       end
367
368       logger.info { "vm uuid: " + vm[:uuid] }
369
370       login_perms = Link.where(tail_uuid: self.uuid,
371                               head_uuid: vm[:uuid],
372                               link_class: 'permission',
373                               name: 'can_login')
374
375       perm_exists = false
376       login_perms.each do |perm|
377         if perm.properties['username'] == repo_name
378           perm_exists = perm
379           break
380         end
381       end
382
383       if perm_exists
384         login_perm = perm_exists
385       else
386         login_perm = Link.create(tail_uuid: self.uuid,
387                                  head_uuid: vm[:uuid],
388                                  link_class: 'permission',
389                                  name: 'can_login',
390                                  properties: {'username' => repo_name})
391         logger.info { "login permission: " + login_perm[:uuid] }
392       end
393
394       return login_perm
395     end
396   end
397
398   # add the user to the 'All users' group
399   def create_user_group_link
400     # Look up the "All users" group (we expect uuid *-*-fffffffffffffff).
401     group = Group.where(name: 'All users').select do |g|
402       g[:uuid].match /-f+$/
403     end.first
404
405     if not group
406       logger.warn "No 'All users' group with uuid '*-*-fffffffffffffff'."
407       raise "No 'All users' group with uuid '*-*-fffffffffffffff' is found"
408     else
409       logger.info { "\"All users\" group uuid: " + group[:uuid] }
410
411       group_perms = Link.where(tail_uuid: self.uuid,
412                               head_uuid: group[:uuid],
413                               link_class: 'permission',
414                               name: 'can_read')
415
416       if !group_perms.any?
417         group_perm = Link.create(tail_uuid: self.uuid,
418                                  head_uuid: group[:uuid],
419                                  link_class: 'permission',
420                                  name: 'can_read')
421         logger.info { "group permission: " + group_perm[:uuid] }
422       else
423         group_perm = group_perms.first
424       end
425
426       return group_perm
427     end
428   end
429
430   # Give the special "System group" permission to manage this user and
431   # all of this user's stuff.
432   #
433   def add_system_group_permission_link
434     act_as_system_user do
435       Link.create(link_class: 'permission',
436                   name: 'can_manage',
437                   tail_uuid: system_group_uuid,
438                   head_uuid: self.uuid)
439     end
440   end
441
442   # Send admin notifications
443   def send_admin_notifications
444     AdminNotifier.new_user(self).deliver
445     if not self.is_active then
446       AdminNotifier.new_inactive_user(self).deliver
447     end
448   end
449
450   # Automatically setup new user during creation
451   def auto_setup_new_user
452     return true if !Rails.configuration.auto_setup_new_users
453     return true if !self.email
454
455     if Rails.configuration.auto_setup_new_users_with_vm_uuid ||
456        Rails.configuration.auto_setup_new_users_with_repository
457       username = self.email.partition('@')[0] if self.email
458       return true if !username
459
460       blacklisted_usernames = Rails.configuration.auto_setup_name_blacklist
461       if blacklisted_usernames.include?(username)
462         return true
463       elsif !(/^[a-zA-Z][-._a-zA-Z0-9]{0,30}[a-zA-Z0-9]$/.match(username))
464         return true
465       else
466         return true if !(username = derive_unique_username username)
467       end
468     end
469
470     # setup user
471     setup_repo_vm_links(username,
472                         Rails.configuration.auto_setup_new_users_with_vm_uuid,
473                         Rails.configuration.default_openid_prefix)
474   end
475
476   # Find a username that starts with the given string and does not collide
477   # with any existing repository name or VM login name
478   def derive_unique_username username
479     while true
480       if Repository.where(name: username).empty?
481         login_collisions = Link.where(link_class: 'permission',
482                                       name: 'can_login').select do |perm|
483           perm.properties['username'] == username
484         end
485         return username if login_collisions.empty?
486       end
487       username = username + SecureRandom.random_number(10).to_s
488     end
489   end
490
491   # Send notification if the user saved profile for the first time
492   def send_profile_created_notification
493     if self.prefs_changed?
494       if self.prefs_was.andand.empty? || !self.prefs_was.andand['profile']
495         profile_notification_address = Rails.configuration.user_profile_notification_address
496         ProfileNotifier.profile_created(self, profile_notification_address).deliver if profile_notification_address
497       end
498     end
499   end
500
501 end