Skip to content

Commit

Permalink
Implement authorization
Browse files Browse the repository at this point in the history
  • Loading branch information
aldesantis committed Dec 19, 2016
1 parent da6405a commit 089d3f8
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 9 deletions.
80 changes: 72 additions & 8 deletions lib/pragma/operation/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,40 @@ class Base
511 => :network_authentication_required
}.freeze

def self.inherited(child)
child.class_eval do
before :setup_context
class << self
# Sets the policy to use for authorizing this operation.
#
# @param klass [Class] a subclass of +Pragma::Policy::Base+
def policy(klass) # rubocop:disable Style/TrivialAccessors
@policy = klass
end

# Returns the name of this operation.
#
# For instance, if the operation is called +API::V1::Post::Operation::Create+, returns
# +create+.
#
# @return [Symbol]
def operation_name
name.split('::').last
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
.tr('-', '_')
.downcase
.to_sym
end

around :handle_halt
def inherited(child)
child.class_eval do
before :setup_context

after :mark_result
after :consolidate_status
after :validate_status
after :set_default_status
around :handle_halt

after :mark_result
after :consolidate_status
after :validate_status
after :set_default_status
end
end
end

Expand Down Expand Up @@ -144,6 +168,46 @@ def head!(status)
fail Halt
end

# Returns the current user.
#
# This is just a shortcut for +context.current_user+.
#
# @return [Object]
def current_user
context.current_user
end

# Authorizes this operation on the provided resource.
#
# @param resource [Object] a resource
#
# @return [Boolean] whether the operation is authorized
def authorize(resource)
policy = self.class.instance_variable_get('@policy').new(
user: current_user,
resource: resource
)

policy.send("#{self.class.operation_name}?")
end

# Authorizes this operation on the provided resource. If the user is not authorized to
# perform the operation, responds with 403 Forbidden and an error body and halts the
# execution.
#
# @param resource [Object] a resource
def authorize!(resource)
return if authorize(resource)

respond_with!(
status: :forbidden,
resource: {
error_type: :forbidden,
error_message: 'You are not authorized to perform this operation.'
}
)
end

private

def setup_context
Expand Down
66 changes: 65 additions & 1 deletion spec/pragma/operation/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ def validate_params
end

let(:params) { { pong: 'HELLO' } }
let(:context) { operation.call(params: params) }
let(:current_user) { nil }

let(:context) { operation.call(params: params, current_user: current_user) }

it 'converts numeric status codes into symbols' do
expect(context.status).to eq(:ok)
Expand Down Expand Up @@ -74,6 +76,12 @@ def call
end
end

describe '.operation_name' do
it 'returns the name of the operation' do
expect(described_class.operation_name).to eq(:base)
end
end

describe '#respond_with!' do
let(:params) { { pong: '' } }

Expand Down Expand Up @@ -125,4 +133,60 @@ def validate_params
end
end
end

describe '#authorize!' do
let(:operation) do
Class.new(described_class) do
# rubocop:disable Lint/ParenthesesAsGroupedExpression
policy (Class.new do
def initialize(user:, resource:)
@user = user
@resource = resource
end

def create?
@user.admin? # rubocop:disable RSpec/InstanceVariable
end
end)
# rubocop:enable Lint/ParenthesesAsGroupedExpression

class << self
def name
'API::V1::Page::Operation::Create'
end
end

def call
resource = OpenStruct.new
authorize! resource

respond_with status: :ok, resource: resource
end
end
end

context 'when the user is authorized' do
let(:current_user) { OpenStruct.new(admin?: true) }

it 'runs the operation' do
expect(context).to be_success
end
end

context 'when the user is not authorized' do
let(:current_user) { OpenStruct.new(admin?: false) }

it 'halts the execution' do
expect(context).not_to be_success
end

it 'responds with the 403 Forbidden status code' do
expect(context.status).to eq(:forbidden)
end

it 'responds with error details' do
expect(context.resource[:error_type]).to eq(:forbidden)
end
end
end
end

0 comments on commit 089d3f8

Please sign in to comment.