Merge branch '2798-go-keep-client' closes #2798
[arvados.git] / services / api / app / models / arvados_model.rb
1 require 'has_uuid'
2
3 class ArvadosModel < ActiveRecord::Base
4   self.abstract_class = true
5
6   include CurrentApiClient      # current_user, current_api_client, etc.
7
8   attr_protected :created_at
9   attr_protected :modified_by_user_uuid
10   attr_protected :modified_by_client_uuid
11   attr_protected :modified_at
12   after_initialize :log_start_state
13   before_save :ensure_permission_to_save
14   before_save :ensure_owner_uuid_is_permitted
15   before_save :ensure_ownership_path_leads_to_user
16   before_destroy :ensure_owner_uuid_is_permitted
17   before_destroy :ensure_permission_to_destroy
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'", dependent: :destroy
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 respond_to? :owner_uuid=
191       self.owner_uuid ||= current_user.uuid
192     end
193     if self.owner_uuid_changed?
194       if current_user.uuid == self.owner_uuid or
195           current_user.can? write: self.owner_uuid
196         # current_user is, or has :write permission on, the new owner
197       else
198         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}"
199         raise PermissionDeniedError
200       end
201     end
202     if new_record?
203       return true
204     elsif current_user.uuid == self.owner_uuid_was or
205         current_user.uuid == self.uuid or
206         current_user.can? write: self.owner_uuid_was
207       # current user is, or has :write permission on, the previous owner
208       return true
209     else
210       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}"
211       raise PermissionDeniedError
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     true
253   end
254
255   def update_modified_by_fields
256     self.updated_at = Time.now
257     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
258     self.modified_at = Time.now
259     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
260     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
261     true
262   end
263
264   def ensure_serialized_attribute_type
265     # Specifying a type in the "serialize" declaration causes rails to
266     # raise an exception if a different data type is retrieved from
267     # the database during load().  The validation preventing such
268     # crash-inducing records from being inserted in the database in
269     # the first place seems to have been left as an exercise to the
270     # developer.
271     self.class.serialized_attributes.each do |colname, attr|
272       if attr.object_class
273         unless self.attributes[colname].is_a? attr.object_class
274           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}"
275         end
276       end
277     end
278   end
279
280   def foreign_key_attributes
281     attributes.keys.select { |a| a.match /_uuid$/ }
282   end
283
284   def skip_uuid_read_permission_check
285     %w(modified_by_client_uuid)
286   end
287
288   def skip_uuid_existence_check
289     []
290   end
291
292   def normalize_collection_uuids
293     foreign_key_attributes.each do |attr|
294       attr_value = send attr
295       if attr_value.is_a? String and
296           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
297         begin
298           send "#{attr}=", Collection.normalize_uuid(attr_value)
299         rescue
300           # TODO: abort instead of silently accepting unnormalizable value?
301         end
302       end
303     end
304   end
305
306   @@UUID_REGEX = /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/
307
308   @@prefixes_hash = nil
309   def self.uuid_prefixes
310     unless @@prefixes_hash
311       @@prefixes_hash = {}
312       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
313         if k.respond_to?(:uuid_prefix)
314           @@prefixes_hash[k.uuid_prefix] = k
315         end
316       end
317     end
318     @@prefixes_hash
319   end
320
321   def self.uuid_like_pattern
322     "_____-#{uuid_prefix}-_______________"
323   end
324
325   def ensure_valid_uuids
326     specials = [system_user_uuid, 'd41d8cd98f00b204e9800998ecf8427e+0']
327
328     foreign_key_attributes.each do |attr|
329       if new_record? or send (attr + "_changed?")
330         next if skip_uuid_existence_check.include? attr
331         attr_value = send attr
332         next if specials.include? attr_value
333         if attr_value
334           if (r = ArvadosModel::resource_class_for_uuid attr_value)
335             unless skip_uuid_read_permission_check.include? attr
336               r = r.readable_by(current_user)
337             end
338             if r.where(uuid: attr_value).count == 0
339               errors.add(attr, "'#{attr_value}' not found")
340             end
341           end
342         end
343       end
344     end
345   end
346
347   class Email
348     def self.kind
349       "email"
350     end
351
352     def kind
353       self.class.kind
354     end
355
356     def self.readable_by (*u)
357       self
358     end
359
360     def self.where (u)
361       [{:uuid => u[:uuid]}]
362     end
363   end
364
365   def self.resource_class_for_uuid(uuid)
366     if uuid.is_a? ArvadosModel
367       return uuid.class
368     end
369     unless uuid.is_a? String
370       return nil
371     end
372     if uuid.match /^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/
373       return Collection
374     end
375     resource_class = nil
376
377     Rails.application.eager_load!
378     uuid.match @@UUID_REGEX do |re|
379       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
380     end
381
382     if uuid.match /.+@.+/
383       return Email
384     end
385
386     nil
387   end
388
389   def log_start_state
390     @old_etag = etag
391     @old_attributes = logged_attributes
392   end
393
394   def log_change(event_type)
395     log = Log.new(event_type: event_type).fill_object(self)
396     yield log
397     log.save!
398     connection.execute "NOTIFY logs, '#{log.id}'"
399     log_start_state
400   end
401
402   def log_create
403     log_change('create') do |log|
404       log.fill_properties('old', nil, nil)
405       log.update_to self
406     end
407   end
408
409   def log_update
410     log_change('update') do |log|
411       log.fill_properties('old', @old_etag, @old_attributes)
412       log.update_to self
413     end
414   end
415
416   def log_destroy
417     log_change('destroy') do |log|
418       log.fill_properties('old', @old_etag, @old_attributes)
419       log.update_to nil
420     end
421   end
422 end