verifica 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE +21 -0
- data/README.md +449 -0
- data/lib/verifica/ace.rb +79 -0
- data/lib/verifica/acl.rb +208 -0
- data/lib/verifica/acl_builder.rb +63 -0
- data/lib/verifica/authorization_result.rb +152 -0
- data/lib/verifica/authorizer.rb +187 -0
- data/lib/verifica/configuration.rb +41 -0
- data/lib/verifica/errors.rb +84 -0
- data/lib/verifica/resource_configuration.rb +57 -0
- data/lib/verifica/sid.rb +215 -0
- data/lib/verifica/version.rb +5 -0
- data/lib/verifica.rb +122 -0
- data/verifica.gemspec +41 -0
- metadata +127 -0
data/lib/verifica/acl.rb
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require_relative "ace"
|
5
|
+
require_relative "acl_builder"
|
6
|
+
|
7
|
+
module Verifica
|
8
|
+
# Access Control List (ACL)
|
9
|
+
#
|
10
|
+
# Access Control List consists of Access Control Entities (ACEs) and defines which
|
11
|
+
# actions are allowed or denied for particular Security Identifiers (SIDs).
|
12
|
+
#
|
13
|
+
# ACL is typically associated with a resource (e.g. Post, Comment, Order) and specifies
|
14
|
+
# which users (or external services, or API clients) are allowed to do what actions on the given resource.
|
15
|
+
#
|
16
|
+
# @see Ace
|
17
|
+
# @see Sid
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
class Acl
|
21
|
+
# Creates a new {AclBuilder} and yields it to the given block.
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# acl = Verifica::Acl.build do |acl|
|
25
|
+
# acl.allow "anonymous", [:read]
|
26
|
+
# acl.allow "authenticated", [:read, :comment]
|
27
|
+
# acl.deny "country:US", [:read, :comment]
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# @return [Acl] Access Control List created by builder
|
31
|
+
#
|
32
|
+
# @api public
|
33
|
+
def self.build
|
34
|
+
builder = AclBuilder.new
|
35
|
+
yield builder
|
36
|
+
builder.build
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :aces
|
40
|
+
protected :aces
|
41
|
+
|
42
|
+
# Creates a new Access Control List with immutable state.
|
43
|
+
# @note Use {.build} instead of this constructor directly.
|
44
|
+
#
|
45
|
+
# @param aces [Array<Ace>] list of Access Control Entries
|
46
|
+
#
|
47
|
+
# @api public
|
48
|
+
def initialize(aces)
|
49
|
+
@aces = Set.new(aces).freeze
|
50
|
+
@allow_deny_by_action = prepare_index.freeze
|
51
|
+
@allowed_actions = Set.new
|
52
|
+
@allow_deny_by_action.each do |action, allow_deny|
|
53
|
+
@allowed_actions.add(action) unless allow_deny[:allowed_sids].empty?
|
54
|
+
|
55
|
+
allow_deny[:allowed_sids].freeze
|
56
|
+
allow_deny[:denied_sids].freeze
|
57
|
+
allow_deny.freeze
|
58
|
+
end
|
59
|
+
|
60
|
+
@allowed_actions.freeze
|
61
|
+
freeze
|
62
|
+
end
|
63
|
+
|
64
|
+
# Checks whether the action is allowed for given Security Identifiers.
|
65
|
+
# For action to be allowed all 3 conditions should be met:
|
66
|
+
#
|
67
|
+
# * ACL and SIDs are not empty
|
68
|
+
# * ACL contains at least one entry that allow given action for any of the SIDs
|
69
|
+
# * ACL contains no entries that deny given action for any of the SIDs
|
70
|
+
#
|
71
|
+
# @param action [Symbol, String] action to check
|
72
|
+
# @param sids [Array<String>, Set<String>] list of Security Identifiers to match for
|
73
|
+
#
|
74
|
+
# @return [Boolean] true if action is allowed
|
75
|
+
#
|
76
|
+
# @api public
|
77
|
+
def action_allowed?(action, sids)
|
78
|
+
return false if empty? || sids.empty?
|
79
|
+
|
80
|
+
action = action.to_sym
|
81
|
+
allow_deny = @allow_deny_by_action[action]
|
82
|
+
|
83
|
+
return false if allow_deny.nil? || !@allowed_actions.include?(action)
|
84
|
+
|
85
|
+
sids = sids.to_set
|
86
|
+
allow_deny[:allowed_sids].intersect?(sids) && !allow_deny[:denied_sids].intersect?(sids)
|
87
|
+
end
|
88
|
+
|
89
|
+
# The opposite of {#action_allowed?}
|
90
|
+
#
|
91
|
+
# @api public
|
92
|
+
def action_denied?(action, sids)
|
93
|
+
!action_allowed?(action, sids)
|
94
|
+
end
|
95
|
+
|
96
|
+
# @note Checking allowed SIDs isn't enough to determine whether the action is allowed.
|
97
|
+
# You need to always check {#denied_sids} as well.
|
98
|
+
#
|
99
|
+
# @param action (see #action_allowed?)
|
100
|
+
#
|
101
|
+
# @return [Array<String>] array of Security Identifiers allowed for a given action or empty array if none
|
102
|
+
#
|
103
|
+
# @api public
|
104
|
+
def allowed_sids(action)
|
105
|
+
sids = @allow_deny_by_action.dig(action.to_sym, :allowed_sids)
|
106
|
+
sids.nil? ? EMPTY_ARRAY : sids.to_a
|
107
|
+
end
|
108
|
+
|
109
|
+
# @note Checking denied SIDs isn't enough to determine whether the action is allowed.
|
110
|
+
# You need to always check {#allowed_sids} as well.
|
111
|
+
#
|
112
|
+
# @param action (see #action_allowed?)
|
113
|
+
#
|
114
|
+
# @return [Array<String>] array of Security Identifiers denied for a given action or empty array if none
|
115
|
+
#
|
116
|
+
# @api public
|
117
|
+
def denied_sids(action)
|
118
|
+
sids = @allow_deny_by_action.dig(action.to_sym, :denied_sids)
|
119
|
+
sids.nil? ? EMPTY_ARRAY : sids.to_a
|
120
|
+
end
|
121
|
+
|
122
|
+
# @param sids (see #action_allowed?)
|
123
|
+
#
|
124
|
+
# @return [Array<Symbol>] array of actions allowed for given Security Identifiers or empty array if none
|
125
|
+
#
|
126
|
+
# @api public
|
127
|
+
def allowed_actions(sids)
|
128
|
+
return EMPTY_ARRAY if sids.empty?
|
129
|
+
|
130
|
+
@allowed_actions.select { |action| action_allowed?(action, sids) }
|
131
|
+
end
|
132
|
+
|
133
|
+
# Creates a new {AclBuilder}, adds existing entries to it and yields it to the given block.
|
134
|
+
# Use this method to extend an existing ACL with additional entries
|
135
|
+
#
|
136
|
+
# @example
|
137
|
+
# base_acl = Verifica::Acl.build do |acl|
|
138
|
+
# acl.allow "superuser", [:read, :write, :delete]
|
139
|
+
# end
|
140
|
+
#
|
141
|
+
# extended_acl = base_acl.build do |acl|
|
142
|
+
# acl.allow "anonymous", [:read]
|
143
|
+
# acl.allow "authenticated", [:read, :comment]
|
144
|
+
# end
|
145
|
+
#
|
146
|
+
# @return [Acl] new Access Control List created by builder
|
147
|
+
#
|
148
|
+
# @api public
|
149
|
+
def build
|
150
|
+
builder = AclBuilder.new(to_a)
|
151
|
+
yield builder
|
152
|
+
builder.build
|
153
|
+
end
|
154
|
+
|
155
|
+
# @example
|
156
|
+
# acl = Verifica::Acl.build { |acl| acl.allow "root", [:read, :write] }
|
157
|
+
# acl.to_a.map(:to_h)
|
158
|
+
# # => [{:sid=>"root", :action=>:read, :allow=>true}, {:sid=>"root", :action=>:write, :allow=>true}]
|
159
|
+
#
|
160
|
+
# @return [Array<Ace>] a new array representing +self+
|
161
|
+
#
|
162
|
+
# @api public
|
163
|
+
def to_a
|
164
|
+
@aces.to_a
|
165
|
+
end
|
166
|
+
|
167
|
+
# @return [Boolean] true if there are no entries in +self+
|
168
|
+
#
|
169
|
+
# @api public
|
170
|
+
def empty?
|
171
|
+
@aces.empty?
|
172
|
+
end
|
173
|
+
|
174
|
+
# @return [Integer] the count of entries in +self+
|
175
|
+
#
|
176
|
+
# @api public
|
177
|
+
def length
|
178
|
+
@aces.length
|
179
|
+
end
|
180
|
+
alias_method :size, :length
|
181
|
+
|
182
|
+
def to_s
|
183
|
+
@aces.map(&:to_h).to_s
|
184
|
+
end
|
185
|
+
|
186
|
+
def ==(other)
|
187
|
+
eql?(other)
|
188
|
+
end
|
189
|
+
|
190
|
+
def eql?(other)
|
191
|
+
self.class == other.class &&
|
192
|
+
@aces == other.aces
|
193
|
+
end
|
194
|
+
|
195
|
+
def hash
|
196
|
+
[self.class, @aces].hash
|
197
|
+
end
|
198
|
+
|
199
|
+
private def prepare_index
|
200
|
+
@aces.each_with_object({}) do |ace, index|
|
201
|
+
action = ace.action
|
202
|
+
allow_deny = index.fetch(action) { {allowed_sids: Set.new, denied_sids: Set.new}.freeze }
|
203
|
+
allow_deny[ace.allow? ? :allowed_sids : :denied_sids].add(ace.sid)
|
204
|
+
index[action] = allow_deny
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Verifica
|
4
|
+
# Builder that holds mutable list of Access Control Entries and methods to add new entries
|
5
|
+
#
|
6
|
+
# @see Acl.build Usage examples
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
class AclBuilder
|
10
|
+
# @note Use {Acl.build} or {Acl#build} instead of this constructor directly
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
def initialize(initial_aces = EMPTY_ARRAY)
|
14
|
+
@aces = initial_aces.dup
|
15
|
+
freeze
|
16
|
+
end
|
17
|
+
|
18
|
+
# Add Access Control Entries that allow particular actions for the given Security Identifier
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# builder = AclBuilder.new
|
22
|
+
# .allow("anonymous", [:read])
|
23
|
+
# .allow("root", [:read, :write, :delete])
|
24
|
+
# acl = builder.build
|
25
|
+
#
|
26
|
+
# @param sid [String] Security Identifier
|
27
|
+
# @param actions [Enumerable<Symbol>, Enumerable<String>] list of actions allowed for the given SID
|
28
|
+
#
|
29
|
+
# @return [self]
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
def allow(sid, actions)
|
33
|
+
@aces.concat(actions.map { |action| Ace.new(sid, action, true) })
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add Access Control Entries that deny particular actions for the given Security Identifier
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# builder = Verifica::AclBuilder.new
|
41
|
+
# .deny("country:US", [:read, :comment])
|
42
|
+
# .deny("country:CA", [:read, :comment])
|
43
|
+
# acl = builder.build
|
44
|
+
#
|
45
|
+
# @param sid [String] Security Identifier
|
46
|
+
# @param actions [Enumerable<Symbol>, Enumerable<String>] list of actions denied for the given SID
|
47
|
+
#
|
48
|
+
# @return [self]
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def deny(sid, actions)
|
52
|
+
@aces.concat(actions.map { |action| Ace.new(sid, action, false) })
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Acl] a new, immutable Access Control List
|
57
|
+
#
|
58
|
+
# @api public
|
59
|
+
def build
|
60
|
+
Acl.new(@aces)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Verifica
|
4
|
+
# Outcome of the authorization, either successful or failed.
|
5
|
+
# Memoizes the state of variables that affected the decision. Could show why the authorization
|
6
|
+
# was successful or failed even if the concerned objects have changed.
|
7
|
+
#
|
8
|
+
# @see Authorizer#authorize
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class AuthorizationResult
|
12
|
+
# @return [Object] subject of the authorization (e.g. current user, external service)
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
attr_reader :subject
|
16
|
+
|
17
|
+
# @return [Object] subject ID returned by +subject.subject_id+
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
attr_reader :subject_id
|
21
|
+
|
22
|
+
# @return [Symbol, nil] subject type returned by +subject.subject_type+
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
attr_reader :subject_type
|
26
|
+
|
27
|
+
# @return [Array<String>] array of subject Security Identifiers returned by +subject.subject_sids+
|
28
|
+
#
|
29
|
+
# @api public
|
30
|
+
attr_reader :subject_sids
|
31
|
+
|
32
|
+
# @return [Object] resource on which {#subject} attempted to perform {#action}
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
attr_reader :resource
|
36
|
+
|
37
|
+
# @return [Object] resource ID returned by +resource.resource_id+
|
38
|
+
#
|
39
|
+
# @api public
|
40
|
+
attr_reader :resource_id
|
41
|
+
|
42
|
+
# @return [Symbol] resource type returned by resource#resource_type
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
attr_reader :resource_type
|
46
|
+
|
47
|
+
# @return [Symbol] action that {#subject} attempted to perform on the {#resource}
|
48
|
+
#
|
49
|
+
# @api public
|
50
|
+
attr_reader :action
|
51
|
+
|
52
|
+
# @return [Acl] Access Control List returned by ACL provider registered for this {#resource_type} in {Authorizer}
|
53
|
+
#
|
54
|
+
# @api public
|
55
|
+
attr_reader :acl
|
56
|
+
|
57
|
+
# @return [Hash] any additional keyword arguments that have been passed to the authorization call
|
58
|
+
#
|
59
|
+
# @see Authorizer#authorize
|
60
|
+
#
|
61
|
+
# @api public
|
62
|
+
attr_reader :context
|
63
|
+
|
64
|
+
# @api private
|
65
|
+
def initialize(subject, resource, action, acl, **context)
|
66
|
+
@subject = subject
|
67
|
+
sids = Verifica.subject_sids(subject, **context)
|
68
|
+
@subject_sids = sids.map { _1.dup.freeze }.freeze
|
69
|
+
@subject_id = subject.subject_id.dup.freeze
|
70
|
+
@subject_type = subject.subject_type&.to_sym
|
71
|
+
@resource = resource
|
72
|
+
@resource_id = resource.resource_id.dup.freeze
|
73
|
+
@resource_type = resource.resource_type.to_sym
|
74
|
+
@action = action
|
75
|
+
@acl = acl
|
76
|
+
@context = context
|
77
|
+
@success = acl.action_allowed?(action, @subject_sids)
|
78
|
+
freeze
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [Boolean] true if given {#action} is allowed for given {#subject}
|
82
|
+
#
|
83
|
+
# @api public
|
84
|
+
def success?
|
85
|
+
@success
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [Boolean] true if given {#action} is denied for given {#subject}
|
89
|
+
#
|
90
|
+
# @api public
|
91
|
+
def failure?
|
92
|
+
!success?
|
93
|
+
end
|
94
|
+
|
95
|
+
# @see Acl#allowed_actions
|
96
|
+
#
|
97
|
+
# @return [Array<Symbol>] array of actions allowed for given {#subject} or empty array if none
|
98
|
+
#
|
99
|
+
# @api public
|
100
|
+
def allowed_actions
|
101
|
+
acl.allowed_actions(subject_sids)
|
102
|
+
end
|
103
|
+
|
104
|
+
# @return [String] human-readable description of authorization result. Includes subject, resource, and outcome
|
105
|
+
#
|
106
|
+
# @api public
|
107
|
+
def message
|
108
|
+
status = success? ? "SUCCESS" : "FAILURE"
|
109
|
+
"Authorization #{status}. Subject '#{subject_type}' id='#{subject_id}'. Resource '#{resource_type}' " \
|
110
|
+
"id='#{resource_id}'. Action '#{action}'"
|
111
|
+
end
|
112
|
+
|
113
|
+
# @return [String] detailed, human-readable description of authorization result.
|
114
|
+
# Includes subject, resource, resource ACL, and explains the reason why authorization was successful or failed.
|
115
|
+
# Extremely useful for debugging.
|
116
|
+
#
|
117
|
+
# @api public
|
118
|
+
def explain
|
119
|
+
<<~MESSAGE
|
120
|
+
#{message}
|
121
|
+
|
122
|
+
\s\sSubject SIDs (#{subject_sids.empty? ? "empty" : subject_sids.size}):
|
123
|
+
\s\s\s\s#{subject_sids}
|
124
|
+
|
125
|
+
\s\sContext:
|
126
|
+
\s\s\s\s#{context}
|
127
|
+
|
128
|
+
\s\sResource ACL (#{acl.empty? ? "empty" : acl.size}):
|
129
|
+
#{acl.to_a.map { "\s\s\s\s#{_1}" }.join("\n")}
|
130
|
+
|
131
|
+
Reason: #{reason_message}
|
132
|
+
MESSAGE
|
133
|
+
end
|
134
|
+
|
135
|
+
private def reason_message
|
136
|
+
if success?
|
137
|
+
sids = acl.allowed_sids(action).intersection(subject_sids).to_a
|
138
|
+
return "subject SID(s) #{sids} allowed for '#{action}' action. No SIDs denied among subject SIDs"
|
139
|
+
end
|
140
|
+
|
141
|
+
return "resource ACL is empty, no actions allowed for any subject" if acl.empty?
|
142
|
+
return "subject SIDs are empty, no actions allowed for any resource" if subject_sids.empty?
|
143
|
+
|
144
|
+
denied = acl.denied_sids(action).intersection(subject_sids).to_a
|
145
|
+
if denied.empty?
|
146
|
+
"among #{subject_sids.size} subject SID(s), none is listed as allowed for '#{action}' action"
|
147
|
+
else
|
148
|
+
"subject SID(s) #{denied} denied for '#{action}' action. Denied SIDs always win regardless of allowed SIDs"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Verifica
|
4
|
+
# @api private
|
5
|
+
def self.subject_sids(subject, **context)
|
6
|
+
if subject.nil?
|
7
|
+
raise Error, "Subject should not be nil"
|
8
|
+
end
|
9
|
+
|
10
|
+
sids = subject.subject_sids(**context)
|
11
|
+
unless sids.is_a?(Array) || sids.is_a?(Set)
|
12
|
+
raise Error, "Expected subject to respond to #subject_sids with Array or Set of SIDs but got '#{sids.class}'"
|
13
|
+
end
|
14
|
+
|
15
|
+
sids
|
16
|
+
end
|
17
|
+
|
18
|
+
# Authorizer is the heart of Verifica. It's an isolated container with no global state
|
19
|
+
# which has a list of resource types registered with their companion AclProviders.
|
20
|
+
#
|
21
|
+
# Authorizer pairs great with Dependency Injection or can be configured and passed in a way that is compatible with your framework.
|
22
|
+
#
|
23
|
+
# @example (see Verifica)
|
24
|
+
#
|
25
|
+
# @see Verifica.authorizer
|
26
|
+
# @see Configuration
|
27
|
+
# @see ResourceConfiguration
|
28
|
+
#
|
29
|
+
# @api public
|
30
|
+
class Authorizer
|
31
|
+
# @note Use {Verifica.authorizer} instead of this constructor directly
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
def initialize(resource_configs)
|
35
|
+
@resources = index_resources(resource_configs).freeze
|
36
|
+
freeze
|
37
|
+
end
|
38
|
+
|
39
|
+
# Checks the authorization of a subject to perform an action on a resource
|
40
|
+
#
|
41
|
+
# * The +subject+ is asked for its Security Identifiers (SIDs) by +subject.subject_sids+
|
42
|
+
# * The +resource+ is asked for its type by +resource.resource_type+
|
43
|
+
# * ACL provider registered for this resource type is asked for {Acl} by +#call(resource, **context)+
|
44
|
+
# * ACL is checked whether the +action+ is allowed for the subject SIDs
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# def show
|
48
|
+
# post = Post.find(params[:id])
|
49
|
+
# authorizer.authorize(current_user, post, :read)
|
50
|
+
#
|
51
|
+
# render json: post
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# @param subject [Object] subject of the authorization (e.g. current user, external service)
|
55
|
+
# @param resource [Object] resource to authorize for, should respond to +#resource_type+
|
56
|
+
# @param action [Symbol, String] action that +subject+ attempts to perform on the +resource+
|
57
|
+
# @param context [Hash] arbitrary keyword arguments to forward to +subject.subject_sids+ and +acl_provider.call+
|
58
|
+
#
|
59
|
+
# @return [AuthorizationResult] authorization result with all details if authorization is successful
|
60
|
+
# @raise [AuthorizationError] if +subject+ isn't authorized to perform +action+ on the given +resource+
|
61
|
+
# @raise [Error] if +resource.resource_type+ isn't registered in +self+
|
62
|
+
#
|
63
|
+
# @see Acl#action_allowed? How ACL is checked whether the action is allowed
|
64
|
+
# @see Configuration#register_resource
|
65
|
+
#
|
66
|
+
# @api public
|
67
|
+
def authorize(subject, resource, action, **context)
|
68
|
+
result = authorization_result(subject, resource, action, **context)
|
69
|
+
raise AuthorizationError, result if result.failure?
|
70
|
+
|
71
|
+
result
|
72
|
+
end
|
73
|
+
|
74
|
+
# The same as {#authorize} but returns true/false instead of rising an exception
|
75
|
+
#
|
76
|
+
# @return [Boolean] true if +action+ on +resource+ is authorized for +subject+
|
77
|
+
# @raise [Error] if +resource.resource_type+ isn't registered in +self+
|
78
|
+
#
|
79
|
+
# @api public
|
80
|
+
def authorized?(subject, resource, action, **context)
|
81
|
+
authorization_result(subject, resource, action, **context).success?
|
82
|
+
end
|
83
|
+
|
84
|
+
# @param subject [Object] subject of the authorization (e.g. current user, external service)
|
85
|
+
# @param resource [Object] resource to get allowed actions for, should respond to +#resource_type+
|
86
|
+
# @param **context (see #authorize)
|
87
|
+
#
|
88
|
+
# @return [Array<Symbol>] array of actions allowed for +subject+ or empty array if none
|
89
|
+
# @raise [Error] if +resource.resource_type+ isn't registered in +self+
|
90
|
+
#
|
91
|
+
# @see Configuration#register_resource
|
92
|
+
# @see Acl#allowed_actions
|
93
|
+
#
|
94
|
+
# @api public
|
95
|
+
def allowed_actions(subject, resource, **context)
|
96
|
+
acl = resource_acl(resource, **context)
|
97
|
+
sids = Verifica.subject_sids(subject)
|
98
|
+
acl.allowed_actions(sids)
|
99
|
+
end
|
100
|
+
|
101
|
+
# @param resource_type [Symbol, String] type of the resource
|
102
|
+
#
|
103
|
+
# @return [ResourceConfiguration] configuration for +resource_type+
|
104
|
+
# @raise [Error] if +resource_type+ isn't registered in +self+
|
105
|
+
#
|
106
|
+
# @see resource_type?
|
107
|
+
# @see Configuration#register_resource
|
108
|
+
#
|
109
|
+
# @api public
|
110
|
+
def resource_config(resource_type)
|
111
|
+
resource_type = resource_type.to_sym
|
112
|
+
config = @resources[resource_type]
|
113
|
+
if config.nil?
|
114
|
+
raise Error, "Unknown resource '#{resource_type}'. Did you forget to register this resource type?"
|
115
|
+
end
|
116
|
+
|
117
|
+
config
|
118
|
+
end
|
119
|
+
|
120
|
+
# @param resource_type (see #resource_config)
|
121
|
+
#
|
122
|
+
# @return [Boolean] true if +resource_type+ is registered in +self+
|
123
|
+
#
|
124
|
+
# @see Configuration#register_resource
|
125
|
+
#
|
126
|
+
# @api public
|
127
|
+
def resource_type?(resource_type)
|
128
|
+
@resources.key?(resource_type.to_sym)
|
129
|
+
end
|
130
|
+
|
131
|
+
# @param resource [Object] resource to get ACL for, should respond to +#resource_type+
|
132
|
+
# @param context [Hash] arbitrary keyword arguments to forward to +acl_provider.call+
|
133
|
+
#
|
134
|
+
# @return [Acl] Access Control List for +resource+
|
135
|
+
# @raise [Error] if +resource_type+ isn't registered in +self+
|
136
|
+
# @raise [Error] if ACL provider for this resource type doesn't respond to +#call(resource, **)+ with {Acl} object
|
137
|
+
#
|
138
|
+
# @see Configuration#register_resource
|
139
|
+
#
|
140
|
+
# @api public
|
141
|
+
def resource_acl(resource, **context)
|
142
|
+
config = config_by_resource(resource)
|
143
|
+
acl = config.acl_provider.call(resource, **context)
|
144
|
+
unless acl.is_a?(Verifica::Acl)
|
145
|
+
type = resource.resource_type
|
146
|
+
raise Error, "'#{type}' resource acl_provider should respond to #call with Acl object but got '#{acl.class}'"
|
147
|
+
end
|
148
|
+
|
149
|
+
acl
|
150
|
+
end
|
151
|
+
|
152
|
+
private def index_resources(resource_configs)
|
153
|
+
resource_configs.each_with_object({}) do |config, by_type|
|
154
|
+
type = config.resource_type
|
155
|
+
if by_type.key?(type)
|
156
|
+
raise Error, "'#{type}' resource registered multiple times. Probably code copy-paste and a bug?"
|
157
|
+
end
|
158
|
+
|
159
|
+
by_type[type] = config
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
private def config_by_resource(resource)
|
164
|
+
if resource.nil?
|
165
|
+
raise Error, "Resource should not be nil"
|
166
|
+
end
|
167
|
+
|
168
|
+
type = resource.resource_type
|
169
|
+
if type.nil?
|
170
|
+
raise Error, "Resource should respond to #resource_type with non-nil type"
|
171
|
+
end
|
172
|
+
|
173
|
+
resource_config(type)
|
174
|
+
end
|
175
|
+
|
176
|
+
private def authorization_result(subject, resource, action, **context)
|
177
|
+
action = action.to_sym
|
178
|
+
possible_actions = config_by_resource(resource).possible_actions
|
179
|
+
unless possible_actions.include?(action)
|
180
|
+
raise Error, "'#{action}' action is not registered as possible for '#{resource.resource_type}' resource"
|
181
|
+
end
|
182
|
+
|
183
|
+
acl = resource_acl(resource, **context)
|
184
|
+
AuthorizationResult.new(subject, resource, action, acl, **context)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "resource_configuration"
|
4
|
+
|
5
|
+
module Verifica
|
6
|
+
# Configuration object for {Authorizer}, holds list of registered resources and other params
|
7
|
+
#
|
8
|
+
# @see Verifica.authorizer Usage examples
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Configuration
|
12
|
+
# @return [Array<ResourceConfiguration>] array of registered resources
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
attr_reader :resources
|
16
|
+
|
17
|
+
# @note Use {Verifica.authorizer} instead of this constructor directly
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
def initialize
|
21
|
+
@resources = []
|
22
|
+
end
|
23
|
+
|
24
|
+
# Register a new resource supported by {Authorizer}
|
25
|
+
#
|
26
|
+
# @see Verifica.authorizer Usage examples
|
27
|
+
#
|
28
|
+
# @param type [Symbol, String] type of the resource
|
29
|
+
# @param possible_actions [Enumerable<Symbol>, Enumerable<String>] list of actions possible for this resource type
|
30
|
+
# @param acl_provider [#call] Access Control List provider for this resource type.
|
31
|
+
# Could be any object that responds to +#call(resource, **)+ and returns {Acl}
|
32
|
+
#
|
33
|
+
# @return [self]
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
def register_resource(type, possible_actions, acl_provider)
|
37
|
+
resources << ResourceConfiguration.new(type, possible_actions, acl_provider)
|
38
|
+
self
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|