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