Merged master
[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   before_create :ensure_permission_to_create
12   before_update :ensure_permission_to_update
13   before_destroy :ensure_permission_to_destroy
14
15   before_validation :maybe_update_modified_by_fields
16   validate :ensure_serialized_attribute_type
17   validate :normalize_collection_uuids
18   validate :ensure_valid_uuids
19
20   has_many :permissions, :foreign_key => :head_uuid, :class_name => 'Link', :primary_key => :uuid, :conditions => "link_class = 'permission'"
21
22   class PermissionDeniedError < StandardError
23     def http_status
24       403
25     end
26   end
27
28   class UnauthorizedError < StandardError
29     def http_status
30       401
31     end
32   end
33
34   def self.kind_class(kind)
35     kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
36   end
37
38   def href
39     "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
40   end
41
42   def self.searchable_columns operator
43     textonly_operator = !operator.match(/[<=>]/)
44     self.columns.collect do |col|
45       if col.name == 'owner_uuid'
46         nil
47       elsif [:string, :text].index(col.type)
48         col.name
49       elsif !textonly_operator and [:datetime, :integer].index(col.type)
50         col.name
51       end
52     end.compact
53   end
54
55   def self.attribute_column attr
56     self.columns.select { |col| col.name == attr.to_s }.first
57   end
58
59   # def eager_load_associations
60   #   self.class.columns.each do |col|
61   #     re = col.name.match /^(.*)_kind$/
62   #     if (re and
63   #         self.respond_to? re[1].to_sym and
64   #         (auuid = self.send((re[1] + '_uuid').to_sym)) and
65   #         (aclass = self.class.kind_class(self.send(col.name.to_sym))) and
66   #         (aobject = aclass.where('uuid=?', auuid).first))
67   #       self.instance_variable_set('@'+re[1], aobject)
68   #     end
69   #   end
70   # end
71
72   def self.readable_by user
73     uuid_list = [user.uuid, *user.groups_i_can(:read)]
74     sanitized_uuid_list = uuid_list.
75       collect { |uuid| sanitize(uuid) }.join(', ')
76     or_references_me = ''
77     if self == Link and user
78       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))"
79     end
80     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'").
81       where("?=? OR #{table_name}.owner_uuid in (?) OR #{table_name}.uuid=? OR permissions.head_uuid IS NOT NULL #{or_references_me}",
82             true, user.is_admin,
83             uuid_list,
84             user.uuid)
85   end
86
87   protected
88
89   def ensure_permission_to_create
90     raise PermissionDeniedError unless permission_to_create
91   end
92
93   def permission_to_create
94     current_user.andand.is_active
95   end
96
97   def ensure_permission_to_update
98     raise PermissionDeniedError unless permission_to_update
99   end
100
101   def permission_to_update
102     if !current_user
103       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
104       return false
105     end
106     if !current_user.is_active
107       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
108       return false
109     end
110     return true if current_user.is_admin
111     if self.uuid_changed?
112       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
113       return false
114     end
115     if self.owner_uuid_changed?
116       if current_user.uuid == self.owner_uuid or
117           current_user.can? write: self.owner_uuid
118         # current_user is, or has :write permission on, the new owner
119       else
120         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}"
121         return false
122       end
123     end
124     if current_user.uuid == self.owner_uuid_was or
125         current_user.uuid == self.uuid or
126         current_user.can? write: self.owner_uuid_was
127       # current user is, or has :write permission on, the previous owner
128       return true
129     else
130       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}"
131       return false
132     end
133   end
134
135   def ensure_permission_to_destroy
136     raise PermissionDeniedError unless permission_to_destroy
137   end
138
139   def permission_to_destroy
140     permission_to_update
141   end
142
143   def maybe_update_modified_by_fields
144     update_modified_by_fields if self.changed? or self.new_record?
145   end
146
147   def update_modified_by_fields
148     self.created_at ||= Time.now
149     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
150     self.modified_at = Time.now
151     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
152     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
153   end
154
155   def ensure_serialized_attribute_type
156     # Specifying a type in the "serialize" declaration causes rails to
157     # raise an exception if a different data type is retrieved from
158     # the database during load().  The validation preventing such
159     # crash-inducing records from being inserted in the database in
160     # the first place seems to have been left as an exercise to the
161     # developer.
162     self.class.serialized_attributes.each do |colname, attr|
163       if attr.object_class
164         unless self.attributes[colname].is_a? attr.object_class
165           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}"
166         end
167       end
168     end
169   end
170
171   def foreign_key_attributes
172     attributes.keys.select { |a| a.match /_uuid$/ }
173   end
174
175   def skip_uuid_read_permission_check
176     %w(modified_by_client_uuid)
177   end
178
179   def normalize_collection_uuids
180     foreign_key_attributes.each do |attr|
181       attr_value = send attr
182       if attr_value.is_a? String and
183           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
184         begin
185           send "#{attr}=", Collection.normalize_uuid(attr_value)
186         rescue
187           # TODO: abort instead of silently accepting unnormalizable value?
188         end
189       end
190     end
191   end
192
193   @@UUID_REGEX = /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/
194
195   @@prefixes_hash = nil
196   def self.uuid_prefixes
197     unless @@prefixes_hash
198       @@prefixes_hash = {}
199       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
200         if k.respond_to?(:uuid_prefix)
201           @@prefixes_hash[k.uuid_prefix] = k
202         end
203       end
204     end
205     @@prefixes_hash
206   end
207
208   def self.uuid_like_pattern
209     "_____-#{uuid_prefix}-_______________"
210   end
211
212   def ensure_valid_uuids
213     specials = [system_user_uuid, 'd41d8cd98f00b204e9800998ecf8427e+0']
214
215     foreign_key_attributes.each do |attr|
216       begin
217         if new_record? or send (attr + "_changed?")
218           attr_value = send attr
219           r = ArvadosModel::resource_class_for_uuid attr_value if attr_value
220           r = r.readable_by(current_user) if r and not skip_uuid_read_permission_check.include? attr
221           if r and r.where(uuid: attr_value).count == 0 and not specials.include? attr_value
222             errors.add(attr, "'#{attr_value}' not found")
223           end
224         end
225       rescue Exception => e
226         bt = e.backtrace.join("\n")
227         errors.add(attr, "'#{attr_value}' error '#{e}'\n#{bt}\n")
228       end
229     end
230   end
231
232   def self.resource_class_for_uuid(uuid)
233     if uuid.is_a? ArvadosModel
234       return uuid.class
235     end
236     unless uuid.is_a? String
237       return nil
238     end
239     if uuid.match /^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/
240       return Collection
241     end
242     resource_class = nil
243
244     Rails.application.eager_load!
245     uuid.match @@UUID_REGEX do |re|
246       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
247     end
248     nil
249   end
250
251 end