Adding missing file.
[arvados.git] / services / api / app / models / arvados_model.rb
1 require 'assign_uuid'
2 class ArvadosModel < ActiveRecord::Base
3   self.abstract_class = true
4
5   include CurrentApiClient      # current_user, current_api_client, etc.
6
7   attr_protected :created_at
8   attr_protected :modified_by_user_uuid
9   attr_protected :modified_by_client_uuid
10   attr_protected :modified_at
11   after_initialize :log_start_state
12   before_save :ensure_permission_to_save
13   before_save :ensure_owner_uuid_is_permitted
14   before_save :ensure_ownership_path_leads_to_user
15   before_destroy :ensure_owner_uuid_is_permitted
16   before_destroy :ensure_permission_to_destroy
17
18   before_create :update_modified_by_fields
19   before_update :maybe_update_modified_by_fields
20   after_create :log_create
21   after_update :log_update
22   after_destroy :log_destroy
23   validate :ensure_serialized_attribute_type
24   validate :normalize_collection_uuids
25   validate :ensure_valid_uuids
26
27   # Note: This only returns permission links. It does not account for
28   # permissions obtained via user.is_admin or
29   # user.uuid==object.owner_uuid.
30   has_many :permissions, :foreign_key => :head_uuid, :class_name => 'Link', :primary_key => :uuid, :conditions => "link_class = 'permission'"
31
32   class PermissionDeniedError < StandardError
33     def http_status
34       403
35     end
36   end
37
38   class UnauthorizedError < StandardError
39     def http_status
40       401
41     end
42   end
43
44   def self.kind_class(kind)
45     kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
46   end
47
48   def href
49     "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
50   end
51
52   def self.searchable_columns operator
53     textonly_operator = !operator.match(/[<=>]/)
54     self.columns.collect do |col|
55       if [:string, :text].index(col.type)
56         col.name
57       elsif !textonly_operator and [:datetime, :integer].index(col.type)
58         col.name
59       end
60     end.compact
61   end
62
63   def self.attribute_column attr
64     self.columns.select { |col| col.name == attr.to_s }.first
65   end
66
67   # Return nil if current user is not allowed to see the list of
68   # writers. Otherwise, return a list of user_ and group_uuids with
69   # write permission. (If not returning nil, current_user is always in
70   # the list because can_manage permission is needed to see the list
71   # of writers.)
72   def writable_by
73     unless (owner_uuid == current_user.uuid or
74             current_user.is_admin or
75             current_user.groups_i_can(:manage).index(owner_uuid))
76       return nil
77     end
78     [owner_uuid, current_user.uuid] + permissions.collect do |p|
79       if ['can_write', 'can_manage'].index p.name
80         p.tail_uuid
81       end
82     end.compact.uniq
83   end
84
85   # Return a query with read permissions restricted to the union of of the
86   # permissions of the members of users_list, i.e. if something is readable by
87   # any user in users_list, it will be readable in the query returned by this
88   # function.
89   def self.readable_by(*users_list)
90     # Get rid of troublesome nils
91     users_list.compact!
92
93     # Check if any of the users are admin.  If so, we're done.
94     if users_list.select { |u| u.is_admin }.empty?
95
96       # Collect the uuids for each user and any groups readable by each user.
97       user_uuids = users_list.map { |u| u.uuid }
98       uuid_list = user_uuids + users_list.flat_map { |u| u.groups_i_can(:read) }
99       sanitized_uuid_list = uuid_list.
100         collect { |uuid| sanitize(uuid) }.join(', ')
101       sql_conds = []
102       sql_params = []
103       or_object_uuid = ''
104
105       # This row is owned by a member of users_list, or owned by a group
106       # readable by a member of users_list
107       # or
108       # This row uuid is the uuid of a member of users_list
109       # or
110       # A permission link exists ('write' and 'manage' implicitly include
111       # 'read') from a member of users_list, or a group readable by users_list,
112       # to this row, or to the owner of this row (see join() below).
113       permitted_uuids = "(SELECT head_uuid FROM links WHERE link_class='permission' AND tail_uuid IN (#{sanitized_uuid_list}))"
114
115       sql_conds += ["#{table_name}.owner_uuid in (?)",
116                     "#{table_name}.uuid in (?)",
117                     "#{table_name}.uuid IN #{permitted_uuids}"]
118       sql_params += [uuid_list, user_uuids]
119
120       if self == Link and users_list.any?
121         # This row is a 'permission' or 'resources' link class
122         # The uuid for a member of users_list is referenced in either the head
123         # or tail of the link
124         sql_conds += ["(#{table_name}.link_class in (#{sanitize 'permission'}, #{sanitize 'resources'}) AND (#{table_name}.head_uuid IN (?) OR #{table_name}.tail_uuid IN (?)))"]
125         sql_params += [user_uuids, user_uuids]
126       end
127
128       if self == Log and users_list.any?
129         # Link head points to the object described by this row
130         sql_conds += ["#{table_name}.object_uuid IN #{permitted_uuids}"]
131
132         # This object described by this row is owned by this user, or owned by a group readable by this user
133         sql_conds += ["#{table_name}.object_owner_uuid in (?)"]
134         sql_params += [uuid_list]
135       end
136
137       # Link head points to this row, or to the owner of this row (the thing to be read)
138       #
139       # Link tail originates from this user, or a group that is readable by this
140       # user (the identity with authorization to read)
141       #
142       # Link class is 'permission' ('write' and 'manage' implicitly include 'read')
143       where(sql_conds.join(' OR '), *sql_params)
144     else
145       # At least one user is admin, so don't bother to apply any restrictions.
146       self
147     end
148   end
149
150   def logged_attributes
151     attributes
152   end
153
154   protected
155
156   def ensure_ownership_path_leads_to_user
157     if new_record? or owner_uuid_changed?
158       uuid_in_path = {owner_uuid => true, uuid => true}
159       x = owner_uuid
160       while (owner_class = self.class.resource_class_for_uuid(x)) != User
161         begin
162           if x == uuid
163             # Test for cycles with the new version, not the DB contents
164             x = owner_uuid
165           elsif !owner_class.respond_to? :find_by_uuid
166             raise ActiveRecord::RecordNotFound.new
167           else
168             x = owner_class.find_by_uuid(x).owner_uuid
169           end
170         rescue ActiveRecord::RecordNotFound => e
171           errors.add :owner_uuid, "is not owned by any user: #{e}"
172           return false
173         end
174         if uuid_in_path[x]
175           if x == owner_uuid
176             errors.add :owner_uuid, "would create an ownership cycle"
177           else
178             errors.add :owner_uuid, "has an ownership cycle"
179           end
180           return false
181         end
182         uuid_in_path[x] = true
183       end
184     end
185     true
186   end
187
188   def ensure_owner_uuid_is_permitted
189     raise PermissionDeniedError if !current_user
190     if self.respond_to? :owner_uuid=
191       self.owner_uuid ||= current_user.uuid
192       if self.owner_uuid_changed?
193         if current_user.uuid == self.owner_uuid or
194             current_user.can? write: self.owner_uuid
195           # current_user is, or has :write permission on, the new owner
196         else
197           logger.warn "User #{current_user.uuid} tried to change owner_uuid of #{self.class.to_s} #{self.uuid} to #{self.owner_uuid} but does not have permission to write to #{self.owner_uuid}"
198           raise PermissionDeniedError
199         end
200       end
201       if new_record?
202         return true
203       elsif current_user.uuid == self.owner_uuid_was or
204           current_user.uuid == self.uuid or
205           current_user.can? write: self.owner_uuid_was
206         # current user is, or has :write permission on, the previous owner
207         return true
208       else
209         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} but does not have permission to write #{self.owner_uuid_was}"
210         raise PermissionDeniedError
211       end
212     end
213   end
214
215   def ensure_permission_to_save
216     unless (new_record? ? permission_to_create : permission_to_update)
217       raise PermissionDeniedError
218     end
219   end
220
221   def permission_to_create
222     current_user.andand.is_active
223   end
224
225   def permission_to_update
226     if !current_user
227       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
228       return false
229     end
230     if !current_user.is_active
231       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
232       return false
233     end
234     return true if current_user.is_admin
235     if self.uuid_changed?
236       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
237       return false
238     end
239     return true
240   end
241
242   def ensure_permission_to_destroy
243     raise PermissionDeniedError unless permission_to_destroy
244   end
245
246   def permission_to_destroy
247     permission_to_update
248   end
249
250   def maybe_update_modified_by_fields
251     update_modified_by_fields if self.changed? or self.new_record?
252   end
253
254   def update_modified_by_fields
255     self.updated_at = Time.now
256     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
257     self.modified_at = Time.now
258     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
259     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
260   end
261
262   def ensure_serialized_attribute_type
263     # Specifying a type in the "serialize" declaration causes rails to
264     # raise an exception if a different data type is retrieved from
265     # the database during load().  The validation preventing such
266     # crash-inducing records from being inserted in the database in
267     # the first place seems to have been left as an exercise to the
268     # developer.
269     self.class.serialized_attributes.each do |colname, attr|
270       if attr.object_class
271         unless self.attributes[colname].is_a? attr.object_class
272           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}"
273         end
274       end
275     end
276   end
277
278   def foreign_key_attributes
279     attributes.keys.select { |a| a.match /_uuid$/ }
280   end
281
282   def skip_uuid_read_permission_check
283     %w(modified_by_client_uuid)
284   end
285
286   def skip_uuid_existence_check
287     []
288   end
289
290   def normalize_collection_uuids
291     foreign_key_attributes.each do |attr|
292       attr_value = send attr
293       if attr_value.is_a? String and
294           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
295         begin
296           send "#{attr}=", Collection.normalize_uuid(attr_value)
297         rescue
298           # TODO: abort instead of silently accepting unnormalizable value?
299         end
300       end
301     end
302   end
303
304   @@UUID_REGEX = /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/
305
306   @@prefixes_hash = nil
307   def self.uuid_prefixes
308     unless @@prefixes_hash
309       @@prefixes_hash = {}
310       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
311         if k.respond_to?(:uuid_prefix)
312           @@prefixes_hash[k.uuid_prefix] = k
313         end
314       end
315     end
316     @@prefixes_hash
317   end
318
319   def self.uuid_like_pattern
320     "_____-#{uuid_prefix}-_______________"
321   end
322
323   def ensure_valid_uuids
324     specials = [system_user_uuid, 'd41d8cd98f00b204e9800998ecf8427e+0']
325
326     foreign_key_attributes.each do |attr|
327       if new_record? or send (attr + "_changed?")
328         next if skip_uuid_existence_check.include? attr
329         attr_value = send attr
330         next if specials.include? attr_value
331         if attr_value
332           if (r = ArvadosModel::resource_class_for_uuid attr_value)
333             unless skip_uuid_read_permission_check.include? attr
334               r = r.readable_by(current_user)
335             end
336             if r.where(uuid: attr_value).count == 0
337               errors.add(attr, "'#{attr_value}' not found")
338             end
339           end
340         end
341       end
342     end
343   end
344
345   class Email
346     def self.kind
347       "email"
348     end
349
350     def kind
351       self.class.kind
352     end
353
354     def self.readable_by (*u)
355       self
356     end
357
358     def self.where (u)
359       [{:uuid => u[:uuid]}]
360     end
361   end
362
363   def self.resource_class_for_uuid(uuid)
364     if uuid.is_a? ArvadosModel
365       return uuid.class
366     end
367     unless uuid.is_a? String
368       return nil
369     end
370     if uuid.match /^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/
371       return Collection
372     end
373     resource_class = nil
374
375     Rails.application.eager_load!
376     uuid.match @@UUID_REGEX do |re|
377       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
378     end
379
380     if uuid.match /.+@.+/
381       return Email
382     end
383
384     nil
385   end
386
387   def log_start_state
388     @old_etag = etag
389     @old_attributes = logged_attributes
390   end
391
392   def log_change(event_type)
393     log = Log.new(event_type: event_type).fill_object(self)
394     yield log
395     log.save!
396     connection.execute "NOTIFY logs, '#{log.id}'"
397     log_start_state
398   end
399
400   def log_create
401     log_change('create') do |log|
402       log.fill_properties('old', nil, nil)
403       log.update_to self
404     end
405   end
406
407   def log_update
408     log_change('update') do |log|
409       log.fill_properties('old', @old_etag, @old_attributes)
410       log.update_to self
411     end
412   end
413
414   def log_destroy
415     log_change('destroy') do |log|
416       log.fill_properties('old', @old_etag, @old_attributes)
417       log.update_to nil
418     end
419   end
420 end