active_entry 1.2.4 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +275 -141
- data/app/controllers/concerns/active_entry/concern.rb +57 -0
- data/app/helpers/active_entry/view_helper.rb +20 -0
- data/lib/active_entry.rb +7 -88
- data/lib/active_entry/base.rb +66 -0
- data/lib/active_entry/errors.rb +44 -61
- data/lib/active_entry/generators.rb +4 -0
- data/lib/active_entry/policy_finder.rb +25 -0
- data/lib/active_entry/railtie.rb +6 -6
- data/lib/active_entry/rspec.rb +56 -0
- data/lib/active_entry/version.rb +1 -1
- data/lib/generators/active_entry/install/USAGE +8 -0
- data/lib/generators/active_entry/install/install_generator.rb +9 -0
- data/lib/generators/active_entry/install/templates/application_policy.rb +7 -0
- data/lib/generators/policy/USAGE +8 -0
- data/lib/generators/policy/policy_generator.rb +7 -0
- data/lib/generators/policy/templates/policy.rb +53 -0
- data/lib/generators/rspec/USAGE +8 -0
- data/lib/generators/rspec/policy_generator.rb +11 -0
- data/lib/generators/rspec/templates/policy_spec.rb +5 -0
- metadata +19 -5
- data/lib/active_entry/controller_methods.rb +0 -99
@@ -0,0 +1,57 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module ActiveEntry
|
4
|
+
module ControllerConcern
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
# Methods .authenticate_now!, .authorize_now!, and .pass_now!
|
9
|
+
[:authenticate, :authorize, :pass].each do |method_name|
|
10
|
+
define_method "#{method_name}_now!" do
|
11
|
+
before_action do
|
12
|
+
args = {}
|
13
|
+
instance_variables.collect{ |v| v.to_s.remove("@").to_sym }.each do |name|
|
14
|
+
value = instance_variable_get ["@", name].join
|
15
|
+
next if value.nil?
|
16
|
+
args[name] = value
|
17
|
+
end
|
18
|
+
send "#{method_name}!", **args
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def verify_authentication!
|
24
|
+
after_action do
|
25
|
+
raise AuthenticationNotPerformedError.new(self.class, action_name) unless @__authentication_done
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def verify_authorization!
|
30
|
+
after_action do
|
31
|
+
raise AuthorizationNotPerformedError.new(self.class, action_name) unless @__authorization_done
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def pass! **args
|
37
|
+
authenticate! **args
|
38
|
+
authorize! **args
|
39
|
+
end
|
40
|
+
|
41
|
+
def authenticate! **args
|
42
|
+
policy_class::Authentication.pass! action_name, **args unless @__authentication_done
|
43
|
+
@__authentication_done = true
|
44
|
+
end
|
45
|
+
|
46
|
+
def authorize! **args
|
47
|
+
policy_class::Authorization.pass! action_name, **args unless @__authorization_done
|
48
|
+
@__authorization_done = true
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def policy_class
|
54
|
+
PolicyFinder.policy_for self.class.name.remove("Controller")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ActiveEntry
|
2
|
+
module ViewHelper
|
3
|
+
def authorized_for? controller_name, action, **args
|
4
|
+
controller_name = controller_name.to_s.camelize.remove "Controller"
|
5
|
+
policy = ActiveEntry::PolicyFinder.policy_for controller_name
|
6
|
+
policy::Authorization.pass? action, **args
|
7
|
+
end
|
8
|
+
|
9
|
+
def link_to_if_authorized name = nil, options = nil, html_options = nil, &block
|
10
|
+
url = url_for options
|
11
|
+
method = options&.is_a?(Hash) && options[:method] ? options[:method].to_s.upcase : "GET"
|
12
|
+
|
13
|
+
recognized_path = Rails.application.routes.recognize_path(url, method: method)
|
14
|
+
|
15
|
+
authorized = authorized_for? recognized_path[:controller], recognized_path[:action]
|
16
|
+
|
17
|
+
link_to name, options, html_options, &block if authorized
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/active_entry.rb
CHANGED
@@ -1,94 +1,13 @@
|
|
1
1
|
require "active_entry/version"
|
2
|
+
require "active_entry/generators"
|
2
3
|
require "active_entry/errors"
|
3
|
-
require "active_entry/
|
4
|
-
require "active_entry/
|
4
|
+
require "active_entry/base"
|
5
|
+
require "active_entry/policy_finder"
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
def verify_authentication!
|
9
|
-
raise ActiveEntry::AuthenticationNotPerformedError unless @_authentication_done == true
|
10
|
-
end
|
11
|
-
|
12
|
-
# Authenticates the user
|
13
|
-
def authenticate!
|
14
|
-
general_decision_maker_method_name = :authenticated?
|
15
|
-
scoped_decision_maker_method_name = [action_name, :authenticated?].join("_").to_sym
|
16
|
-
|
17
|
-
general_decision_maker_defined = respond_to? general_decision_maker_method_name, true
|
18
|
-
scoped_decision_maker_defined = respond_to? scoped_decision_maker_method_name, true
|
19
|
-
|
20
|
-
# Check if a scoped decision maker method is defined and use it over
|
21
|
-
# general decision maker method.
|
22
|
-
decision_maker_to_use = scoped_decision_maker_defined ? scoped_decision_maker_method_name : general_decision_maker_method_name
|
23
|
-
|
24
|
-
# Raise an error if the #authenticate? action isn't defined.
|
25
|
-
#
|
26
|
-
# This ensures that you actually do authentication in your controller.
|
27
|
-
if !scoped_decision_maker_defined && !general_decision_maker_defined
|
28
|
-
raise ActiveEntry::AuthenticationDecisionMakerMissingError
|
29
|
-
end
|
30
|
-
|
31
|
-
error = {}
|
32
|
-
|
33
|
-
if method(decision_maker_to_use).arity > 0
|
34
|
-
is_authenticated = send decision_maker_to_use, error
|
35
|
-
else
|
36
|
-
is_authenticated = send decision_maker_to_use
|
37
|
-
end
|
38
|
-
|
39
|
-
# Tell #verify_authentication! that authentication
|
40
|
-
# has been performed.
|
41
|
-
@_authentication_done = true
|
42
|
-
|
43
|
-
# If the authenticated? method returns not true
|
44
|
-
# it raises the ActiveEntry::NotAuthenticatedError.
|
45
|
-
#
|
46
|
-
# Use the .rescue_from method from ActionController::Base
|
47
|
-
# to catch the exception and show the user a proper error message.
|
48
|
-
raise ActiveEntry::NotAuthenticatedError.new(error) unless is_authenticated == true
|
49
|
-
end
|
7
|
+
require_relative "../app/controllers/concerns/active_entry/concern" if defined?(ActionController::Base)
|
8
|
+
require 'active_entry/railtie' if defined?(Rails)
|
50
9
|
|
51
|
-
|
52
|
-
def verify_authorization!
|
53
|
-
raise ActiveEntry::AuthorizationNotPerformedError unless @_authorization_done == true
|
54
|
-
end
|
55
|
-
|
56
|
-
# Authorizes the user.
|
57
|
-
def authorize!
|
58
|
-
general_decision_maker_method_name = :authorized?
|
59
|
-
scoped_decision_maker_method_name = [action_name, :authorized?].join("_").to_sym
|
10
|
+
require "active_support/inflector"
|
60
11
|
|
61
|
-
|
62
|
-
scoped_decision_maker_defined = respond_to? scoped_decision_maker_method_name, true
|
63
|
-
|
64
|
-
# Check if a scoped decision maker method is defined and use it over
|
65
|
-
# general decision maker method.
|
66
|
-
decision_maker_to_use = scoped_decision_maker_defined ? scoped_decision_maker_method_name : general_decision_maker_method_name
|
67
|
-
|
68
|
-
# Raise an error if the #authorize? action isn't defined.
|
69
|
-
#
|
70
|
-
# This ensures that you actually do authorization in your controller.
|
71
|
-
if !scoped_decision_maker_defined && !general_decision_maker_defined
|
72
|
-
raise ActiveEntry::AuthorizationDecisionMakerMissingError
|
73
|
-
end
|
74
|
-
|
75
|
-
error = {}
|
76
|
-
|
77
|
-
if method(decision_maker_to_use).arity > 0
|
78
|
-
is_authorized = send(decision_maker_to_use, error)
|
79
|
-
else
|
80
|
-
is_authorized = send(decision_maker_to_use)
|
81
|
-
end
|
82
|
-
|
83
|
-
# Tell #verify_authorization! that authorization
|
84
|
-
# has been performed.
|
85
|
-
@_authorization_done = true
|
86
|
-
|
87
|
-
# If the authorized? method does not return true
|
88
|
-
# it raises the ActiveEntry::NotAuthorizedError
|
89
|
-
#
|
90
|
-
# Use the .rescue_from method from ActionController::Base
|
91
|
-
# to catch the exception and show the user a proper error message.
|
92
|
-
raise ActiveEntry::NotAuthorizedError.new(error) unless is_authorized == true
|
93
|
-
end
|
12
|
+
module ActiveEntry
|
94
13
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module ActiveEntry
|
2
|
+
class Base
|
3
|
+
class Authentication < Base
|
4
|
+
AUTH_ERROR = NotAuthenticatedError
|
5
|
+
|
6
|
+
def self.pass! method_name, **args
|
7
|
+
new(method_name, **args).pass!
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.pass? method_name, **args
|
11
|
+
new(method_name, **args).pass?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Authorization < Base
|
16
|
+
AUTH_ERROR = NotAuthorizedError
|
17
|
+
|
18
|
+
def self.pass! method_name, **args
|
19
|
+
new(method_name, **args).pass!
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.pass? method_name, **args
|
23
|
+
new(method_name, **args).pass?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize method_name, **args
|
28
|
+
@_method_name_to_entrify = method_name
|
29
|
+
@_args = args
|
30
|
+
@_args.each { |name, value| instance_variable_set ["@", name].join, value }
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def pass! method_name, **args
|
35
|
+
Authentication.pass! method_name, **args
|
36
|
+
Authorization.pass! method_name, **args
|
37
|
+
end
|
38
|
+
|
39
|
+
def pass? method_name, **args
|
40
|
+
Authentication.pass? method_name, **args
|
41
|
+
Authorization.pass? method_name, **args
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def pass!
|
47
|
+
pass? or raise self.class::AUTH_ERROR.new(@error, @_method_name_to_entrify, @_args)
|
48
|
+
end
|
49
|
+
|
50
|
+
def pass?
|
51
|
+
decision_maker_method.call == true
|
52
|
+
end
|
53
|
+
|
54
|
+
def success
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def decision_maker_method
|
61
|
+
decision_maker_method_name = [@_method_name_to_entrify, "?"].join
|
62
|
+
raise DecisionMakerMethodNotDefinedError.new(self.class, decision_maker_method_name) unless respond_to?(decision_maker_method_name)
|
63
|
+
method decision_maker_method_name
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/active_entry/errors.rb
CHANGED
@@ -1,82 +1,65 @@
|
|
1
|
-
# @author Tobias Feistmantl
|
2
1
|
module ActiveEntry
|
3
|
-
|
4
|
-
# Other, more specific, errors inherit from this one.
|
5
|
-
#
|
6
|
-
# @raise [AuthorizationError]
|
7
|
-
# if something generic is happening.
|
8
|
-
class AuthorizationError < StandardError
|
2
|
+
class Error < StandardError
|
9
3
|
end
|
10
4
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
5
|
+
class AuthError < Error
|
6
|
+
attr_reader :error, :method, :arguments
|
7
|
+
|
8
|
+
def initialize error, method, arguments
|
9
|
+
@error = error
|
10
|
+
@method = method
|
11
|
+
@arguments = arguments
|
12
|
+
@message = "Not authenticated/authorized for method ##{@method}"
|
13
|
+
|
14
|
+
super @message
|
15
|
+
end
|
17
16
|
end
|
18
17
|
|
19
|
-
|
20
|
-
#
|
21
|
-
# @raise [AuthorizationDecisionMakerMissingError]
|
22
|
-
# if the #authorized? method isn't defined
|
23
|
-
# in the controller class.
|
24
|
-
class AuthorizationDecisionMakerMissingError < AuthorizationError
|
18
|
+
class NotAuthenticatedError < AuthError
|
25
19
|
end
|
26
20
|
|
27
|
-
|
28
|
-
|
29
|
-
# @raise [NotAuthorizedError]
|
30
|
-
# if authorized? isn't returning true.
|
31
|
-
#
|
32
|
-
# @note
|
33
|
-
# Should always be called at the end
|
34
|
-
# of the #authorize! method.
|
35
|
-
class NotAuthorizedError < AuthorizationError
|
36
|
-
attr_reader :error
|
21
|
+
class NotAuthorizedError < AuthError
|
22
|
+
end
|
37
23
|
|
38
|
-
|
39
|
-
|
24
|
+
class NotPerformedError < Error
|
25
|
+
attr_reader :class_name, :method
|
26
|
+
|
27
|
+
def initialize class_name, method
|
28
|
+
@class_name = class_name
|
29
|
+
@method = method
|
30
|
+
@message = "Auth not performed for #{@class_name}##{@method}."
|
31
|
+
|
32
|
+
super @message
|
40
33
|
end
|
41
34
|
end
|
42
35
|
|
43
|
-
|
44
|
-
# Base class for authentication errors.
|
45
|
-
#
|
46
|
-
# @raise [AuthenticationError]
|
47
|
-
# if something generic happens.
|
48
|
-
class AuthenticationError < StandardError
|
36
|
+
class AuthenticationNotPerformedError < NotPerformedError
|
49
37
|
end
|
50
38
|
|
51
|
-
|
52
|
-
#
|
53
|
-
# @raise [AuthenticationNotPerformedError]
|
54
|
-
# if authenticate! is not called
|
55
|
-
# in the controller class.
|
56
|
-
class AuthenticationNotPerformedError < AuthenticationError
|
39
|
+
class AuthorizationNotPerformedError < NotPerformedError
|
57
40
|
end
|
58
41
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
42
|
+
class NotDefinedError < Error
|
43
|
+
attr_reader :policy_name, :class_name
|
44
|
+
|
45
|
+
def initialize policy_name, class_name
|
46
|
+
@policy_name = policy_name
|
47
|
+
@class_name = class_name
|
48
|
+
@message = "Policy #{policy_name} for class #{@class_name} not defined."
|
49
|
+
|
50
|
+
super @message
|
51
|
+
end
|
65
52
|
end
|
66
53
|
|
67
|
-
|
68
|
-
|
69
|
-
# @raise [NotAuthenticatedError]
|
70
|
-
# if authenticated? isn't returning true.
|
71
|
-
#
|
72
|
-
# @note
|
73
|
-
# Should always be called at the end
|
74
|
-
# of the #authenticate! method.
|
75
|
-
class NotAuthenticatedError < AuthenticationError
|
76
|
-
attr_reader :error
|
54
|
+
class DecisionMakerMethodNotDefinedError < Error
|
55
|
+
attr_reader :policy_name, :decision_maker_method_name
|
77
56
|
|
78
|
-
def initialize
|
79
|
-
@
|
57
|
+
def initialize policy_name, decision_maker_method_name
|
58
|
+
@policy_name = policy_name
|
59
|
+
@decision_maker_method_name = decision_maker_method_name
|
60
|
+
@message = "Decision maker #{policy_name}##{decision_maker_method_name} is not defined."
|
61
|
+
|
62
|
+
super @message
|
80
63
|
end
|
81
64
|
end
|
82
65
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ActiveEntry
|
2
|
+
class PolicyFinder
|
3
|
+
attr_reader :class_name
|
4
|
+
|
5
|
+
def initialize class_name
|
6
|
+
@class_name = class_name
|
7
|
+
end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def policy_for class_name
|
11
|
+
new(class_name).policy
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def policy
|
16
|
+
policy_class_name.safe_constantize or raise NotDefinedError.new(policy_class_name, @class_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def policy_class_name
|
22
|
+
[@class_name, "Policy"].join
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/active_entry/railtie.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
+
require_relative '../../app/helpers/active_entry/view_helper'
|
2
|
+
|
1
3
|
module ActiveEntry
|
2
|
-
class Railtie <
|
3
|
-
initializer
|
4
|
-
ActiveSupport.on_load
|
5
|
-
::ActionController::Base.include(ActiveEntry)
|
6
|
-
end
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
initializer "active_entry.view_helper" do
|
6
|
+
ActiveSupport.on_load(:action_view) { include ActiveEntry::ViewHelper }
|
7
7
|
end
|
8
8
|
end
|
9
|
-
end
|
9
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ActiveEntry
|
2
|
+
module Rspec
|
3
|
+
module Matchers
|
4
|
+
extend ::RSpec::Matchers::DSL
|
5
|
+
|
6
|
+
matcher :be_authenticated_for do |action, **args|
|
7
|
+
match do |policy|
|
8
|
+
policy.pass? action, **args
|
9
|
+
end
|
10
|
+
|
11
|
+
description do
|
12
|
+
"be authenticated for #{action}"
|
13
|
+
end
|
14
|
+
|
15
|
+
failure_message do |policy|
|
16
|
+
"expected that #{policy} passes authentication for #{action}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
matcher :be_authorized_for do |action, **args|
|
21
|
+
match do |policy|
|
22
|
+
policy.pass? action, **args
|
23
|
+
end
|
24
|
+
|
25
|
+
description do
|
26
|
+
"be authorized for #{action}"
|
27
|
+
end
|
28
|
+
|
29
|
+
failure_message do |policy|
|
30
|
+
"expected that #{policy} passes authorization for #{action}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module DSL
|
36
|
+
end
|
37
|
+
|
38
|
+
module PolicyExampleGroup
|
39
|
+
include ActiveEntry::Rspec::Matchers
|
40
|
+
|
41
|
+
def self.included(base)
|
42
|
+
base.metadata[:type] = :policy
|
43
|
+
base.extend ActiveEntry::Rspec::DSL
|
44
|
+
super
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
RSpec.configure do |config|
|
51
|
+
config.include(
|
52
|
+
ActiveEntry::Rspec::PolicyExampleGroup,
|
53
|
+
type: :policy,
|
54
|
+
file_path: %r{spec/policies}
|
55
|
+
)
|
56
|
+
end
|