Class: Shamu::Security::Policy

Inherits:
Object
  • Object
show all
Includes:
Roles
Defined in:
lib/shamu/security/policy.rb

Overview

...

Examples:

class UserPolicy < Shamu::Security::Policy

  role :admin, inherits: :manager
  role :manager
  role :user

  private

    def permissions
      alias_action :email, to: :contact

      permit :contact, UserEntity if in_role?( :manager )
      permit :email, UserEntity do |user|
        user.public_profile?
      end
    end
end

principal = Shamu::Security::Principal.new( user_id: user.id )
policy = UserPolicy.new(
  principal: principal,
  roles: roles_service.roles_for( principal )
  )

if policy.permit? :contact, user
  mail_to user
end

Direct Known Subclasses

ActiveRecordPolicy, NoPolicy

Dependencies collapse

DSL collapse

Instance Method Summary collapse

Methods included from Roles

expand_roles, role, role_defined?, roles

Constructor Details

#initialize(principal: nil, roles: nil, related_user_ids: nil) ⇒ Policy

Returns a new instance of Policy.



59
60
61
62
63
# File 'lib/shamu/security/policy.rb', line 59

def initialize( principal: nil, roles: nil, related_user_ids: nil )
  @principal        = principal || Principal.new
  @roles            = roles || []
  @related_user_ids = Array.wrap( related_user_ids )
end

Instance Attribute Details

#principalPrincipal

Returns principal holding user identity and access credentials.

Returns:

  • (Principal)

    principal holding user identity and access credentials.



45
46
47
# File 'lib/shamu/security/policy.rb', line 45

def principal
  @principal
end

may act on behalf of.

Returns:

  • (Array<Integer>)

    additional user ids that the #principal is



54
55
56
# File 'lib/shamu/security/policy.rb', line 54

def related_user_ids
  @related_user_ids
end

#rolesArray<Roles>

Returns roles that have been granted to the #principal.

Returns:



49
50
51
# File 'lib/shamu/security/policy.rb', line 49

def roles
  @roles
end

Instance Method Details

#add_rule(actions, resource, result, &block) ⇒ Object



327
328
329
# File 'lib/shamu/security/policy.rb', line 327

def add_rule( actions, resource, result, &block )
  rules.unshift PolicyRule.new( expand_aliases( actions ), resource, result, block )
end

#alias_action(*actions, to: fail)

This method returns an undefined value.

Add an action alias so that granting the alias will result in permits for any of the listed actions.

Examples:

alias_action :show, :list, to: :read
permit :read, :stuff

permit?( :show, :stuff )  # => :yes
permit?( :list, :stuff )  # => :yes
permit?( :read, :stuff )  # => :yes
permit?( :write, :stuff ) # => false

Parameters:

  • actions (Array<Symbol>)

    to alias.

  • to (Symbol) (defaults to: fail)

    the action that should permit all the listed aliases.



287
288
289
290
# File 'lib/shamu/security/policy.rb', line 287

def alias_action( *actions, to: fail ) # bug in rubocop chokes on trailing required keyword
  aliases[to] ||= []
  aliases[to] |= actions
end

#authorize!(action, resource, additional_context = nil) ⇒ resource

Authorize the given action on the given resource. If it is not permitted then an exception is raised.

Parameters:

  • action (Symbol)

    to perform.

  • resource (Object)

    the resource the action will be performed on.

  • additional_context (Object) (defaults to: nil)

    that the policy may consider.

Returns:

Raises:



71
72
73
74
75
76
77
78
79
# File 'lib/shamu/security/policy.rb', line 71

def authorize!( action, resource, additional_context = nil )
  return resource if permit?( action, resource, additional_context ) == :yes

  fail Security::AccessDeniedError,
       action: action,
       resource: resource,
       additional_context: additional_context,
       principal: principal
end

#deny(*actions) {|resource, additional_context| ... }

This method returns an undefined value.

Explicitly deny an action previously granted with #permit.

Parameters:

  • actions (Array<Symbol>)

    to be permitted.

  • resource (Object)

    to perform the action on or the Class of instances to permit the action on.

Yields:

Yield Parameters:

  • resource (Object)

    instance or Class offered to #permit? that the requested action is to be performed on.

  • additional_context (Object)

    offered to #permit?.

Yield Returns:

  • (Boolean)

    true to deny the action.



244
245
246
247
# File 'lib/shamu/security/policy.rb', line 244

def deny( *actions, &block )
  resource, actions = extract_resource( actions )
  add_rule( actions, resource, false, &block )
end

#dsl_resourceObject



318
319
320
# File 'lib/shamu/security/policy.rb', line 318

def dsl_resource
  @dsl_resource || fail( "Provide a `resource` argument or use a #resource block to declare the protected resource." ) # rubocop:disable Metrics/LineLength
end

#expand_alias_into(candidate, expanded) ⇒ Object



340
341
342
343
344
345
346
347
348
349
# File 'lib/shamu/security/policy.rb', line 340

def expand_alias_into( candidate, expanded )
  return unless mapped = aliases[candidate]

  mapped.each do |action|
    next if expanded.include? action

    expanded << action
    expand_alias_into( action, expanded )
  end
end

#expand_aliases(actions) ⇒ Object



331
332
333
334
335
336
337
338
# File 'lib/shamu/security/policy.rb', line 331

def expand_aliases( actions )
  expanded = actions.dup
  actions.each do |action|
    expand_alias_into( action, expanded )
  end

  expanded
end

#extract_resource(actions) ⇒ Object



