X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/cbf0429a8a5edd2f8a8d9b01ce39334e07e56bf8..35c20b4ad8220131f7f6bad6b3806a7d28df3ef3:/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 9dfca2d941..95fd055d49 100644 --- a/services/api/app/models/arvados_model.rb +++ b/services/api/app/models/arvados_model.rb @@ -1,4 +1,5 @@ -require 'assign_uuid' +require 'has_uuid' + class ArvadosModel < ActiveRecord::Base self.abstract_class = true @@ -9,15 +10,17 @@ class ArvadosModel < ActiveRecord::Base attr_protected :modified_by_client_uuid attr_protected :modified_at after_initialize :log_start_state - before_create :ensure_permission_to_create - before_update :ensure_permission_to_update + before_save :ensure_permission_to_save + before_save :ensure_owner_uuid_is_permitted + before_save :ensure_ownership_path_leads_to_user + before_destroy :ensure_owner_uuid_is_permitted before_destroy :ensure_permission_to_destroy - before_create :update_modified_by_fields before_update :maybe_update_modified_by_fields after_create :log_create after_update :log_update after_destroy :log_destroy + after_find :convert_serialized_symbols_to_strings validate :ensure_serialized_attribute_type validate :normalize_collection_uuids validate :ensure_valid_uuids @@ -62,6 +65,24 @@ class ArvadosModel < ActiveRecord::Base self.columns.select { |col| col.name == attr.to_s }.first end + # Return nil if current user is not allowed to see the list of + # writers. Otherwise, return a list of user_ and group_uuids with + # write permission. (If not returning nil, current_user is always in + # the list because can_manage permission is needed to see the list + # of writers.) + def writable_by + unless (owner_uuid == current_user.uuid or + current_user.is_admin or + current_user.groups_i_can(:manage).index(owner_uuid)) + return nil + end + [owner_uuid, current_user.uuid] + permissions.collect do |p| + if ['can_write', 'can_manage'].index p.name + p.tail_uuid + end + end.compact.uniq + end + # Return a query with read permissions restricted to the union of of the # permissions of the members of users_list, i.e. if something is readable by # any user in users_list, it will be readable in the query returned by this @@ -90,9 +111,11 @@ class ArvadosModel < ActiveRecord::Base # A permission link exists ('write' and 'manage' implicitly include # 'read') from a member of users_list, or a group readable by users_list, # to this row, or to the owner of this row (see join() below). + permitted_uuids = "(SELECT head_uuid FROM links WHERE link_class='permission' AND tail_uuid IN (#{sanitized_uuid_list}))" + sql_conds += ["#{table_name}.owner_uuid in (?)", "#{table_name}.uuid in (?)", - "permissions.head_uuid IS NOT NULL"] + "#{table_name}.uuid IN #{permitted_uuids}"] sql_params += [uuid_list, user_uuids] if self == Link and users_list.any? @@ -105,7 +128,7 @@ class ArvadosModel < ActiveRecord::Base if self == Log and users_list.any? # Link head points to the object described by this row - or_object_uuid = ", #{table_name}.object_uuid" + sql_conds += ["#{table_name}.object_uuid IN #{permitted_uuids}"] # This object described by this row is owned by this user, or owned by a group readable by this user sql_conds += ["#{table_name}.object_owner_uuid in (?)"] @@ -118,15 +141,11 @@ class ArvadosModel < ActiveRecord::Base # user (the identity with authorization to read) # # Link class is 'permission' ('write' and 'manage' implicitly include 'read') - - joins("LEFT JOIN links permissions ON permissions.head_uuid in (#{table_name}.owner_uuid, #{table_name}.uuid #{or_object_uuid}) AND permissions.tail_uuid in (#{sanitized_uuid_list}) AND permissions.link_class='permission'") - .where(sql_conds.join(' OR '), *sql_params).uniq - + where(sql_conds.join(' OR '), *sql_params) else # At least one user is admin, so don't bother to apply any restrictions. self end - end def logged_attributes @@ -135,16 +154,75 @@ class ArvadosModel < ActiveRecord::Base protected - def ensure_permission_to_create - raise PermissionDeniedError unless permission_to_create + def ensure_ownership_path_leads_to_user + if new_record? or owner_uuid_changed? + uuid_in_path = {owner_uuid => true, uuid => true} + x = owner_uuid + while (owner_class = self.class.resource_class_for_uuid(x)) != User + begin + if x == uuid + # Test for cycles with the new version, not the DB contents + x = owner_uuid + elsif !owner_class.respond_to? :find_by_uuid + raise ActiveRecord::RecordNotFound.new + else + x = owner_class.find_by_uuid(x).owner_uuid + end + rescue ActiveRecord::RecordNotFound => e + errors.add :owner_uuid, "is not owned by any user: #{e}" + return false + end + if uuid_in_path[x] + if x == owner_uuid + errors.add :owner_uuid, "would create an ownership cycle" + else + errors.add :owner_uuid, "has an ownership cycle" + end + return false + end + uuid_in_path[x] = true + end + end + true end - def permission_to_create - current_user.andand.is_active + def ensure_owner_uuid_is_permitted + raise PermissionDeniedError if !current_user + if respond_to? :owner_uuid= + self.owner_uuid ||= current_user.uuid + end + if self.owner_uuid_changed? + if new_record? + return true + elsif current_user.uuid == self.owner_uuid or + current_user.can? write: self.owner_uuid + # current_user is, or has :write permission on, the new owner + else + 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}" + raise PermissionDeniedError + end + end + if new_record? + return true + elsif current_user.uuid == self.owner_uuid_was or + current_user.uuid == self.uuid or + current_user.can? write: self.owner_uuid_was + # current user is, or has :write permission on, the previous owner + return true + else + 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}" + raise PermissionDeniedError + end end - def ensure_permission_to_update - raise PermissionDeniedError unless permission_to_update + def ensure_permission_to_save + unless (new_record? ? permission_to_create : permission_to_update) + raise PermissionDeniedError + end + end + + def permission_to_create + current_user.andand.is_active end def permission_to_update @@ -161,24 +239,7 @@ class ArvadosModel < ActiveRecord::Base logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}" return false end - if self.owner_uuid_changed? - if current_user.uuid == self.owner_uuid or - current_user.can? write: self.owner_uuid - # current_user is, or has :write permission on, the new owner - else - 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}" - return false - end - end - if current_user.uuid == self.owner_uuid_was or - current_user.uuid == self.uuid or - current_user.can? write: self.owner_uuid_was - # current user is, or has :write permission on, the previous owner - return true - else - 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}" - return false - end + return true end def ensure_permission_to_destroy @@ -191,6 +252,7 @@ class ArvadosModel < ActiveRecord::Base def maybe_update_modified_by_fields update_modified_by_fields if self.changed? or self.new_record? + true end def update_modified_by_fields @@ -199,6 +261,39 @@ class ArvadosModel < ActiveRecord::Base self.modified_at = Time.now self.modified_by_user_uuid = current_user ? current_user.uuid : nil self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil + true + end + + def self.has_symbols? x + if x.is_a? Hash + x.each do |k,v| + return true if has_symbols?(k) or has_symbols?(v) + end + false + elsif x.is_a? Array + x.each do |k| + return true if has_symbols?(k) + end + false + else + (x.class == Symbol) + end + end + + def self.recursive_stringify x + if x.is_a? Hash + Hash[x.collect do |k,v| + [recursive_stringify(k), recursive_stringify(v)] + end] + elsif x.is_a? Array + x.collect do |k| + recursive_stringify k + end + elsif x.is_a? Symbol + x.to_s + else + x + end end def ensure_serialized_attribute_type @@ -210,13 +305,31 @@ class ArvadosModel < ActiveRecord::Base # developer. self.class.serialized_attributes.each do |colname, attr| if attr.object_class - unless self.attributes[colname].is_a? attr.object_class - self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}" + 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 end + def convert_serialized_symbols_to_strings + # ensure_serialized_attribute_type should prevent symbols from + # getting into the database in the first place. If someone managed + # to get them into the database (perhaps using an older version) + # we'll convert symbols to strings when loading from the + # database. (Otherwise, loading and saving an object with existing + # symbols in a serialized field will crash.) + 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])) + end + end + end + def foreign_key_attributes attributes.keys.select { |a| a.match /_uuid$/ } end