X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/90209af8fa35bc99c9821db0c815404d1234ef31..441ef97e93a951b349356df96d8a6ef604c6cab7:/services/api/app/models/arvados_model.rb diff --git a/services/api/app/models/arvados_model.rb b/services/api/app/models/arvados_model.rb index 96dc85e1e4..d1a0bc5794 100644 --- a/services/api/app/models/arvados_model.rb +++ b/services/api/app/models/arvados_model.rb @@ -9,10 +9,6 @@ class ArvadosModel < ActiveRecord::Base include DbCurrentTime extend RecordFilters - attr_protected :created_at - attr_protected :modified_by_user_uuid - attr_protected :modified_by_client_uuid - attr_protected :modified_at after_initialize :log_start_state before_save :ensure_permission_to_save before_save :ensure_owner_uuid_is_permitted @@ -27,17 +23,16 @@ class ArvadosModel < ActiveRecord::Base after_find :convert_serialized_symbols_to_strings before_validation :normalize_collection_uuids before_validation :set_default_owner - validate :ensure_serialized_attribute_type validate :ensure_valid_uuids # Note: This only returns permission links. It does not account for # permissions obtained via user.is_admin or # user.uuid==object.owner_uuid. has_many(:permissions, + ->{where(link_class: 'permission')}, foreign_key: :head_uuid, class_name: 'Link', - primary_key: :uuid, - conditions: "link_class = 'permission'") + primary_key: :uuid) class PermissionDeniedError < StandardError def http_status @@ -51,6 +46,12 @@ class ArvadosModel < ActiveRecord::Base end end + class LockFailedError < StandardError + def http_status + 422 + end + end + class InvalidStateTransitionError < StandardError def http_status 422 @@ -77,6 +78,46 @@ class ArvadosModel < ActiveRecord::Base "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}" end + def self.permit_attribute_params raw_params + # strong_parameters does not provide security: permissions are + # implemented with before_save hooks. + # + # The following permit! is necessary even with + # "ActionController::Parameters.permit_all_parameters = true", + # because permit_all does not permit nested attributes. + if raw_params + serialized_attributes.each do |colname, coder| + param = raw_params[colname.to_sym] + if param.nil? + # ok + elsif !param.is_a?(coder.object_class) + raise ArgumentError.new("#{colname} parameter must be #{coder.object_class}, not #{param.class}") + elsif has_nonstring_keys?(param) + raise ArgumentError.new("#{colname} parameter cannot have non-string hash keys") + end + end + end + ActionController::Parameters.new(raw_params).permit! + end + + def initialize raw_params={}, *args + super(self.class.permit_attribute_params(raw_params), *args) + end + + # Reload "old attributes" for logging, too. + def reload(*args) + super + log_start_state + end + + def self.create raw_params={}, *args + super(permit_attribute_params(raw_params), *args) + end + + def update_attributes raw_params={}, *args + super(self.class.permit_attribute_params(raw_params), *args) + end + def self.selectable_attributes(template=:user) # Return an array of attribute name strings that can be selected # in the given template. @@ -155,6 +196,14 @@ class ArvadosModel < ActiveRecord::Base ["id", "uuid"] end + def self.limit_index_columns_read + # This method returns a list of column names. + # If an index request reads that column from the database, + # APIs that return lists will only fetch objects until reaching + # max_index_database_read bytes of data from those columns. + [] + end + # If current user can manage the object, return an array of uuids of # users and groups that have permission to write the object. The # first two elements are always [self.owner_uuid, current user's @@ -201,7 +250,8 @@ class ArvadosModel < ActiveRecord::Base # Check if any of the users are admin. If so, we're done. if users_list.select { |u| u.is_admin }.any? - return self + # Return existing relation with no new filters. + return where({}) end # Collect the UUIDs of the authorized users. @@ -458,6 +508,7 @@ class ArvadosModel < ActiveRecord::Base def update_modified_by_fields current_time = db_current_time + self.created_at = created_at_was || current_time self.updated_at = current_time self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid= self.modified_at = current_time @@ -466,6 +517,19 @@ class ArvadosModel < ActiveRecord::Base true end + def self.has_nonstring_keys? x + if x.is_a? Hash + x.each do |k,v| + return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v) + end + elsif x.is_a? Array + x.each do |v| + return true if has_nonstring_keys?(v) + end + end + false + end + def self.has_symbols? x if x.is_a? Hash x.each do |k,v| @@ -502,8 +566,15 @@ class ArvadosModel < ActiveRecord::Base end def self.where_serialized(colname, value) - sorted = deep_sort_hash(value) - where("#{colname.to_s} IN (?)", [sorted.to_yaml, SafeJSON.dump(sorted)]) + if value.empty? + # rails4 stores as null, rails3 stored as serialized [] or {} + sql = "#{colname.to_s} is null or #{colname.to_s} IN (?)" + sorted = value + else + sql = "#{colname.to_s} IN (?)" + sorted = deep_sort_hash(value) + end + where(sql, [sorted.to_yaml, SafeJSON.dump(sorted)]) end Serializer = { @@ -512,25 +583,18 @@ class ArvadosModel < ActiveRecord::Base } def self.serialize(colname, type) - super(colname, Serializer[type]) + coder = Serializer[type] + @serialized_attributes ||= {} + @serialized_attributes[colname.to_s] = coder + super(colname, coder) end - def ensure_serialized_attribute_type - # Specifying a type in the "serialize" declaration causes rails to - # raise an exception if a different data type is retrieved from - # the database during load(). The validation preventing such - # crash-inducing records from being inserted in the database in - # the first place seems to have been left as an exercise to the - # developer. - self.class.serialized_attributes.each do |colname, attr| - if attr.object_class - if self.attributes[colname].class != attr.object_class - self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}, not a #{self.attributes[colname].class.to_s}" - elsif self.class.has_symbols? attributes[colname] - self.errors.add colname.to_sym, "must not contain symbols: #{attributes[colname].inspect}" - end - end - end + def self.serialized_attributes + @serialized_attributes ||= {} + end + + def serialized_attributes + self.class.serialized_attributes end def convert_serialized_symbols_to_strings @@ -543,8 +607,8 @@ class ArvadosModel < ActiveRecord::Base self.class.serialized_attributes.each do |colname, attr| if self.class.has_symbols? attributes[colname] attributes[colname] = self.class.recursive_stringify attributes[colname] - self.send(colname + '=', - self.class.recursive_stringify(attributes[colname])) + send(colname + '=', + self.class.recursive_stringify(attributes[colname])) end end end