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