verifica 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|