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