322
323
324
325
# File 'lib/shamu/security/policy.rb', line 322

def extract_resource( actions )
  resource = actions.last.is_a?( Symbol ) ? dsl_resource : actions.pop
  [ resource, actions ]
end

#fail_on_active_record_check(resource) ⇒ Object



351
352
353
354
355
356
357
358
# File 'lib/shamu/security/policy.rb', line 351

def fail_on_active_record_check( resource )
  return unless resource
  return unless defined? ActiveRecord

  if resource.is_a?( ActiveRecord::Base ) || ( resource.is_a?( Class ) && resource < ActiveRecord::Base )
    fail NoActiveRecordPolicyChecksError
  end
end

#in_role?(*roles) ⇒ Boolean

Returns true if the #principal has been granted one of the given roles.

Parameters:

  • roles (Array<Symbol>)

    to check.

Returns:

  • (Boolean)

    true if the #principal has been granted one of the given roles.



133
134
135
# File 'lib/shamu/security/policy.rb', line 133

def in_role?( *roles )
  ( principal_roles & roles ).any?
end

#is_principal?(id) ⇒ Boolean

ids on the principal.

Parameters:

  • id (Integer)

    of the candidate user.

Returns:

  • (Boolean)

    true if the given id is one of the authorized user



150
151
152
# File 'lib/shamu/security/policy.rb', line 150

def is_principal?( id ) # rubocop:disable Style/PredicateName
  principal.try( :user_id ) == id || related_user_ids.include?( id )
end

#permissions

This method returns an undefined value.

Hook to be overridden by a derived class to define the set of rules that #permit? should consider when evaluating the #principal's permissions on a resource.

Rules defined in the permissions block are evaluated in reverse order such that the last matching #permit or #deny will determine the permission.

If no rules match, the permission is denied.

Examples:

def permissions
  permit :read, UserEntity

  deny :read, UserEntity do |user|
    user.protected_account? && !in_role( :admin )
  end
end


180
181
182
183
184
185
186
187
188
189
190
# File 'lib/shamu/security/policy.rb', line 180

def permissions
  if respond_to?( :anonymous_permissions, true ) && respond_to?( :authenticated_permissions, true )
    if principal.user_id
      authenticated_permissions
    else
      anonymous_permissions
    end
  else
    fail IncompleteSetupError, "Permissions have not been defined. Add a private `permissions` method to #{ self.class.name }" # rubocop:disable Metrics/LineLength
  end
end

#permit(*actions) {|resource, additional_context| ... }

This method returns an undefined value.

permit :read, UserEntity permit :show, :dashboard permit :update, UserEntity do |user| user.id == principal.user_id end permit :destroy, UserEntity do |user, additional_context| in_role?( :admin ) && additional_context[:custom_data] == :safe end

Parameters:

  • actions (Array<Symbol>)

    to be permitted.

  • resource (Object)

    to perform the action on or the Class of instances to permit the action on.

Yields:

Yield Parameters:

  • resource (Object)

    instance or Class offered to #permit? that the requested action is to be performed on.

  • additional_context (Object)

    offered to #permit?.

Yield Returns:

  • (:yes, :maybe, false)

    see #permit?.



228
229
230
231
232
233
# File 'lib/shamu/security/policy.rb', line 228

def permit( *actions, &block )
  result = @when_elevated ? :maybe : :yes
  resource, actions = extract_resource( actions )

  add_rule( actions, resource, result, &block )
end

#permit?(action, resource, additional_context = nil) ⇒ :yes, ...

Determines if the given action may be performed on the given resource.

Parameters:

  • action (Symbol)

    to perform.

  • resource (Object)

    the resource the action will be performed on.

  • additional_context (Object) (defaults to: nil)

    that the policy may consider.

Returns:

  • (:yes, :maybe, false)

    a truthy value if permitted, otherwise false. The truthy value depends on the certainty of the policy. A value of :yes or true indicates the action is always permitted. A value of :maybe indicates the action is permitted but the user may need to present additional credentials such as logging on this session or entering a TFA code.



93
94
95
96
97
98
99
100
101
102
103
# File 'lib/shamu/security/policy.rb', line 93

def permit?( action, resource, additional_context = nil )
  fail_on_active_record_check( resource )

  rules.each do |rule|
    next unless rule.match?( action, resource, additional_context )

    return rule.result
  end

  false
end

#resource(resource) ⇒ Object

Define the resource to #permit or #deny access to. Inside the block you can omit the resource param on DSL methods that expect it.

Examples:

resource UserEntity do
  permit :read
  permit :update do |user|
    user.id == principal.user_id
  end

  permit :chop, OtherKindOfEntity
end


307
308
309
310
311
312
313
# File 'lib/shamu/security/policy.rb', line 307

def resource( resource )
  last_resource = @dsl_resource
  @dsl_resource = resource
  yield
ensure
  @dsl_resource = last_resource
end

#when_elevated(&block)

This method returns an undefined value.

Only #authorize! the permissions defined in the given block when the #principal has elevated this session by providing their credentials.

Permissions defined in the block will yield a :maybe result when queried via #permit? and will raise an AccessDeniedError when an #authorize! check is enforced.

This allows you to enable/disable UX in response to what a user should be capable of doing but wait to actually allow it until they have offered their credentials.



263
264
265
266
267
268
# File 'lib/shamu/security/policy.rb', line 263

def when_elevated( &block )
  current = @when_elevated
  @when_elevated = true
  yield
  @when_elevated = current
end