shamu 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +26 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +89 -30
- data/.yardopts +4 -5
- data/Gemfile +24 -12
- data/Guardfile +5 -0
- data/LABELS.md +22 -0
- data/README.md +41 -0
- data/Rakefile +12 -0
- data/circle.yml +7 -3
- data/config.ru +7 -0
- data/lib/shamu/active_record.rb +7 -0
- data/lib/shamu/attributes/assignment.rb +114 -0
- data/lib/shamu/attributes/equality.rb +40 -0
- data/lib/shamu/attributes/fluid_assignment.rb +49 -0
- data/lib/shamu/attributes/validation.rb +74 -0
- data/lib/shamu/attributes.rb +255 -0
- data/lib/shamu/auditing/README.md +0 -0
- data/lib/shamu/auditing/audit_record.rb +32 -0
- data/lib/shamu/auditing/auditing_service.rb +32 -0
- data/lib/shamu/auditing/list_scope.rb +22 -0
- data/lib/shamu/auditing/logging_auditing_service.rb +16 -0
- data/lib/shamu/auditing/support.rb +75 -0
- data/lib/shamu/auditing/transaction.rb +58 -0
- data/lib/shamu/auditing.rb +12 -0
- data/lib/shamu/entities/README.md +1 -0
- data/lib/shamu/entities/active_record.rb +123 -0
- data/lib/shamu/entities/active_record_soft_destroy.rb +91 -0
- data/lib/shamu/entities/entity.rb +196 -0
- data/lib/shamu/entities/entity_path.rb +87 -0
- data/lib/shamu/entities/identity_cache.rb +64 -0
- data/lib/shamu/entities/list.rb +54 -0
- data/lib/shamu/entities/list_scope/dates.rb +57 -0
- data/lib/shamu/entities/list_scope/paging.rb +51 -0
- data/lib/shamu/entities/list_scope/scoped_paging.rb +65 -0
- data/lib/shamu/entities/list_scope/sorting.rb +76 -0
- data/lib/shamu/entities/list_scope.rb +105 -0
- data/lib/shamu/entities/null_entity.rb +88 -0
- data/lib/shamu/entities.rb +11 -0
- data/lib/shamu/error.rb +23 -5
- data/lib/shamu/events/README.md +0 -0
- data/lib/shamu/events/active_record/channel.rb +36 -0
- data/lib/shamu/events/active_record/message.rb +52 -0
- data/lib/shamu/events/active_record/migration.rb +49 -0
- data/lib/shamu/events/active_record/runner.rb +28 -0
- data/lib/shamu/events/active_record/service.rb +174 -0
- data/lib/shamu/events/active_record.rb +13 -0
- data/lib/shamu/events/channel_stats.rb +23 -0
- data/lib/shamu/events/error.rb +24 -0
- data/lib/shamu/events/events_service.rb +136 -0
- data/lib/shamu/events/in_memory/async_service.rb +48 -0
- data/lib/shamu/events/in_memory/service.rb +97 -0
- data/lib/shamu/events/in_memory.rb +10 -0
- data/lib/shamu/events/message.rb +38 -0
- data/lib/shamu/events/support.rb +60 -0
- data/lib/shamu/events.rb +12 -0
- data/lib/shamu/features/README.md +0 -0
- data/lib/shamu/features/conditions/condition.rb +39 -0
- data/lib/shamu/features/conditions/env.rb +37 -0
- data/lib/shamu/features/conditions/hosts.rb +25 -0
- data/lib/shamu/features/conditions/matching.rb +16 -0
- data/lib/shamu/features/conditions/not_matching.rb +16 -0
- data/lib/shamu/features/conditions/percentage.rb +44 -0
- data/lib/shamu/features/conditions/proc.rb +54 -0
- data/lib/shamu/features/conditions/roles.rb +23 -0
- data/lib/shamu/features/conditions/schedule_at.rb +27 -0
- data/lib/shamu/features/conditions.rb +18 -0
- data/lib/shamu/features/config_service.rb +10 -0
- data/lib/shamu/features/context.rb +80 -0
- data/lib/shamu/features/env_store.rb +88 -0
- data/lib/shamu/features/errors.rb +29 -0
- data/lib/shamu/features/features_service.rb +168 -0
- data/lib/shamu/features/list_scope.rb +30 -0
- data/lib/shamu/features/selector.rb +50 -0
- data/lib/shamu/features/support.rb +51 -0
- data/lib/shamu/features/toggle.rb +149 -0
- data/lib/shamu/features/toggle_codec.rb +69 -0
- data/lib/shamu/features.rb +16 -0
- data/lib/shamu/locale/en.yml +22 -2
- data/lib/shamu/logger.rb +13 -0
- data/lib/shamu/rack/README.md +0 -0
- data/lib/shamu/rack/cookies.rb +115 -0
- data/lib/shamu/rack/cookies_middleware.rb +26 -0
- data/lib/shamu/rack/query_params.rb +41 -0
- data/lib/shamu/rack/query_params_middleware.rb +24 -0
- data/lib/shamu/rack.rb +12 -0
- data/lib/shamu/rails/controller.rb +131 -0
- data/lib/shamu/rails/entity.rb +168 -0
- data/lib/shamu/rails/features.rb +13 -0
- data/lib/shamu/rails/railtie.rb +30 -0
- data/lib/shamu/rails.rb +10 -0
- data/lib/shamu/rspec/matchers.rb +44 -0
- data/lib/shamu/rspec.rb +1 -0
- data/lib/shamu/security/README.md +0 -0
- data/lib/shamu/security/active_record_policy.rb +106 -0
- data/lib/shamu/security/error.rb +65 -0
- data/lib/shamu/security/hashed_value.rb +71 -0
- data/lib/shamu/security/no_policy.rb +15 -0
- data/lib/shamu/security/policy.rb +289 -0
- data/lib/shamu/security/policy_refinement.rb +50 -0
- data/lib/shamu/security/policy_rule.rb +59 -0
- data/lib/shamu/security/principal.rb +72 -0
- data/lib/shamu/security/roles.rb +62 -0
- data/lib/shamu/security/roles_service.rb +30 -0
- data/lib/shamu/security/support.rb +83 -0
- data/lib/shamu/security.rb +43 -0
- data/lib/shamu/services/README.md +2 -0
- data/lib/shamu/services/active_record.rb +58 -0
- data/lib/shamu/services/active_record_crud.rb +378 -0
- data/lib/shamu/services/error.rb +24 -0
- data/lib/shamu/services/lazy_association.rb +31 -0
- data/lib/shamu/services/lazy_transform.rb +97 -0
- data/lib/shamu/services/request.rb +122 -0
- data/lib/shamu/services/request_support.rb +124 -0
- data/lib/shamu/services/result.rb +75 -0
- data/lib/shamu/services/service.rb +355 -0
- data/lib/shamu/services.rb +12 -0
- data/lib/shamu/sessions/README.md +2 -0
- data/lib/shamu/sessions/cookie_store.rb +79 -0
- data/lib/shamu/sessions/session_store.rb +42 -0
- data/lib/shamu/sessions.rb +8 -0
- data/lib/shamu/to_bool_extension.rb +57 -0
- data/lib/shamu/to_model_id_extension.rb +50 -0
- data/lib/shamu/version.rb +10 -4
- data/lib/shamu.rb +18 -6
- data/shamu.gemspec +21 -10
- data/spec/internal/README.md +4 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/schema.rb +3 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/lib/shamu/active_record_support.rb +32 -0
- data/spec/lib/shamu/attributes/assignment_spec.rb +129 -0
- data/spec/lib/shamu/attributes/equality_spec.rb +63 -0
- data/spec/lib/shamu/attributes/fluid_assignment_spec.rb +31 -0
- data/spec/lib/shamu/attributes/validation_spec.rb +53 -0
- data/spec/lib/shamu/attributes_spec.rb +331 -0
- data/spec/lib/shamu/auditing/logging_auditing_service_spec.rb +18 -0
- data/spec/lib/shamu/auditing/support_spec.rb +41 -0
- data/spec/lib/shamu/entities/active_record_soft_destroy_spec.rb +82 -0
- data/spec/lib/shamu/entities/active_record_spec.rb +66 -0
- data/spec/lib/shamu/entities/entity_path_spec.rb +40 -0
- data/spec/lib/shamu/entities/entity_spec.rb +56 -0
- data/spec/lib/shamu/entities/identity_cache_spec.rb +69 -0
- data/spec/lib/shamu/entities/list_scope/dates_spec.rb +47 -0
- data/spec/lib/shamu/entities/list_scope/paging_spec.rb +41 -0
- data/spec/lib/shamu/entities/list_scope/scoped_paging_spec.rb +40 -0
- data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +59 -0
- data/spec/lib/shamu/entities/list_scope_spec.rb +127 -0
- data/spec/lib/shamu/entities/list_spec.rb +60 -0
- data/spec/lib/shamu/entities/null_entity_spec.rb +94 -0
- data/spec/lib/shamu/events/active_record/migration_spec.rb +11 -0
- data/spec/lib/shamu/events/active_record/service_spec.rb +139 -0
- data/spec/lib/shamu/events/events_service_spec.rb +57 -0
- data/spec/lib/shamu/events/in_memory/async_service_spec.rb +37 -0
- data/spec/lib/shamu/events/in_memory/service_spec.rb +36 -0
- data/spec/lib/shamu/events/message_spec.rb +7 -0
- data/spec/lib/shamu/events/support_spec.rb +44 -0
- data/spec/lib/shamu/features/conditions/condition_spec.rb +8 -0
- data/spec/lib/shamu/features/conditions/env_spec.rb +29 -0
- data/spec/lib/shamu/features/conditions/hosts_spec.rb +21 -0
- data/spec/lib/shamu/features/conditions/matching_spec.rb +23 -0
- data/spec/lib/shamu/features/conditions/percentage_spec.rb +71 -0
- data/spec/lib/shamu/features/conditions/proc_spec.rb +28 -0
- data/spec/lib/shamu/features/env_store_spec.rb +48 -0
- data/spec/lib/shamu/features/features.yml +34 -0
- data/spec/lib/shamu/features/features_service_spec.rb +109 -0
- data/spec/lib/shamu/features/secondary.yml +5 -0
- data/spec/lib/shamu/features/selector_spec.rb +17 -0
- data/spec/lib/shamu/features/support_spec.rb +45 -0
- data/spec/lib/shamu/features/toggle_codec_spec.rb +28 -0
- data/spec/lib/shamu/features/toggle_spec.rb +42 -0
- data/spec/lib/shamu/rack/cookies_middleware_spec.rb +33 -0
- data/spec/lib/shamu/rack/cookies_spec.rb +43 -0
- data/spec/lib/shamu/rack/query_params_middleware_spec.rb +33 -0
- data/spec/lib/shamu/rack/query_params_spec.rb +23 -0
- data/spec/lib/shamu/rails/controller_spec.rb +74 -0
- data/spec/lib/shamu/rails/entity_spec.rb +150 -0
- data/spec/lib/shamu/rails/features.yml +13 -0
- data/spec/lib/shamu/rails/features_spec.rb +45 -0
- data/spec/lib/shamu/security/active_record_policy_spec.rb +38 -0
- data/spec/lib/shamu/security/hashed_value_spec.rb +41 -0
- data/spec/lib/shamu/security/policy_refinement_spec.rb +61 -0
- data/spec/lib/shamu/security/policy_rule_spec.rb +60 -0
- data/spec/lib/shamu/security/policy_spec.rb +158 -0
- data/spec/lib/shamu/security/roles_spec.rb +46 -0
- data/spec/lib/shamu/services/active_record_crud_spec.rb +460 -0
- data/spec/lib/shamu/services/active_record_spec.rb +92 -0
- data/spec/lib/shamu/services/lazy_association_spec.rb +31 -0
- data/spec/lib/shamu/services/lazy_transform_spec.rb +96 -0
- data/spec/lib/shamu/services/request_spec.rb +58 -0
- data/spec/lib/shamu/services/request_support_spec.rb +129 -0
- data/spec/lib/shamu/services/result_spec.rb +37 -0
- data/spec/lib/shamu/services/service_spec.rb +307 -0
- data/spec/lib/shamu/sessions/cookie_store_spec.rb +44 -0
- data/spec/lib/shamu/to_bool_extension_spec.rb +67 -0
- data/spec/lib/shamu/to_model_id_extension_spec.rb +54 -0
- data/spec/rails_helper.rb +13 -0
- data/spec/spec_helper.rb +17 -12
- data/spec/support/active_record.rb +17 -0
- data/spec/support/database.rb +14 -0
- data/spec/support/logger.rb +0 -0
- metadata +383 -9
- data/spec/lib/shamu_spec.rb +0 -5
- /data/{spec/internal/log/test.log → lib/shamu/attributes/README.md} +0 -0
@@ -0,0 +1,289 @@
|
|
1
|
+
require "shamu/security/roles"
|
2
|
+
|
3
|
+
module Shamu
|
4
|
+
module Security
|
5
|
+
|
6
|
+
# ...
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class UserPolicy < Shamu::Security::Policy
|
10
|
+
#
|
11
|
+
# role :admin, inherits: :manager
|
12
|
+
# role :manager
|
13
|
+
# role :user
|
14
|
+
#
|
15
|
+
# private
|
16
|
+
#
|
17
|
+
# def permissions
|
18
|
+
# alias_action :email, to: :contact
|
19
|
+
#
|
20
|
+
# permit :contact, UserEntity if in_role?( :manager )
|
21
|
+
# permit :email, UserEntity do |user|
|
22
|
+
# user.public_profile?
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# principal = Shamu::Security::Principal.new( user_id: user.id )
|
28
|
+
# policy = UserPolicy.new(
|
29
|
+
# principal: principal,
|
30
|
+
# roles: roles_service.roles_for( principal )
|
31
|
+
# )
|
32
|
+
#
|
33
|
+
# if policy.permit? :contact, user
|
34
|
+
# mail_to user
|
35
|
+
# end
|
36
|
+
class Policy
|
37
|
+
include Security::Roles
|
38
|
+
|
39
|
+
# ============================================================================
|
40
|
+
# @!group Dependencies
|
41
|
+
#
|
42
|
+
|
43
|
+
# @!attribute
|
44
|
+
# @return [Principal] principal holding user identity and access credentials.
|
45
|
+
attr_reader :principal
|
46
|
+
|
47
|
+
# @!attribute
|
48
|
+
# @return [Array<Roles>] roles that have been granted to the {#principal}.
|
49
|
+
attr_reader :roles
|
50
|
+
#
|
51
|
+
# @!endgroup Dependencies
|
52
|
+
|
53
|
+
def initialize( principal: nil, roles: nil )
|
54
|
+
@principal = principal || Principal.new
|
55
|
+
@roles = roles || []
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# Authorize the given `action` on the given resource. If it is not
|
60
|
+
# {#permit? permitted} then an exception is raised.
|
61
|
+
#
|
62
|
+
# @param (see #permit?)
|
63
|
+
# @return [resource]
|
64
|
+
# @raise [AccessDeniedError] if not permitted.
|
65
|
+
def authorize!( action, resource, additional_context = nil )
|
66
|
+
return resource if permit?( action, resource, additional_context ) == :yes
|
67
|
+
|
68
|
+
fail Security::AccessDeniedError,
|
69
|
+
action: action,
|
70
|
+
resource: resource,
|
71
|
+
additional_context: additional_context,
|
72
|
+
principal: principal
|
73
|
+
end
|
74
|
+
|
75
|
+
# Determines if the given `action` may be performed on the given
|
76
|
+
# `resource`.
|
77
|
+
#
|
78
|
+
# @param [Symbol] action to perform.
|
79
|
+
# @param [Object] resource the resource the action will be performed on.
|
80
|
+
# @param [Object] additional_context that the policy may consider.
|
81
|
+
# @return [:yes, :maybe, false] a truthy value if permitted, otherwise
|
82
|
+
# false. The truthy value depends on the certainty of the policy. A
|
83
|
+
# value of `:yes` or `true` indicates the action is always permitted.
|
84
|
+
# A value of `:maybe` indicates the action is permitted but the user
|
85
|
+
# may need to present additional credentials such as logging on this
|
86
|
+
# session or entering a TFA code.
|
87
|
+
def permit?( action, resource, additional_context = nil )
|
88
|
+
fail_on_active_record_check( resource )
|
89
|
+
|
90
|
+
rules.each do |rule|
|
91
|
+
next unless rule.match?( action, resource, additional_context )
|
92
|
+
|
93
|
+
return rule.result
|
94
|
+
end
|
95
|
+
|
96
|
+
false
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# The rules that have been defined.
|
102
|
+
def rules
|
103
|
+
@rules ||= begin
|
104
|
+
@rules = []
|
105
|
+
permissions
|
106
|
+
@rules
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Mapping of action names to aliases.
|
111
|
+
def aliases
|
112
|
+
@aliases ||= {}
|
113
|
+
end
|
114
|
+
|
115
|
+
# @!visibility public
|
116
|
+
#
|
117
|
+
# @param [Array<Symbol>] roles to check.
|
118
|
+
# @return [Boolean] true if the {#principal} has been granted one of the
|
119
|
+
# given roles.
|
120
|
+
def in_role?( *roles )
|
121
|
+
( principal_roles & roles ).any?
|
122
|
+
end
|
123
|
+
|
124
|
+
def principal_roles
|
125
|
+
@principal_roles ||= begin
|
126
|
+
expanded = self.class.expand_roles( *roles )
|
127
|
+
expanded << :user if principal.user_id && self.class.role_defined?( :user )
|
128
|
+
expanded
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# ============================================================================
|
133
|
+
# @!group DSL
|
134
|
+
#
|
135
|
+
|
136
|
+
# @!visibility public
|
137
|
+
#
|
138
|
+
# Hook to be overridden by a derived class to define the set of rules
|
139
|
+
# that {#permit?} should consider when evaluating the {#principal}'s
|
140
|
+
# permissions on a resource.
|
141
|
+
#
|
142
|
+
# Rules defined in the permissions block are evaluated in reverse order
|
143
|
+
# such that the last matching {#permit} or {#deny} will determine the
|
144
|
+
# permission.
|
145
|
+
#
|
146
|
+
# If no rules match, the permission is denied.
|
147
|
+
#
|
148
|
+
# @example
|
149
|
+
# def permissions
|
150
|
+
# permit :read, UserEntity
|
151
|
+
#
|
152
|
+
# deny :read, UserEntity do |user|
|
153
|
+
# user.protected_account? && !in_role( :admin )
|
154
|
+
# end
|
155
|
+
# end
|
156
|
+
#
|
157
|
+
# @return [void]
|
158
|
+
def permissions
|
159
|
+
fail IncompleteSetupError, "Permissions have not been defined. Add a private `permissions` method to #{ self.class.name }" # rubocop:disable Metrics/LineLength
|
160
|
+
end
|
161
|
+
|
162
|
+
# @!visibility public
|
163
|
+
#
|
164
|
+
# Permit one or more `actions` to be performed on a given `resource`.
|
165
|
+
#
|
166
|
+
# When a block is provided the policy will yield to the block to allow
|
167
|
+
# for more complex or context aware policy checks. The block is not
|
168
|
+
# called if the resource offered to {#permit?} is a Class or Module.
|
169
|
+
#
|
170
|
+
# @example
|
171
|
+
# permit :read, UserEntity
|
172
|
+
# permit :show, :dashboard
|
173
|
+
# permit :update, UserEntity do |user|
|
174
|
+
# user.id == principal.user_id
|
175
|
+
# end
|
176
|
+
# permit :destroy, UserEntity do |user, additional_context|
|
177
|
+
# in_role?( :admin ) && additional_context[:custom_data] == :safe
|
178
|
+
# end
|
179
|
+
#
|
180
|
+
# @param [Array<Symbol>] actions to be permitted.
|
181
|
+
# @param [Object] resource to perform the action on or the Class of
|
182
|
+
# instances to permit the action on.
|
183
|
+
# @yield ( resource, additional_context )
|
184
|
+
# @yieldparam [Object] resource instance or Class offered to {#permit?}
|
185
|
+
# that the requested action is to be performed on.
|
186
|
+
# @yieldparam [Object] additional_context offered to {#permit?}.
|
187
|
+
# @yieldreturn [:yes, :maybe, false] see {#permit?}.
|
188
|
+
# @return [void]
|
189
|
+
def permit( *actions, resource, &block )
|
190
|
+
result = @when_elevated ? :maybe : :yes
|
191
|
+
|
192
|
+
add_rule( actions, resource, result, &block )
|
193
|
+
end
|
194
|
+
|
195
|
+
# @!visibility public
|
196
|
+
#
|
197
|
+
# Explicitly deny an action previously granted with {#permit}.
|
198
|
+
#
|
199
|
+
# @param (see #permit)
|
200
|
+
# @return [void]
|
201
|
+
# @yield (see #permit)
|
202
|
+
# @yieldparam (see #permit)
|
203
|
+
# @yieldreturn [Boolean] true to deny the action.
|
204
|
+
def deny( *actions, resource, &block )
|
205
|
+
add_rule( actions, resource, false, &block )
|
206
|
+
end
|
207
|
+
|
208
|
+
# @!visibility public
|
209
|
+
#
|
210
|
+
# Only {#authorize!} the permissions defined in the given block when the
|
211
|
+
# {#principal} has elevated this session by providing their credentials.
|
212
|
+
#
|
213
|
+
# Permissions defined in the block will yield a `:maybe` result when
|
214
|
+
# queried via {#permit?} and will raise an {AccessDeniedError} when
|
215
|
+
# an {#authorize!} check is enforced.
|
216
|
+
#
|
217
|
+
# This allows you to enable/disable UX in response to what a user should
|
218
|
+
# be capable of doing but wait to actually allow it until they have
|
219
|
+
# offered their credentials.
|
220
|
+
#
|
221
|
+
# @return [void]
|
222
|
+
def when_elevated( &block )
|
223
|
+
current = @when_elevated
|
224
|
+
@when_elevated = true
|
225
|
+
yield
|
226
|
+
@when_elevated = current
|
227
|
+
end
|
228
|
+
|
229
|
+
# @!visibility public
|
230
|
+
#
|
231
|
+
# Add an action alias so that granting the alias will result in permits
|
232
|
+
# for any of the listed actions.
|
233
|
+
#
|
234
|
+
# @example
|
235
|
+
# alias_action :show, :list, to: :read
|
236
|
+
# permit :read, :stuff
|
237
|
+
#
|
238
|
+
# permit?( :show, :stuff ) # => :yes
|
239
|
+
# permit?( :list, :stuff ) # => :yes
|
240
|
+
# permit?( :read, :stuff ) # => :yes
|
241
|
+
# permit?( :write, :stuff ) # => false
|
242
|
+
#
|
243
|
+
# @param [Array<Symbol>] actions to alias.
|
244
|
+
# @param [Symbol] to the action that should permit all the listed aliases.
|
245
|
+
# @return [void]
|
246
|
+
def alias_action( *actions, to: fail ) # bug in rubocop chokes on trailing required keyword
|
247
|
+
aliases[to] ||= []
|
248
|
+
aliases[to] |= actions
|
249
|
+
end
|
250
|
+
|
251
|
+
#
|
252
|
+
# @!endgroup DSL
|
253
|
+
|
254
|
+
def add_rule( actions, resource, result, &block )
|
255
|
+
rules.unshift PolicyRule.new( expand_aliases( actions ), resource, result, block )
|
256
|
+
end
|
257
|
+
|
258
|
+
def expand_aliases( actions )
|
259
|
+
expanded = actions.dup
|
260
|
+
actions.each do |action|
|
261
|
+
expand_alias_into( action, expanded )
|
262
|
+
end
|
263
|
+
|
264
|
+
expanded
|
265
|
+
end
|
266
|
+
|
267
|
+
def expand_alias_into( candidate, expanded )
|
268
|
+
return unless mapped = aliases[candidate]
|
269
|
+
|
270
|
+
mapped.each do |action|
|
271
|
+
next if expanded.include? action
|
272
|
+
|
273
|
+
expanded << action
|
274
|
+
expand_alias_into( action, expanded )
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def fail_on_active_record_check( resource )
|
279
|
+
return unless resource
|
280
|
+
return unless defined? ActiveRecord
|
281
|
+
|
282
|
+
if resource.is_a?( ActiveRecord::Base ) || ( resource.is_a?( Class ) && resource < ActiveRecord::Base )
|
283
|
+
fail NoActiveRecordPolicyChecksError
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Security
|
3
|
+
|
4
|
+
# Defines how an {ActiveRecord::Relation} is refined for an
|
5
|
+
# {ActiveRecordPolicy}.
|
6
|
+
class PolicyRefinement
|
7
|
+
|
8
|
+
def initialize( actions, model_class, block )
|
9
|
+
@actions = actions
|
10
|
+
@model_class = model_class
|
11
|
+
@block = block
|
12
|
+
end
|
13
|
+
|
14
|
+
# Determines if the refinement matches the request action permission on
|
15
|
+
# the given relation.
|
16
|
+
#
|
17
|
+
# @param [Symbol] action to be performed on entities projected from the
|
18
|
+
# `relation`.
|
19
|
+
# @param [ActiveRecord::Relation] relation to refine.
|
20
|
+
# @param [Object] additional context offered to {Policy#permit?}.
|
21
|
+
#
|
22
|
+
# @return [Boolean] true if the rule is a match.
|
23
|
+
def match?( action, relation, additional_context )
|
24
|
+
return false unless actions.include? action
|
25
|
+
return false unless model_class_match?( relation )
|
26
|
+
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
# Apply the refinement to the relation.
|
31
|
+
#
|
32
|
+
# @param [ActiveRecord::Relation] relation to refine
|
33
|
+
# @return [ActiveRecord::Relation]
|
34
|
+
def apply( relation, additional_context )
|
35
|
+
( block && block.call( relation, additional_context ) ) || relation
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :actions
|
41
|
+
attr_reader :model_class
|
42
|
+
attr_reader :block
|
43
|
+
|
44
|
+
def model_class_match?( candidate )
|
45
|
+
model_class <= candidate.klass
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Security
|
3
|
+
|
4
|
+
# A rule capturing the permitted actions and resources for {Policy}
|
5
|
+
# permissions.
|
6
|
+
class PolicyRule
|
7
|
+
|
8
|
+
# ============================================================================
|
9
|
+
# @!group Attributes
|
10
|
+
#
|
11
|
+
|
12
|
+
# @!attribute
|
13
|
+
# @return [Object] the value to return as the result of a {Policy#permit?}
|
14
|
+
# call if the rule matches the request.
|
15
|
+
attr_reader :result
|
16
|
+
|
17
|
+
#
|
18
|
+
# @!endgroup Attributes
|
19
|
+
|
20
|
+
def initialize( actions, resource, result, block )
|
21
|
+
@actions = actions
|
22
|
+
@resource = resource
|
23
|
+
@result = result
|
24
|
+
@block = block
|
25
|
+
end
|
26
|
+
|
27
|
+
# Determines if the rule matches the request action permission on the
|
28
|
+
# given resource.
|
29
|
+
#
|
30
|
+
# @param [Symbol] action to be performed.
|
31
|
+
# @param [Object] resource the action will be performed on.
|
32
|
+
# @param [Object] additional context offered to {Policy#permit?}.
|
33
|
+
#
|
34
|
+
# @return [Boolean] true if the rule is a match.
|
35
|
+
def match?( action, resource, additional_context )
|
36
|
+
return false unless actions.include? action
|
37
|
+
return false unless resource_match?( resource )
|
38
|
+
|
39
|
+
if block && !resource.is_a?( Module )
|
40
|
+
block.call( resource, additional_context )
|
41
|
+
else
|
42
|
+
true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :actions
|
49
|
+
attr_reader :resource
|
50
|
+
attr_reader :block
|
51
|
+
|
52
|
+
def resource_match?( candidate )
|
53
|
+
return true if resource == candidate
|
54
|
+
return true if resource.is_a?( Module ) && candidate.is_a?( resource )
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Security
|
3
|
+
|
4
|
+
# ...
|
5
|
+
class Principal
|
6
|
+
|
7
|
+
# ============================================================================
|
8
|
+
# @!group Attributes
|
9
|
+
#
|
10
|
+
|
11
|
+
# @!attribute
|
12
|
+
# @return [Object] id of the currently authenticated user. May be cached,
|
13
|
+
# for example bu via persistent cookie. See {#elevated}.
|
14
|
+
attr_reader :user_id
|
15
|
+
|
16
|
+
# @!attribute
|
17
|
+
# @return [Principal] the parent principal when a user or service is
|
18
|
+
# impersonating another user.
|
19
|
+
attr_reader :parent_principal
|
20
|
+
|
21
|
+
# @!attribute
|
22
|
+
# @return [String] the IP address of the remote user.
|
23
|
+
attr_reader :remote_ip
|
24
|
+
|
25
|
+
# @!attribute
|
26
|
+
# @return [Boolean] true if the user has elevated this session by
|
27
|
+
# providing their credentials.
|
28
|
+
attr_reader :elevated
|
29
|
+
alias_method :elevated?, :elevated
|
30
|
+
|
31
|
+
#
|
32
|
+
# @!endgroup Attributes
|
33
|
+
|
34
|
+
def initialize( user_id: nil, parent_principal: nil, remote_ip: nil, elevated: false )
|
35
|
+
@user_id = user_id
|
36
|
+
@parent_principal = parent_principal
|
37
|
+
@remote_ip = remote_ip
|
38
|
+
@elevated = elevated
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Array<Object>] all of the user ids in the security principal
|
42
|
+
# chain, starting from the root.
|
43
|
+
def user_id_chain
|
44
|
+
@user_ids ||= begin
|
45
|
+
user_ids = []
|
46
|
+
principal = self
|
47
|
+
while principal
|
48
|
+
user_ids << principal.user_id
|
49
|
+
principal = principal.parent_principal
|
50
|
+
end
|
51
|
+
|
52
|
+
user_ids.reverse
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Boolean] true if the [#user_id] is being impersonated.
|
57
|
+
def impersonated?
|
58
|
+
!!parent_principal
|
59
|
+
end
|
60
|
+
|
61
|
+
# Create a new impersonation {Principal}, cloning relevant principal to the
|
62
|
+
# new instance.
|
63
|
+
#
|
64
|
+
# @param [Object] user_id of the user to impersonate.
|
65
|
+
# @return [Principal] the new principal.
|
66
|
+
def impersonate( user_id )
|
67
|
+
self.class.new( user_id: user_id, parent_principal: self, remote_ip: remote_ip, elevated: elevated )
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Security
|
3
|
+
|
4
|
+
# Mixin for {Policy} and {Support} classes to define security roles
|
5
|
+
# including inheritance.
|
6
|
+
module Roles
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
|
11
|
+
# @return [Hash] the named roles defined on the class.
|
12
|
+
def roles
|
13
|
+
@roles ||= {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Define a named role.
|
17
|
+
#
|
18
|
+
# @param [Symbol] name of the role.
|
19
|
+
# @param [Array<Symbol>] inherits additional roles that are
|
20
|
+
# automatically inherited when the named role is granted.
|
21
|
+
# @return [void]
|
22
|
+
def role( name, inherits: nil )
|
23
|
+
roles[ name.to_sym ] = { inherits: Array( inherits ) }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Expand the given roles to include the roles that they have inherited.
|
27
|
+
# @param [Array<Symbol>] roles
|
28
|
+
# @return [Array<Symbol>] the expanded roles.
|
29
|
+
def expand_roles( *roles )
|
30
|
+
expand_roles_into( roles, [] )
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param [Symbol] the role to check.
|
34
|
+
# @return [Boolean] true if the role has been defined.
|
35
|
+
def role_defined?( role )
|
36
|
+
roles.key?( role.to_sym )
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def expand_roles_into( roles, expanded )
|
42
|
+
roles.each do |name|
|
43
|
+
name = name.to_sym
|
44
|
+
|
45
|
+
next unless role = self.roles[ name ]
|
46
|
+
expanded << name
|
47
|
+
|
48
|
+
role[ :inherits ].each do |inherited|
|
49
|
+
next if expanded.include?( inherited )
|
50
|
+
|
51
|
+
expanded << inherited
|
52
|
+
expand_roles_into( [ inherited ], expanded )
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
expanded
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Security
|
3
|
+
|
4
|
+
# Used to determine the roles that the current {Principal} should be given
|
5
|
+
# on a {Services::Service}.
|
6
|
+
module RolesService
|
7
|
+
|
8
|
+
# @!visibility private
|
9
|
+
def self.create( scorpion, * )
|
10
|
+
scorpion.new EmptyRolesService
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param [Principal] principal of the currently logged in user.
|
14
|
+
# @return [Array<Symbol>] the roles granted to the principal.
|
15
|
+
def roles_for( principal )
|
16
|
+
[]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Default {RolesService} always returns an empty set.
|
20
|
+
class EmptyRolesService
|
21
|
+
include RolesService
|
22
|
+
|
23
|
+
# (see RolesService#roles_for)
|
24
|
+
def roles_for( principal )
|
25
|
+
[]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Security
|
3
|
+
|
4
|
+
# Adds support for authorizing and querying security {Policy} to a
|
5
|
+
# {Services::Service}.
|
6
|
+
module Support
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
# ============================================================================
|
10
|
+
# @!group Dependencies
|
11
|
+
#
|
12
|
+
|
13
|
+
# @!attribute security_principal
|
14
|
+
# @return [Security::Principal] the principal offered to the service for
|
15
|
+
# policy resolution.
|
16
|
+
|
17
|
+
# @!attribute roles_service
|
18
|
+
# @return [Security::RolesService] a roles service to retrieve the roles
|
19
|
+
# granted to the {#security_principal}.
|
20
|
+
|
21
|
+
#
|
22
|
+
# @!endgroup Dependencies
|
23
|
+
|
24
|
+
included do
|
25
|
+
attr_dependency :security_principal, Security::Principal unless method_defined? :security_principal
|
26
|
+
attr_dependency :roles_service, Security::RolesService unless method_defined? :roles_service
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Policy] the security {Policy} for the service.
|
30
|
+
def policy
|
31
|
+
@policy ||= _policy_class.new(
|
32
|
+
principal: security_principal,
|
33
|
+
roles: roles_service.roles_for( security_principal )
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @!method authorize!( action, resource, additional_context = nil )
|
38
|
+
# @see Security::Policy#authorize!
|
39
|
+
# @return [resource]
|
40
|
+
|
41
|
+
# @!method permit?( action, resource, additional_context = nil )
|
42
|
+
# @see Policy#permit?
|
43
|
+
# @return [:yes, :maybe, false]
|
44
|
+
|
45
|
+
delegate :authorize!, :permit?, to: :policy
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def _policy_class
|
50
|
+
if service_policy_delegation?
|
51
|
+
delegate_policy_class
|
52
|
+
else
|
53
|
+
policy_class
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @!visibility public
|
58
|
+
#
|
59
|
+
# Override to declare the policy class to use for the service.
|
60
|
+
#
|
61
|
+
# @return [Class] a {Policy} class used to authorize actions.
|
62
|
+
def policy_class
|
63
|
+
fail Security::IncompleteSetupError, "No policy class defined. Override #policy_class in #{ self.class.name } to declare policy." # rubocop:disable Metrics/LineLength
|
64
|
+
end
|
65
|
+
|
66
|
+
# @!visibility public
|
67
|
+
#
|
68
|
+
# @return [Class] a {Policy} class used when
|
69
|
+
# {#service_policy_delegation?} is true.
|
70
|
+
def delegate_policy_class
|
71
|
+
NoPolicy
|
72
|
+
end
|
73
|
+
|
74
|
+
# @!visibility public
|
75
|
+
#
|
76
|
+
# @return [Boolean] true if the service has been asked to delegate
|
77
|
+
# policy checks to the upstream service and
|
78
|
+
def service_policy_delegation?
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Shamu
|
2
|
+
# {include:file:lib/shamu/security/README.md}
|
3
|
+
module Security
|
4
|
+
require "shamu/security/error"
|
5
|
+
require "shamu/security/principal"
|
6
|
+
require "shamu/security/policy"
|
7
|
+
require "shamu/security/policy_rule"
|
8
|
+
require "shamu/security/no_policy"
|
9
|
+
require "shamu/security/support"
|
10
|
+
require "shamu/security/roles"
|
11
|
+
require "shamu/security/roles_service"
|
12
|
+
require "shamu/security/hashed_value"
|
13
|
+
|
14
|
+
# See {.private_key}
|
15
|
+
ENV_PRIVATE_KEY = "SHAMU_PRIVATE_KEY".freeze
|
16
|
+
|
17
|
+
# @!attribute
|
18
|
+
#
|
19
|
+
# A strong key used to authenticate (not encrypt) input from untrusted
|
20
|
+
# sources (such as cookies, headers, etc).
|
21
|
+
#
|
22
|
+
# If the key has not been {#private_key= set then shamu will look for an
|
23
|
+
# environment variable named SHAMU_PRIVATE_KEY.
|
24
|
+
#
|
25
|
+
# ## To generate a strong key
|
26
|
+
#
|
27
|
+
# ```
|
28
|
+
# # 1024-bit private key
|
29
|
+
# key = SecureRandom.base64( 128 )
|
30
|
+
# ```
|
31
|
+
# @return [String]
|
32
|
+
def self.private_key
|
33
|
+
@private_key ||= ENV[ ENV_PRIVATE_KEY ] || fail( "No private key configured. Set Shamu::Security.private_key or add an the #{ ENV_PRIVATE_KEY } environment variable to the host." ) # rubocop:disable Metrics/LineLength
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param [String] key to use.
|
37
|
+
# @return [String]
|
38
|
+
def self.private_key=( key )
|
39
|
+
@private_key = key && Base64.decode64( key )
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|