2873: permission links are owned by root
[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 :send_admin_notifications
16
17   has_many :authorized_keys, :foreign_key => :authorized_user_uuid, :primary_key => :uuid
18
19   api_accessible :user, extend: :common do |t|
20     t.add :email
21     t.add :full_name
22     t.add :first_name
23     t.add :last_name
24     t.add :identity_url
25     t.add :is_active
26     t.add :is_admin
27     t.add :is_invited
28     t.add :prefs
29   end
30
31   ALL_PERMISSIONS = {read: true, write: true, manage: true}
32
33   def full_name
34     "#{first_name} #{last_name}".strip
35   end
36
37   def is_invited
38     !!(self.is_active ||
39        Rails.configuration.new_users_are_active ||
40        self.groups_i_can(:read).select { |x| x.match /-f+$/ }.first)
41   end
42
43   def groups_i_can(verb)
44     self.group_permissions.select { |uuid, mask| mask[verb] }.keys
45   end
46
47   def can?(actions)
48     return true if is_admin
49     actions.each do |action, target|
50       target_uuid = target
51       if target.respond_to? :uuid
52         target_uuid = target.uuid
53       end
54       next if target_uuid == self.uuid
55       next if (group_permissions[target_uuid] and
56                group_permissions[target_uuid][action])
57       if target.respond_to? :owner_uuid
58         next if target.owner_uuid == self.uuid
59         next if (group_permissions[target.owner_uuid] and
60                  group_permissions[target.owner_uuid][action])
61       end
62       return false
63     end
64     true
65   end
66
67   def self.invalidate_permissions_cache
68     Rails.cache.delete_matched(/^groups_for_user_/)
69   end
70
71   # Return a hash of {group_uuid: perm_hash} where perm_hash[:read]
72   # and perm_hash[:write] are true if this user can read and write
73   # objects owned by group_uuid.
74   def group_permissions
75     Rails.cache.fetch "groups_for_user_#{self.uuid}" do
76       permissions_from = {}
77       todo = {self.uuid => true}
78       done = {}
79       while !todo.empty?
80         lookup_uuids = todo.keys
81         lookup_uuids.each do |uuid| done[uuid] = true end
82         todo = {}
83         newgroups = []
84         Group.where('owner_uuid in (?)', lookup_uuids).each do |group|
85           newgroups << [group.owner_uuid, group.uuid, 'can_manage']
86         end
87         Link.where('tail_uuid in (?) and link_class = ? and (head_uuid like ? or head_uuid like ?)',
88                    lookup_uuids,
89                    'permission',
90                    Group.uuid_like_pattern,
91                    User.uuid_like_pattern).each do |link|
92           newgroups << [link.tail_uuid, link.head_uuid, link.name]
93         end
94         newgroups.each do |tail_uuid, head_uuid, perm_name|
95           unless done.has_key? head_uuid
96             todo[head_uuid] = true
97           end
98           link_permissions = {}
99           case perm_name
100           when 'can_read'
101             link_permissions = {read:true}
102           when 'can_write'
103             link_permissions = {read:true,write:true}
104           when 'can_manage'
105             link_permissions = ALL_PERMISSIONS
106           end
107           permissions_from[tail_uuid] ||= {}
108           permissions_from[tail_uuid][head_uuid] ||= {}
109           link_permissions.each do |k,v|
110             permissions_from[tail_uuid][head_uuid][k] ||= v
111           end
112         end
113       end
114       search_permissions(self.uuid, permissions_from)
115     end
116   end
117
118   def self.setup(user, openid_prefix, repo_name=nil, vm_uuid=nil)
119     return user.setup_repo_vm_links(repo_name, vm_uuid, openid_prefix)
120   end
121
122   # create links
123   def setup_repo_vm_links(repo_name, vm_uuid, openid_prefix)
124     oid_login_perm = create_oid_login_perm openid_prefix
125     repo_perm = create_user_repo_link repo_name
126     vm_login_perm = create_vm_login_permission_link vm_uuid, repo_name
127     group_perm = create_user_group_link
128
129     return [oid_login_perm, repo_perm, vm_login_perm, group_perm, self].compact
130   end
131
132   # delete user signatures, login, repo, and vm perms, and mark as inactive
133   def unsetup
134     # delete oid_login_perms for this user
135     oid_login_perms = Link.where(tail_uuid: self.email,
136                                  link_class: 'permission',
137                                  name: 'can_login')
138     oid_login_perms.each do |perm|
139       Link.delete perm
140     end
141
142     # delete repo_perms for this user
143     repo_perms = Link.where(tail_uuid: self.uuid,
144                             link_class: 'permission',
145                             name: 'can_write')
146     repo_perms.each do |perm|
147       Link.delete perm
148     end
149
150     # delete vm_login_perms for this user
151     vm_login_perms = Link.where(tail_uuid: self.uuid,
152                                 link_class: 'permission',
153                                 name: 'can_login')
154     vm_login_perms.each do |perm|
155       Link.delete perm
156     end
157
158     # delete "All users' group read permissions for this user
159     group = Group.where(name: 'All users').select do |g|
160       g[:uuid].match /-f+$/
161     end.first
162     group_perms = Link.where(tail_uuid: self.uuid,
163                              head_uuid: group[:uuid],
164                              link_class: 'permission',
165                              name: 'can_read')
166     group_perms.each do |perm|
167       Link.delete perm
168     end
169
170     # delete any signatures by this user
171     signed_uuids = Link.where(link_class: 'signature',
172                               tail_uuid: self.uuid)
173     signed_uuids.each do |sign|
174       Link.delete sign
175     end
176
177     # mark the user as inactive
178     self.is_active = false
179     self.save!
180   end
181
182   def owns? object_uuid
183     return User.find_user_owning(object_uuid).andand.uuid == uuid
184   end
185
186   def can_manage? object_uuid
187     is_admin or
188       owns?(object_uuid) or
189       has_permission?(:can_manage, object_uuid)
190   end
191
192   protected
193
194   # Returns the first User found in the ownership path for obj_uuid.
195   # If obj_uuid is not owned by any user, returns nil.
196   #
197   # TODO(twp): this code largely stolen from
198   # ArvadosModel::ensure_ownership_path_leads_to_user. See if we can
199   # refactor these methods to share more code.
200   #
201   def self.find_user_owning obj_uuid
202     uuid_in_path = {obj_uuid => true}
203     # Walk up the owner_uuid chain for obj_uuid until one of these
204     # conditions is met:
205     #   - the owner_uuid belongs to the User class
206     #   - no owner_uuid is found (no User owns this object)
207     #   - we discover an ownership cycle (a fatal consistency error)
208     #
209     x = obj_uuid
210     while (owner_class = ArvadosModel.resource_class_for_uuid(x)) != User
211       begin
212         if !owner_class.respond_to? :find_by_uuid
213           raise ActiveRecord::RecordNotFound.new
214         else
215           x = owner_class.find_by_uuid(x).owner_uuid
216         end
217       rescue ActiveRecord::RecordNotFound => e
218         # errors.add :owner_uuid, "is not owned by any user: #{e}"
219         return nil
220       end
221       # If there is an ownership cycle, we can conclude that
222       # no User owns this object.
223       if uuid_in_path[x]
224         return nil
225       end
226       uuid_in_path[x] = true
227     end
228     return owner_class.find_by_uuid(x)
229   end
230
231   def ensure_ownership_path_leads_to_user
232     true
233   end
234
235   def permission_to_update
236     # users must be able to update themselves (even if they are
237     # inactive) in order to create sessions
238     self == current_user or super
239   end
240
241   def permission_to_create
242     current_user.andand.is_admin or
243       (self == current_user and
244        self.is_active == Rails.configuration.new_users_are_active)
245   end
246
247   def check_auto_admin
248     if User.where("uuid not like '%-000000000000000'").where(:is_admin => true).count == 0 and Rails.configuration.auto_admin_user
249       if current_user.email == Rails.configuration.auto_admin_user
250         self.is_admin = true
251         self.is_active = true
252       end
253     end
254   end
255
256   def prevent_privilege_escalation
257     if current_user.andand.is_admin
258       return true
259     end
260     if self.is_active_changed?
261       if self.is_active != self.is_active_was
262         logger.warn "User #{current_user.uuid} tried to change is_active from #{self.is_admin_was} to #{self.is_admin} for #{self.uuid}"
263         self.is_active = self.is_active_was
264       end
265     end
266     if self.is_admin_changed?
267       if self.is_admin != self.is_admin_was
268         logger.warn "User #{current_user.uuid} tried to change is_admin from #{self.is_admin_was} to #{self.is_admin} for #{self.uuid}"
269         self.is_admin = self.is_admin_was
270       end
271     end
272     true
273   end
274
275   def prevent_inactive_admin
276     if self.is_admin and not self.is_active
277       # There is no known use case for the strange set of permissions
278       # that would result from this change. It's safest to assume it's
279       # a mistake and disallow it outright.
280       raise "Admin users cannot be inactive"
281     end
282     true
283   end
284
285   def search_permissions(start, graph, merged={}, upstream_mask=nil, upstream_path={})
286     nextpaths = graph[start]
287     return merged if !nextpaths
288     return merged if upstream_path.has_key? start
289     upstream_path[start] = true
290     upstream_mask ||= ALL_PERMISSIONS
291     nextpaths.each do |head, mask|
292       merged[head] ||= {}
293       mask.each do |k,v|
294         merged[head][k] ||= v if upstream_mask[k]
295       end
296       search_permissions(head, graph, merged, upstream_mask.select { |k,v| v && merged[head][k] }, upstream_path)
297     end
298     upstream_path.delete start
299     merged
300   end
301
302   def create_oid_login_perm (openid_prefix)
303     login_perm_props = { "identity_url_prefix" => openid_prefix}
304
305     # Check oid_login_perm
306     oid_login_perms = Link.where(tail_uuid: self.email,
307                                    link_class: 'permission',
308                                    name: 'can_login').where("head_uuid = ?", self.uuid)
309
310     if !oid_login_perms.any?
311       # create openid login permission
312       oid_login_perm = Link.create(link_class: 'permission',
313                                    name: 'can_login',
314                                    tail_uuid: self.email,
315                                    head_uuid: self.uuid,
316                                    properties: login_perm_props
317                                   )
318       logger.info { "openid login permission: " + oid_login_perm[:uuid] }
319     else
320       oid_login_perm = oid_login_perms.first
321     end
322
323     return oid_login_perm
324   end
325
326   def create_user_repo_link(repo_name)
327     # repo_name is optional
328     if not repo_name
329       logger.warn ("Repository name not given for #{self.uuid}.")
330       return
331     end
332
333     # Check for an existing repository with the same name we're about to use.
334     repo = Repository.where(name: repo_name).first
335
336     if repo
337       logger.warn "Repository exists for #{repo_name}: #{repo[:uuid]}."
338
339       # Look for existing repository access for this repo
340       repo_perms = Link.where(tail_uuid: self.uuid,
341                               head_uuid: repo[:uuid],
342                               link_class: 'permission',
343                               name: 'can_write')
344       if repo_perms.any?
345         logger.warn "User already has repository access " +
346             repo_perms.collect { |p| p[:uuid] }.inspect
347         return repo_perms.first
348       end
349     end
350
351     # create repo, if does not already exist
352     repo ||= Repository.create(name: repo_name)
353     logger.info { "repo uuid: " + repo[:uuid] }
354
355     repo_perm = Link.create(tail_uuid: self.uuid,
356                             head_uuid: repo[:uuid],
357                             link_class: 'permission',
358                             name: 'can_write')
359     logger.info { "repo permission: " + repo_perm[:uuid] }
360     return repo_perm
361   end
362
363   # create login permission for the given vm_uuid, if it does not already exist
364   def create_vm_login_permission_link(vm_uuid, repo_name)
365     begin
366
367       # vm uuid is optional
368       if vm_uuid
369         vm = VirtualMachine.where(uuid: vm_uuid).first
370
371         if not vm
372           logger.warn "Could not find virtual machine for #{vm_uuid.inspect}"
373           raise "No vm found for #{vm_uuid}"
374         end
375       else
376         return
377       end
378
379       logger.info { "vm uuid: " + vm[:uuid] }
380
381       login_perms = Link.where(tail_uuid: self.uuid,
382                               head_uuid: vm[:uuid],
383                               link_class: 'permission',
384                               name: 'can_login')
385
386       perm_exists = false
387       login_perms.each do |perm|
388         if perm.properties['username'] == repo_name
389           perm_exists = perm
390           break
391         end
392       end
393
394       if perm_exists
395         login_perm = perm_exists
396       else
397         login_perm = Link.create(tail_uuid: self.uuid,
398                                  head_uuid: vm[:uuid],
399                                  link_class: 'permission',
400                                  name: 'can_login',
401                                  properties: {'username' => repo_name})
402         logger.info { "login permission: " + login_perm[:uuid] }
403       end
404
405       return login_perm
406     end
407   end
408
409   # add the user to the 'All users' group
410   def create_user_group_link
411     # Look up the "All users" group (we expect uuid *-*-fffffffffffffff).
412     group = Group.where(name: 'All users').select do |g|
413       g[:uuid].match /-f+$/
414     end.first
415
416     if not group
417       logger.warn "No 'All users' group with uuid '*-*-fffffffffffffff'."
418       raise "No 'All users' group with uuid '*-*-fffffffffffffff' is found"
419     else
420       logger.info { "\"All users\" group uuid: " + group[:uuid] }
421
422       group_perms = Link.where(tail_uuid: self.uuid,
423                               head_uuid: group[:uuid],
424                               link_class: 'permission',
425                               name: 'can_read')
426
427       if !group_perms.any?
428         group_perm = Link.create(tail_uuid: self.uuid,
429                                  head_uuid: group[:uuid],
430                                  link_class: 'permission',
431                                  name: 'can_read')
432         logger.info { "group permission: " + group_perm[:uuid] }
433       else
434         group_perm = group_perms.first
435       end
436
437       return group_perm
438     end
439   end
440
441   # Give the special "System group" permission to manage this user and
442   # all of this user's stuff.
443   #
444   def add_system_group_permission_link
445     act_as_system_user do
446       Link.create(link_class: 'permission',
447                   name: 'can_manage',
448                   tail_uuid: system_group_uuid,
449                   head_uuid: self.uuid)
450     end
451   end
452
453   # Send admin notifications
454   def send_admin_notifications
455     AdminNotifier.new_user(self).deliver
456     if not self.is_active then
457       AdminNotifier.new_inactive_user(self).deliver
458     end
459   end
460 end