Class: Verifica::Authorizer

Inherits:
Object
  • Object
show all
Defined in:
lib/verifica/authorizer.rb

Overview

Authorizer is the heart of Verifica. It’s an isolated container with no global state which has a list of resource types registered with their companion AclProviders.

Authorizer pairs great with Dependency Injection or can be configured and passed in a way that is compatible with your framework.

Examples:

require "verifica"

User = Struct.new(:id, :role, keyword_init: true) do
  # Verifica expects each security subject to respond to #subject_id, #subject_type, and #subject_sids
  alias_method :subject_id, :id
  def subject_type = :user

  def subject_sids(**)
    role == "root" ? ["root"] : ["authenticated", "user:#{id}"]
  end
end

Video = Struct.new(:id, :author_id, :public, keyword_init: true) do
  # Verifica expects each secured resource to respond to #resource_id, and #resource_type
  alias_method :resource_id, :id
  def resource_type = :video
end

video_acl_provider = lambda do |video, **|
  Verifica::Acl.build do |acl|
    acl.allow "root", [:read, :write, :delete, :comment]
    acl.allow "user:#{video.author_id}", [:read, :write, :delete, :comment]

    if video.public
      acl.allow "authenticated", [:read, :comment]
    end
  end
end

authorizer = Verifica.authorizer do |config|
  config.register_resource :video, [:read, :write, :delete, :comment], video_acl_provider
end

public_video = Video.new(id: 1, author_id: 1000, public: true)
private_video = Video.new(id: 2, author_id: 1000, public: false)

superuser = User.new(id: 777, role: "root")
video_author = User.new(id: 1000, role: "user")
other_user = User.new(id: 2000, role: "user")

authorizer.authorized?(superuser, private_video, :delete) # => true
authorizer.authorized?(video_author, private_video, :delete) # => true
authorizer.authorized?(other_user, private_video, :read) # => false
authorizer.authorized?(other_user, public_video, :comment) # => true

begin
  # raises Verifica::AuthorizationError: Authorization FAILURE. Subject 'user' id='2000'. Resource 'video' id='1'. Action 'write'
  authorizer.authorize(other_user, public_video, :write)
rescue Verifica::AuthorizationError => e
  e.explain # => Long-form explanation of why action is not authorized, your debugging friend
end

# #authorization_result returns a special object with a bunch of useful info
auth_result = authorizer.authorization_result(superuser, private_video, :delete)
auth_result.success? # => true
auth_result.subject_id # => 777
auth_result.resource_type # => :video
auth_result.action # => :delete
auth_result.allowed_actions # => [:read, :write, :delete, :comment]
auth_result.explain # => Long-form explanation of why action is authorized

See Also:

Instance Method Summary collapse

Constructor Details

#initialize(resource_configs) ⇒ Authorizer

Note:

Use Verifica.authorizer instead of this constructor directly

Returns a new instance of Authorizer.



34
35
36
37
# File 'lib/verifica/authorizer.rb', line 34

def initialize(resource_configs)
  @resources = index_resources(resource_configs).freeze
  freeze
end

Instance Method Details

#allowed_actions(subject, resource, **context) ⇒ Array<Symbol>

Returns array of actions allowed for subject or empty array if none.

Parameters:

  • subject (Object)

    subject of the authorization (e.g. current user, external service)

  • resource (Object)

    resource to authorize for, should respond to #resource_type

  • context (Hash)

    arbitrary keyword arguments to forward to subject.subject_sids and acl_provider.call

Returns:

  • (Array<Symbol>)

    array of actions allowed for subject or empty array if none

Raises:

  • (Error)

    if resource.resource_type isn’t registered in self

See Also:



122
123
124
125
126
# File 'lib/verifica/authorizer.rb', line 122

def allowed_actions(subject, resource, **context)
  acl = resource_acl(resource, **context)
  sids = Verifica.subject_sids(subject)
  acl.allowed_actions(sids)
end

#authorization_result(subject, resource, action, **context) ⇒ AuthorizationResult

The same as #authorize but returns a special result object instead of rising an exception

Parameters:

  • subject (Object)

    subject of the authorization (e.g. current user, external service)

  • resource (Object)

    resource to authorize for, should respond to #resource_type

  • action (Symbol, String)

    action that subject attempts to perform on the resource

  • context (Hash)

    arbitrary keyword arguments to forward to subject.subject_sids and acl_provider.call

Returns:

Raises:

  • (Error)

    if resource.resource_type isn’t registered in self



100
101
102
103
104
105
106
107
108
109
# File 'lib/verifica/authorizer.rb', line 100

