Merge branch 'master' into 1776-setup-user-email
[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.created_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 normalize_collection_uuids
189     foreign_key_attributes.each do |attr|
190       attr_value = send attr
191       if attr_value.is_a? String and
192           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
193         begin
194           send "#{attr}=", Collection.normalize_uuid(attr_value)
195         rescue
196           # TODO: abort instead of silently accepting unnormalizable value?
197         end
198       end
199     end
200   end
201
202   @@UUID_REGEX = /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/
203
204   @@prefixes_hash = nil
205   def self.uuid_prefixes
206     unless @@prefixes_hash
207       @@prefixes_hash = {}
208       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
209         if k.respond_to?(:uuid_prefix)
210           @@prefixes_hash[k.uuid_prefix] = k
211         end
212       end
213     end
214     @@prefixes_hash
215   end
216
217   def self.uuid_like_pattern
218     "_____-#{uuid_prefix}-_______________"
219   end
220
221   def ensure_valid_uuids
222     specials = [system_user_uuid, 'd41d8cd98f00b204e9800998ecf8427e+0']
223
224     foreign_key_attributes.each do |attr|
225       if new_record? or send (attr + "_changed?")
226         attr_value = send attr
227         r = ArvadosModel::resource_class_for_uuid attr_value if attr_value
228         r = r.readable_by(current_user) if r and not skip_uuid_read_permission_check.include? attr
229         if r and r.where(uuid: attr_value).count == 0 and not specials.include? attr_value
230           errors.add(attr, "'#{attr_value}' not found")
231         end
232       end
233     end
234   end
235
236   class Email
237     def self.kind
238       "email"
239     end
240
241     def kind
242       self.class.kind
243     end
244
245     def self.readable_by (u)
246       self
247     end
248
249     def self.where (u)
250       [{:uuid => u[:uuid]}]
251     end
252   end
253
254   def self.resource_class_for_uuid(uuid)
255     if uuid.is_a? ArvadosModel
256       return uuid.class
257     end
258     unless uuid.is_a? String
259       return nil
260     end
261     if uuid.match /^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/
262       return Collection
263     end
264     resource_class = nil
265
266     Rails.application.eager_load!
267     uuid.match @@UUID_REGEX do |re|
268       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
269     end
270
271     if uuid.match /.+@.+/
272       return Email
273     end
274
275     nil
276   end
277
278   def log_start_state
279     @old_etag = etag
280     @old_attributes = logged_attributes
281   end
282
283   def log_change(event_type)
284     log = Log.new(event_type: event_type).fill_object(self)
285     yield log
286     log.save!
287     log_start_state
288   end
289
290   def log_create
291     log_change('create') do |log|
292       log.fill_properties('old', nil, nil)
293       log.update_to self
294     end
295   end
296
297   def log_update
298     log_change('update') do |log|
299       log.fill_properties('old', @old_etag, @old_attributes)
300       log.update_to self
301     end
302   end
303
304   def log_destroy
305     log_change('destroy') do |log|
306       log.fill_properties('old', @old_etag, @old_attributes)
307       log.update_to nil
308     end
309   end
310 end