- Added object_owner_uuid to logs table, which records the owner of the object
[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_create :ensure_permission_to_create
13   before_update :ensure_permission_to_update
14   before_destroy :ensure_permission_to_destroy
15
16   before_create :update_modified_by_fields
17   before_update :maybe_update_modified_by_fields
18   after_create :log_create
19   after_update :log_update
20   after_destroy :log_destroy
21   validate :ensure_serialized_attribute_type
22   validate :normalize_collection_uuids
23   validate :ensure_valid_uuids
24
25   has_many :permissions, :foreign_key => :head_uuid, :class_name => 'Link', :primary_key => :uuid, :conditions => "link_class = 'permission'"
26
27   class PermissionDeniedError < StandardError
28     def http_status
29       403
30     end
31   end
32
33   class UnauthorizedError < StandardError
34     def http_status
35       401
36     end
37   end
38
39   def self.kind_class(kind)
40     kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
41   end
42
43   def href
44     "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
45   end
46
47   def self.searchable_columns operator
48     textonly_operator = !operator.match(/[<=>]/)
49     self.columns.collect do |col|
50       if col.name == 'owner_uuid'
51         nil
52       elsif [:string, :text].index(col.type)
53         col.name
54       elsif !textonly_operator and [:datetime, :integer].index(col.type)
55         col.name
56       end
57     end.compact
58   end
59
60   def self.attribute_column attr
61     self.columns.select { |col| col.name == attr.to_s }.first
62   end
63
64   # def eager_load_associations
65   #   self.class.columns.each do |col|
66   #     re = col.name.match /^(.*)_kind$/
67   #     if (re and
68   #         self.respond_to? re[1].to_sym and
69   #         (auuid = self.send((re[1] + '_uuid').to_sym)) and
70   #         (aclass = self.class.kind_class(self.send(col.name.to_sym))) and
71   #         (aobject = aclass.where('uuid=?', auuid).first))
72   #       self.instance_variable_set('@'+re[1], aobject)
73   #     end
74   #   end
75   # end
76
77   def self.readable_by user
78     uuid_list = [user.uuid, *user.groups_i_can(:read)]
79     sanitized_uuid_list = uuid_list.
80       collect { |uuid| sanitize(uuid) }.join(', ')
81     or_references_me = ''
82
83     if self == Link and user
84       or_references_me = "OR (#{table_name}.link_class in (#{sanitize 'permission'}, #{sanitize 'resources'}) AND #{sanitize user.uuid} IN (#{table_name}.head_uuid, #{table_name}.tail_uuid))"
85     end
86
87     if self == Log and user
88       object_owner = ", #{table_name}.object_owner_uuid"
89       or_object_owner = "OR (#{table_name}.object_owner_uuid in (#{sanitized_uuid_list}))"
90     end
91
92     # Link head points to this row, or to the owner of this row
93     # (or owner of object described by this row, for logs table only)
94     # Link tail originates from this user, or a group that is readable by this user
95     # Link is any permission link ('write' and 'manage' implicitly grant 'read')
96     # or
97     # This row is owned by this user, or owned by a group readable by this user
98     # or
99     # This row uuid is equal this user uuid
100     # or
101     # This is the links table
102     # This row is a permission link
103     # The current user is referenced in either the head or tail of the link
104     # or
105     # This is the logs table
106     # This object described by this row is owned by this user, or owned by a group readable by this user
107     joins("LEFT JOIN links permissions ON permissions.head_uuid in (#{table_name}.owner_uuid, #{table_name}.uuid #{object_owner}) AND permissions.tail_uuid in (#{sanitized_uuid_list}) AND permissions.link_class='permission'").
108       where("?=? OR #{table_name}.owner_uuid in (?) OR #{table_name}.uuid=? OR permissions.head_uuid IS NOT NULL #{or_references_me} #{or_object_owner}",
109             true, user.is_admin,
110             uuid_list,
111             user.uuid)
112   end
113
114   def logged_attributes
115     attributes
116   end
117
118   protected
119
120   def ensure_permission_to_create
121     raise PermissionDeniedError unless permission_to_create
122   end
123
124   def permission_to_create
125     current_user.andand.is_active
126   end
127
128   def ensure_permission_to_update
129     raise PermissionDeniedError unless permission_to_update
130   end
131
132   def permission_to_update
133     if !current_user
134       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
135       return false
136     end
137     if !current_user.is_active
138       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
139       return false
140     end
141     return true if current_user.is_admin
142     if self.uuid_changed?
143       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
144       return false
145     end
146     if self.owner_uuid_changed?
147       if current_user.uuid == self.owner_uuid or
148           current_user.can? write: self.owner_uuid
149         # current_user is, or has :write permission on, the new owner
150       else
151         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}"
152         return false
153       end
154     end
155     if current_user.uuid == self.owner_uuid_was or
156         current_user.uuid == self.uuid or
157         current_user.can? write: self.owner_uuid_was
158       # current user is, or has :write permission on, the previous owner
159       return true
160     else
161       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}"
162       return false
163     end
164   end
165
166   def ensure_permission_to_destroy
167     raise PermissionDeniedError unless permission_to_destroy
168   end
169
170   def permission_to_destroy
171     permission_to_update
172   end
173
174   def maybe_update_modified_by_fields
175     update_modified_by_fields if self.changed? or self.new_record?
176   end
177
178   def update_modified_by_fields
179     self.updated_at = Time.now
180     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
181     self.modified_at = Time.now
182     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
183     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
184   end
185
186   def ensure_serialized_attribute_type
187     # Specifying a type in the "serialize" declaration causes rails to
188     # raise an exception if a different data type is retrieved from
189     # the database during load().  The validation preventing such
190     # crash-inducing records from being inserted in the database in
191     # the first place seems to have been left as an exercise to the
192     # developer.
193     self.class.serialized_attributes.each do |colname, attr|
194       if attr.object_class
195         unless self.attributes[colname].is_a? attr.object_class
196           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}"
197         end
198       end
199     end
200   end
201
202   def foreign_key_attributes
203     attributes.keys.select { |a| a.match /_uuid$/ }
204   end
205
206   def skip_uuid_read_permission_check
207     %w(modified_by_client_uuid)
208   end
209
210   def skip_uuid_existence_check
211     []
212   end
213
214   def normalize_collection_uuids
215     foreign_key_attributes.each do |attr|
216       attr_value = send attr
217       if attr_value.is_a? String and
218           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
219         begin
220           send "#{attr}=", Collection.normalize_uuid(attr_value)
221         rescue
222           # TODO: abort instead of silently accepting unnormalizable value?
223         end
224       end
225     end
226   end
227
228   @@UUID_REGEX = /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/
229
230   @@prefixes_hash = nil
231   def self.uuid_prefixes
232     unless @@prefixes_hash
233       @@prefixes_hash = {}
234       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
235         if k.respond_to?(:uuid_prefix)
236           @@prefixes_hash[k.uuid_prefix] = k
237         end
238       end
239     end
240     @@prefixes_hash
241   end
242
243   def self.uuid_like_pattern
244     "_____-#{uuid_prefix}-_______________"
245   end
246
247   def ensure_valid_uuids
248     specials = [system_user_uuid, 'd41d8cd98f00b204e9800998ecf8427e+0']
249
250     foreign_key_attributes.each do |attr|
251       if new_record? or send (attr + "_changed?")
252         next if skip_uuid_existence_check.include? attr
253         attr_value = send attr
254         next if specials.include? attr_value
255         if attr_value
256           if (r = ArvadosModel::resource_class_for_uuid attr_value)
257             unless skip_uuid_read_permission_check.include? attr
258               r = r.readable_by(current_user)
259             end
260             if r.where(uuid: attr_value).count == 0
261               errors.add(attr, "'#{attr_value}' not found")
262             end
263           end
264         end
265       end
266     end
267   end
268
269   class Email
270     def self.kind
271       "email"
272     end
273
274     def kind
275       self.class.kind
276     end
277
278     def self.readable_by (u)
279       self
280     end
281
282     def self.where (u)
283       [{:uuid => u[:uuid]}]
284     end
285   end
286
287   def self.resource_class_for_uuid(uuid)
288     if uuid.is_a? ArvadosModel
289       return uuid.class
290     end
291     unless uuid.is_a? String
292       return nil
293     end
294     if uuid.match /^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/
295       return Collection
296     end
297     resource_class = nil
298
299     Rails.application.eager_load!
300     uuid.match @@UUID_REGEX do |re|
301       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
302     end
303
304     if uuid.match /.+@.+/
305       return Email
306     end
307
308     nil
309   end
310
311   def log_start_state
312     @old_etag = etag
313     @old_attributes = logged_attributes
314   end
315
316   def log_change(event_type)
317     log = Log.new(event_type: event_type).fill_object(self)
318     yield log
319     log.save!
320     connection.execute "NOTIFY logs, '#{log.id}'"
321     log_start_state
322   end
323
324   def log_create
325     log_change('create') do |log|
326       log.fill_properties('old', nil, nil)
327       log.update_to self
328     end
329   end
330
331   def log_update
332     log_change('update') do |log|
333       log.fill_properties('old', @old_etag, @old_attributes)
334       log.update_to self
335     end
336   end
337
338   def log_destroy
339     log_change('destroy') do |log|
340       log.fill_properties('old', @old_etag, @old_attributes)
341       log.update_to nil
342     end
343   end
344 end