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