Add 'apps/arv-web/' from commit 'f9732ad8460d013c2f28363655d0d1b91894dca5'
[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     return if self.uuid.end_with?('anonymouspublic')
238     if (User.where("email = ?",self.email).where(:is_admin => true).count == 0 and
239         Rails.configuration.auto_admin_user and self.email == Rails.configuration.auto_admin_user) or
240        (User.where("uuid not like '%-000000000000000'").where(:is_admin => true).count == 0 and 
241         Rails.configuration.auto_admin_first_user)
242       self.is_admin = true
243       self.is_active = true
244     end
245   end
246
247   def prevent_privilege_escalation
248     if current_user.andand.is_admin
249       return true
250     end
251     if self.is_active_changed?
252       if self.is_active != self.is_active_was
253         logger.warn "User #{current_user.uuid} tried to change is_active from #{self.is_admin_was} to #{self.is_admin} for #{self.uuid}"
254         self.is_active = self.is_active_was
255       end
256     end
257     if self.is_admin_changed?
258       if self.is_admin != self.is_admin_was
259         logger.warn "User #{current_user.uuid} tried to change is_admin from #{self.is_admin_was} to #{self.is_admin} for #{self.uuid}"
260         self.is_admin = self.is_admin_was
261       end
262     end
263     true
264   end
265
266   def prevent_inactive_admin
267     if self.is_admin and not self.is_active
268       # There is no known use case for the strange set of permissions
269       # that would result from this change. It's safest to assume it's
270       # a mistake and disallow it outright.
271       raise "Admin users cannot be inactive"
272     end
273     true
274   end
275
276   def search_permissions(start, graph, merged={}, upstream_mask=nil, upstream_path={})
277     nextpaths = graph[start]
278     return merged if !nextpaths
279     return merged if upstream_path.has_key? start
280     upstream_path[start] = true
281     upstream_mask ||= ALL_PERMISSIONS
282     nextpaths.each do |head, mask|
283       merged[head] ||= {}
284       mask.each do |k,v|
285         merged[head][k] ||= v if upstream_mask[k]
286       end
287       search_permissions(head, graph, merged, upstream_mask.select { |k,v| v && merged[head][k] }, upstream_path)
288     end
289     upstream_path.delete start
290     merged
291   end
292
293   def create_oid_login_perm (openid_prefix)
294     login_perm_props = { "identity_url_prefix" => openid_prefix}
295
296     # Check oid_login_perm
297     oid_login_perms = Link.where(tail_uuid: self.email,
298                                    link_class: 'permission',
299                                    name: 'can_login').where("head_uuid = ?", self.uuid)
300
301     if !oid_login_perms.any?
302       # create openid login permission
303       oid_login_perm = Link.create(link_class: 'permission',
304                                    name: 'can_login',
305                                    tail_uuid: self.email,
306                                    head_uuid: self.uuid,
307                                    properties: login_perm_props
308                                   )
309       logger.info { "openid login permission: " + oid_login_perm[:uuid] }
310     else
311       oid_login_perm = oid_login_perms.first
312     end
313
314     return oid_login_perm
315   end
316
317   def create_user_repo_link(repo_name)
318     # repo_name is optional
319     if not repo_name
320       logger.warn ("Repository name not given for #{self.uuid}.")
321       return
322     end
323
324     # Check for an existing repository with the same name we're about to use.
325     repo = Repository.where(name: repo_name).first
326
327     if repo
328       logger.warn "Repository exists for #{repo_name}: #{repo[:uuid]}."
329
330       # Look for existing repository access for this repo
331       repo_perms = Link.where(tail_uuid: self.uuid,
332                               head_uuid: repo[:uuid],
333                               link_class: 'permission',
334                               name: 'can_manage')
335       if repo_perms.any?
336         logger.warn "User already has repository access " +
337             repo_perms.collect { |p| p[:uuid] }.inspect
338         return repo_perms.first
339       end
340     end
341
342     # create repo, if does not already exist
343     repo ||= Repository.create(name: repo_name)
344     logger.info { "repo uuid: " + repo[:uuid] }
345
346     repo_perm = Link.create(tail_uuid: self.uuid,
347                             head_uuid: repo[:uuid],
348                             link_class: 'permission',
349                             name: 'can_manage')
350     logger.info { "repo permission: " + repo_perm[:uuid] }
351     return repo_perm
352   end
353
354   # create login permission for the given vm_uuid, if it does not already exist
355   def create_vm_login_permission_link(vm_uuid, repo_name)
356     begin
357
358       # vm uuid is optional
359       if vm_uuid
360         vm = VirtualMachine.where(uuid: vm_uuid).first
361
362         if not vm
363           logger.warn "Could not find virtual machine for #{vm_uuid.inspect}"
364           raise "No vm found for #{vm_uuid}"
365         end
366       else
367         return
368       end
369
370       logger.info { "vm uuid: " + vm[:uuid] }
371
372       login_perms = Link.where(tail_uuid: self.uuid,
373                               head_uuid: vm[:uuid],
374                               link_class: 'permission',
375                               name: 'can_login')
376
377       perm_exists = false
378       login_perms.each do |perm|
379         if perm.properties['username'] == repo_name
380           perm_exists = perm
381           break
382         end
383       end
384
385       if perm_exists
386         login_perm = perm_exists
387       else
388         login_perm = Link.create(tail_uuid: self.uuid,
389                                  head_uuid: vm[:uuid],
390                                  link_class: 'permission',
391                                  name: 'can_login',
392                                  properties: {'username' => repo_name})
393         logger.info { "login permission: " + login_perm[:uuid] }
394       end
395
396       return login_perm
397     end
398   end
399
400   # add the user to the 'All users' group
401   def create_user_group_link
402     return (Link.where(tail_uuid: self.uuid,
403                        head_uuid: all_users_group[:uuid],
404                        link_class: 'permission',
405                        name: 'can_read').first or
406             Link.create(tail_uuid: self.uuid,
407                         head_uuid: all_users_group[:uuid],
408                         link_class: 'permission',
409                         name: 'can_read'))
410   end
411
412   # Give the special "System group" permission to manage this user and
413   # all of this user's stuff.
414   #
415   def add_system_group_permission_link
416     act_as_system_user do
417       Link.create(link_class: 'permission',
418                   name: 'can_manage',
419                   tail_uuid: system_group_uuid,
420                   head_uuid: self.uuid)
421     end
422   end
423
424   # Send admin notifications
425   def send_admin_notifications
426     AdminNotifier.new_user(self).deliver
427     if not self.is_active then
428       AdminNotifier.new_inactive_user(self).deliver
429     end
430   end
431
432   # Automatically setup new user during creation
433   def auto_setup_new_user
434     return true if !Rails.configuration.auto_setup_new_users
435     return true if !self.email
436     return true if self.uuid == system_user_uuid
437     return true if self.uuid == anonymous_user_uuid
438
439     if Rails.configuration.auto_setup_new_users_with_vm_uuid ||
440        Rails.configuration.auto_setup_new_users_with_repository
441       username = self.email.partition('@')[0] if self.email
442       return true if !username
443
444       blacklisted_usernames = Rails.configuration.auto_setup_name_blacklist
445       if blacklisted_usernames.include?(username)
446         return true
447       elsif !(/^[a-zA-Z][-._a-zA-Z0-9]{0,30}[a-zA-Z0-9]$/.match(username))
448         return true
449       else
450         return true if !(username = derive_unique_username username)
451       end
452     end
453
454     # setup user
455     setup_repo_vm_links(username,
456                         Rails.configuration.auto_setup_new_users_with_vm_uuid,
457                         Rails.configuration.default_openid_prefix)
458   end
459
460   # Find a username that starts with the given string and does not collide
461   # with any existing repository name or VM login name
462   def derive_unique_username username
463     while true
464       if Repository.where(name: username).empty?
465         login_collisions = Link.where(link_class: 'permission',
466                                       name: 'can_login').select do |perm|
467           perm.properties['username'] == username
468         end
469         return username if login_collisions.empty?
470       end
471       username = username + SecureRandom.random_number(10).to_s
472     end
473   end
474
475   # Send notification if the user saved profile for the first time
476   def send_profile_created_notification
477     if self.prefs_changed?
478       if self.prefs_was.andand.empty? || !self.prefs_was.andand['profile']
479         profile_notification_address = Rails.configuration.user_profile_notification_address
480         ProfileNotifier.profile_created(self, profile_notification_address).deliver if profile_notification_address
481       end
482     end
483   end
484
485 end