module Scrivito # # @api public # class User # # Valid action verbs for the explicit rules. # @api public # VERBS = [ :create, :delete, :invite_to, :publish, :read, :write, ].freeze class << self # # Defines a new user. # # @api public # # @param [String] id The unique, unalterable id of the user. # The user id is used to associate the user with the corresponding CMS resources. # It is persisted in the CMS. # # @raise [Scrivito::ScrivitoError] if id is blank # @raise [Scrivito::ScrivitoError] if id is more than 64 characters long # # @yieldparam [Scrivito::UserDefinition] user object to define rules on # # @see Scrivito::UserDefinition#can_always # @see Scrivito::UserDefinition#can_never # @see Scrivito::UserDefinition#description # @see Scrivito::UserDefinition#restrict_obj_publish # @see Scrivito::UserDefinition#suggest_users # # @example # Scrivito::User.define('alice') do |user_definition| # user_definition.description { 'Alice Almighty' } # # user_definition.can_always(:read, :workspace) # user_definition.can_always(:write, :workspace) # user_definition.can_always(:publish, :workspace, 'You can always publish workspaces.') # end # # Scrivito::User.define('bob') do |user_definition| # user_definition.description('Bob Doe') # # user_definition.can_never(:create, :workspace, 'You are not allowed to create workspaces.') # user_definition.can_always(:read, :workspace) # # user_definition.restrict_obj_publish(using: :_obj_class) do |obj_class| # if obj_class == 'BlogPost' # false # else # 'You are not allowed to publish blog posts.' # end # end # end # def define(id, &block) assert_valid_id(id) define_user(id, &block) end # # Returns an anonymous system user who can always create workspaces, can always read, write, # publish, delete, and invite others to collaborate on any workspace. # @example Check whether the user may publish a particular object: # Scrivito::User.system_user.can_publish?(Obj.root) # # => true # # @example Get the notification messages for publishing restrictions. An empty array indicates that no restrictions exist. # Scrivito::User.system_user.restriction_messages_for(Obj.root) # # => [] # # @api public # @return [Scrivito::User] the system user # def system_user define_user { |user| user.is_admin! } end def unknown_user(id) new(id: id, explicit_rules: {}) end def find(id) if Configuration.find_user_proc user = Scrivito::Configuration.find_user_proc.call(id) assert_valid_user(user) user end end private def define_user(id = nil) user_definition = UserDefinition.new(id) yield user_definition if block_given? user_definition.user end def assert_valid_id(id) raise ScrivitoError.new('User id can not be blank') if id.blank? raise ScrivitoError.new('User id is too long (max length 64)') if id.length > 64 end def assert_valid_user(user) unless user.is_a?(User) || user.nil? raise ScrivitoError.new("Expected an instance of #{self} or nil, but got #{user.inspect}") end end end attr_reader :id, :explicit_rules, :description_proc, :suggest_users_proc, :restriction_set def initialize(options) @id = options[:id] @explicit_rules = options[:explicit_rules] @description_proc = options[:description_proc] @suggest_users_proc = options[:suggest_users_proc] @restriction_set = options[:restriction_set] @explicit_rules.each_key { |rule| assert_valid_verb(rule.second) } end def can?(verb, workspace) assert_valid_verb(verb) can_always?(verb, :workspace) || verb == :create && can_create? || verb == :read && can_read?(workspace) || can_as_owner?(verb, workspace) end def can_always?(verb, subject) assert_valid_verb(verb) @explicit_rules.has_key?([:can_always, verb, subject]) end def can_never?(verb, subject) assert_valid_verb(verb) @explicit_rules.has_key?([:can_never, verb, subject]) end def owner_of?(workspace) membership = workspace.memberships[self] membership ? membership.role == 'owner' : false end # # Checks whether the User may publish changes to a specific {Scrivito::BasicObj Obj}. # # @api public # @param [Scrivito::BasicObj] obj the object to be published # @return [Boolean] true if the user is allowed to publish the object, otherwise false # def can_publish?(obj) restriction_messages_for(obj).empty? end # # Checks whether the User may publish changes to an {Scrivito::BasicObj Obj} and returns # the message specified in a {Scrivito::UserDefinition#restrict_obj_publish} callback if they # may not. If the user may publish the CMS object, an empty array is returned. # # @api public # @param [Scrivito::BasicObj] obj the object to be published # @return [Array] Hints on why the user cannot publish the object # def restriction_messages_for(obj) assert_restrictions_applicable(obj) return [] if can_always?(:publish, :workspace) if obj.modification == Modification::EDITED base_revision_obj = obj.in_revision(obj.revision.workspace.base_revision) restriction_set.restriction_messages_for(obj) | restriction_set.restriction_messages_for(base_revision_obj) else restriction_set.restriction_messages_for(obj) end end def description @description ||= calculate_description end def suggest_users(input) if suggest_users_proc suggested_users = sandbox_suggest_user_proc(input) suggested_users.nil? ? [] : suggested_users else user = self.class.find(input) user ? [user] : [] end end # By convention, only the anonymous admin user has a +nil+ id. def system_user? id.nil? end private def can_create? !can_never?(:create, :workspace) end def can_read?(workspace) workspace.published? || can_as_owner?(:read, workspace) end def can_as_owner?(verb, workspace) workspace.is_a?(Workspace) && owner_of?(workspace) && !can_never?(verb, :workspace) end def calculate_description description_proc ? description_proc.call : id end def assert_valid_verb(verb) raise ScrivitoError.new("Invalid verb '#{verb}'") unless VERBS.include?(verb) end def assert_restrictions_applicable(obj) if obj.revision.base? raise ScrivitoError.new("The revision of #{obj.inspect} may not be a base revision") end end def sandbox_suggest_user_proc(input) suggest_users_proc.call(input) rescue Exception => e message = %{Method `suggest_users' of the user "#{id}" raised an error on input "#{input}"} Warning.error(message, e) nil end end end