Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/declarative_authorization/authorization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ class Config
# - event details (hash)
attr_accessor :authorization_denied_callback

# Optional callback to wrap authorization check execution.
# Must return the result of executing the authorization check block.
attr_accessor :trace_authorization

def initialize
@authorization_denied_callback = nil
@trace_authorization = nil
end
end

Expand Down
11 changes: 10 additions & 1 deletion lib/declarative_authorization/controller/grape.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require File.dirname(__FILE__) + '/../authorization.rb'
require File.dirname(__FILE__) + '/dsl.rb'
require File.dirname(__FILE__) + '/runtime.rb'
require File.dirname(__FILE__) + '/observability.rb'

#
# This mixin can be used to add declarative authorization support to APIs built using Grape
Expand Down Expand Up @@ -31,6 +32,7 @@ def self.included(base) # :nodoc:

base.helpers do
include ::Authorization::Controller::Runtime
include ::Authorization::Controller::Observability

def authorization_engine
::Authorization::Engine.instance
Expand All @@ -43,7 +45,14 @@ def filter_access_filter # :nodoc:
# Acceessing route raises an exception when the response is a 405 MethodNotAllowed
return
end
unless allowed?("#{request.request_method} #{route.origin}")

action = "#{request.request_method} #{route.origin}"

allowed = trace_authorization(api: api_class&.name, action: action) do
allowed?(action)
end

unless allowed
if respond_to?(:permission_denied, true)
# permission_denied needs to render or redirect
send(:permission_denied)
Expand Down
18 changes: 18 additions & 0 deletions lib/declarative_authorization/controller/observability.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

#
# Observability support for declarative authorization
#
module Authorization
module Controller
module Observability
def trace_authorization(*args, &block)
if ::Authorization.config.trace_authorization
::Authorization.config.trace_authorization.call(*args, &block)
else
yield
end
end
end
end
end
8 changes: 7 additions & 1 deletion lib/declarative_authorization/controller/rails.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require File.dirname(__FILE__) + '/../authorization.rb'
require File.dirname(__FILE__) + '/dsl.rb'
require File.dirname(__FILE__) + '/runtime.rb'
require File.dirname(__FILE__) + '/observability.rb'

#
# Mixin to be added to rails controllers
Expand All @@ -18,6 +19,7 @@ def self.included(base) # :nodoc:
end

base.include Runtime
base.include Observability
end

module ClassMethods
Expand Down Expand Up @@ -280,7 +282,11 @@ def filter_resource_access(options = {})
protected

def filter_access_filter # :nodoc:
unless allowed?(action_name)
allowed = trace_authorization(controller: self.class.name, action: action_name) do
allowed?(action_name)
end

unless allowed
if respond_to?(:permission_denied, true)
# permission_denied needs to render or redirect
send(:permission_denied)
Expand Down
64 changes: 64 additions & 0 deletions test/grape_api_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -536,4 +536,68 @@ def test_context
assert !last_endpoint.authorized?
end
end

##################
class ObservabilityMocksAPI < MocksAPI
filter_access_to 'GET /observability_mocks/allowed_action', :require => :allow, :context => :observability_mocks
filter_access_to 'GET /observability_mocks/denied_action', :require => :deny, :context => :observability_mocks
define_action_methods :allowed_action, :denied_action
end

class ObservabilityAPITest < ApiTestCase
tests ObservabilityMocksAPI

def teardown
Authorization.config.trace_authorization = nil
end

def test_observability_callback_called_on_allowed_action
setup_trace_callback

request!(MockUser.new(:test_role), "/observability_mocks/allowed_action", observability_reader)
assert last_endpoint.authorized?
assert_equal 'ObservabilityMocksAPI', @callback_context[:api]
assert_equal 'GET /observability_mocks/allowed_action', @callback_context[:action]
assert_equal true, @callback_result
end

def test_observability_callback_called_on_denied_action
setup_trace_callback

request!(MockUser.new(:test_role), "/observability_mocks/denied_action", observability_reader)
assert !last_endpoint.authorized?
assert_equal 'ObservabilityMocksAPI', @callback_context[:api]
assert_equal 'GET /observability_mocks/denied_action', @callback_context[:action]
assert_equal false, @callback_result
end

def test_filter_works_without_trace_callback
request!(MockUser.new(:test_role), "/observability_mocks/allowed_action", observability_reader)
assert last_endpoint.authorized?

request!(MockUser.new(:test_role), "/observability_mocks/denied_action", observability_reader)
assert !last_endpoint.authorized?
end

private

def setup_trace_callback
Authorization.config.trace_authorization = lambda { |context, &block|
@callback_context = context
@callback_result = block.call
}
end

def observability_reader
reader = Authorization::Reader::DSLReader.new
reader.parse %{
authorization do
role :test_role do
has_permission_on :observability_mocks, :to => :allow
end
end
}
reader
end
end
end
64 changes: 64 additions & 0 deletions test/rails_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -575,3 +575,67 @@ def test_context
assert !@controller.authorized?
end
end

##################
class ObservabilityMocksController < MocksController
filter_access_to :allowed_action
filter_access_to :denied_action
define_action_methods :allowed_action, :denied_action
end

class ObservabilityControllerTest < ActionController::TestCase
tests ObservabilityMocksController

def teardown
Authorization.config.trace_authorization = nil
end

def test_observability_callback_called_on_allowed_action
setup_trace_callback

request!(MockUser.new(:test_role), "allowed_action", observability_reader)
assert @controller.authorized?
assert_equal 'ObservabilityMocksController', @callback_context[:controller]
assert_equal 'allowed_action', @callback_context[:action]
assert_equal true, @callback_result
end

def test_observability_callback_called_on_denied_action
setup_trace_callback

request!(MockUser.new(:test_role), "denied_action", observability_reader)
assert !@controller.authorized?
assert_equal 'ObservabilityMocksController', @callback_context[:controller]
assert_equal 'denied_action', @callback_context[:action]
assert_equal false, @callback_result
end

def test_filter_works_without_trace_callback
request!(MockUser.new(:test_role), "allowed_action", observability_reader)
assert @controller.authorized?

request!(MockUser.new(:test_role), "denied_action", observability_reader)
assert !@controller.authorized?
end

private

def setup_trace_callback
Authorization.config.trace_authorization = lambda { |context, &block|
@callback_context = context
@callback_result = block.call
}
end

def observability_reader
reader = Authorization::Reader::DSLReader.new
reader.parse %{
authorization do
role :test_role do
has_permission_on :observability_mocks, :to => :allowed_action
end
end
}
reader
end
end