def authorization_result(subject, resource, action, **context)
  action = action.to_sym
  possible_actions = config_by_resource(resource).possible_actions
  unless possible_actions.include?(action)
    raise Error, "'#{action}' action is not registered as possible for '#{resource.resource_type}' resource"
  end

  acl = resource_acl(resource, **context)
  AuthorizationResult.new(subject, resource, action, acl, **context)
end

#authorize(subject, resource, action, **context) ⇒ AuthorizationResult

Checks the authorization of a subject to perform an action on a resource

  • The subject is asked for its Security Identifiers (SIDs) by subject.subject_sids

  • The resource is asked for its type by resource.resource_type

  • ACL provider registered for this resource type is asked for Verifica::Acl by #call(resource, **context)

  • ACL is checked whether the action is allowed for the subject SIDs

Examples:

def show
  post = Post.find(params[:id])
  authorizer.authorize(current_user, post, :read)

  render json: post
end

Parameters:

  • subject (Object)

    subject of the authorization (e.g. current user, external service)

  • resource (Object)

    resource to authorize for, should respond to #resource_type

  • action (Symbol, String)

    action that subject attempts to perform on the resource

  • context (Hash)

    arbitrary keyword arguments to forward to subject.subject_sids and acl_provider.call

Returns:

  • (AuthorizationResult)

    authorization result with all details if authorization is successful

Raises:

  • (AuthorizationError)

    if subject isn’t authorized to perform action on the given resource

  • (Error)

    if resource.resource_type isn’t registered in self

See Also:



67
68
69
70
71
72
# File 'lib/verifica/authorizer.rb', line 67

def authorize(subject, resource, action, **context)
  result = authorization_result(subject, resource, action, **context)
  raise AuthorizationError, result if result.failure?

  result
end

#authorized?(subject, resource, action, **context) ⇒ Boolean

The same as #authorize but returns true/false instead of rising an exception

Parameters:

  • subject (Object)

    subject of the authorization (e.g. current user, external service)

  • resource (Object)

    resource to authorize for, should respond to #resource_type

  • action (Symbol, String)

    action that subject attempts to perform on the resource

  • context (Hash)

    arbitrary keyword arguments to forward to subject.subject_sids and acl_provider.call

Returns:

  • (Boolean)

    true if action on resource is authorized for subject

Raises:

  • (Error)

    if resource.resource_type isn’t registered in self



85
86
87
# File 'lib/verifica/authorizer.rb', line 85

def authorized?(subject, resource, action, **context)
  authorization_result(subject, resource, action, **context).success?
end

#resource_acl(resource, **context) ⇒ Acl

Returns Access Control List for resource.

Parameters:

  • context (Hash)

    arbitrary keyword arguments to forward to acl_provider.call

  • resource (Object)

    resource to authorize for, should respond to #resource_type

Returns:

  • (Acl)

    Access Control List for resource

Raises:

  • (Error)

    if resource_type isn’t registered in self

  • (Error)

    if ACL provider for this resource type doesn’t respond to #call(resource, **) with Verifica::Acl object

See Also:



168
169
170
171
172
173
174
175
176
177
178
# File 'lib/verifica/authorizer.rb', line 168

def resource_acl(resource, **context)
  config = config_by_resource(resource)
  acl = config.acl_provider.call(resource, **context)
  # trade-off flexibility to increase robustness here by requiring a specific type
  unless acl.is_a?(Verifica::Acl)
    type = resource.resource_type
    raise Error, "'#{type}' resource acl_provider should respond to #call with Acl object but got '#{acl.class}'"
  end

  acl
end

#resource_config(resource_type) ⇒ ResourceConfiguration

Returns configuration for resource_type.

Parameters:

  • resource_type (Symbol, String)

    type of the resource

Returns:

Raises:

  • (Error)

    if resource_type isn’t registered in self

See Also:



137
138
139
140
141
142
143
144
145
# File 'lib/verifica/authorizer.rb', line 137

def resource_config(resource_type)
  resource_type = resource_type.to_sym
  config = @resources[resource_type]
  if config.nil?
    raise Error, "Unknown resource '#{resource_type}'. Did you forget to register this resource type?"
  end

  config
end

#resource_type?(resource_type) ⇒ Boolean

Returns true if resource_type is registered in self.

Parameters:

  • resource_type (Symbol, String)

    type of the resource

Returns:

  • (Boolean)

    true if resource_type is registered in self

See Also:



154
155
156
# File 'lib/verifica/authorizer.rb', line 154

def resource_type?(resource_type)
  @resources.key?(resource_type.to_sym)
end