shamu 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (207) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +26 -0
  3. data/.gitignore +2 -1
  4. data/.rubocop.yml +89 -30
  5. data/.yardopts +4 -5
  6. data/Gemfile +24 -12
  7. data/Guardfile +5 -0
  8. data/LABELS.md +22 -0
  9. data/README.md +41 -0
  10. data/Rakefile +12 -0
  11. data/circle.yml +7 -3
  12. data/config.ru +7 -0
  13. data/lib/shamu/active_record.rb +7 -0
  14. data/lib/shamu/attributes/assignment.rb +114 -0
  15. data/lib/shamu/attributes/equality.rb +40 -0
  16. data/lib/shamu/attributes/fluid_assignment.rb +49 -0
  17. data/lib/shamu/attributes/validation.rb +74 -0
  18. data/lib/shamu/attributes.rb +255 -0
  19. data/lib/shamu/auditing/README.md +0 -0
  20. data/lib/shamu/auditing/audit_record.rb +32 -0
  21. data/lib/shamu/auditing/auditing_service.rb +32 -0
  22. data/lib/shamu/auditing/list_scope.rb +22 -0
  23. data/lib/shamu/auditing/logging_auditing_service.rb +16 -0
  24. data/lib/shamu/auditing/support.rb +75 -0
  25. data/lib/shamu/auditing/transaction.rb +58 -0
  26. data/lib/shamu/auditing.rb +12 -0
  27. data/lib/shamu/entities/README.md +1 -0
  28. data/lib/shamu/entities/active_record.rb +123 -0
  29. data/lib/shamu/entities/active_record_soft_destroy.rb +91 -0
  30. data/lib/shamu/entities/entity.rb +196 -0
  31. data/lib/shamu/entities/entity_path.rb +87 -0
  32. data/lib/shamu/entities/identity_cache.rb +64 -0
  33. data/lib/shamu/entities/list.rb +54 -0
  34. data/lib/shamu/entities/list_scope/dates.rb +57 -0
  35. data/lib/shamu/entities/list_scope/paging.rb +51 -0
  36. data/lib/shamu/entities/list_scope/scoped_paging.rb +65 -0
  37. data/lib/shamu/entities/list_scope/sorting.rb +76 -0
  38. data/lib/shamu/entities/list_scope.rb +105 -0
  39. data/lib/shamu/entities/null_entity.rb +88 -0
  40. data/lib/shamu/entities.rb +11 -0
  41. data/lib/shamu/error.rb +23 -5
  42. data/lib/shamu/events/README.md +0 -0
  43. data/lib/shamu/events/active_record/channel.rb +36 -0
  44. data/lib/shamu/events/active_record/message.rb +52 -0
  45. data/lib/shamu/events/active_record/migration.rb +49 -0
  46. data/lib/shamu/events/active_record/runner.rb +28 -0
  47. data/lib/shamu/events/active_record/service.rb +174 -0
  48. data/lib/shamu/events/active_record.rb +13 -0
  49. data/lib/shamu/events/channel_stats.rb +23 -0
  50. data/lib/shamu/events/error.rb +24 -0
  51. data/lib/shamu/events/events_service.rb +136 -0
  52. data/lib/shamu/events/in_memory/async_service.rb +48 -0
  53. data/lib/shamu/events/in_memory/service.rb +97 -0
  54. data/lib/shamu/events/in_memory.rb +10 -0
  55. data/lib/shamu/events/message.rb +38 -0
  56. data/lib/shamu/events/support.rb +60 -0
  57. data/lib/shamu/events.rb +12 -0
  58. data/lib/shamu/features/README.md +0 -0
  59. data/lib/shamu/features/conditions/condition.rb +39 -0
  60. data/lib/shamu/features/conditions/env.rb +37 -0
  61. data/lib/shamu/features/conditions/hosts.rb +25 -0
  62. data/lib/shamu/features/conditions/matching.rb +16 -0
  63. data/lib/shamu/features/conditions/not_matching.rb +16 -0
  64. data/lib/shamu/features/conditions/percentage.rb +44 -0
  65. data/lib/shamu/features/conditions/proc.rb +54 -0
  66. data/lib/shamu/features/conditions/roles.rb +23 -0
  67. data/lib/shamu/features/conditions/schedule_at.rb +27 -0
  68. data/lib/shamu/features/conditions.rb +18 -0
  69. data/lib/shamu/features/config_service.rb +10 -0
  70. data/lib/shamu/features/context.rb +80 -0
  71. data/lib/shamu/features/env_store.rb +88 -0
  72. data/lib/shamu/features/errors.rb +29 -0
  73. data/lib/shamu/features/features_service.rb +168 -0
  74. data/lib/shamu/features/list_scope.rb +30 -0
  75. data/lib/shamu/features/selector.rb +50 -0
  76. data/lib/shamu/features/support.rb +51 -0
  77. data/lib/shamu/features/toggle.rb +149 -0
  78. data/lib/shamu/features/toggle_codec.rb +69 -0
  79. data/lib/shamu/features.rb +16 -0
  80. data/lib/shamu/locale/en.yml +22 -2
  81. data/lib/shamu/logger.rb +13 -0
  82. data/lib/shamu/rack/README.md +0 -0
  83. data/lib/shamu/rack/cookies.rb +115 -0
  84. data/lib/shamu/rack/cookies_middleware.rb +26 -0
  85. data/lib/shamu/rack/query_params.rb +41 -0
  86. data/lib/shamu/rack/query_params_middleware.rb +24 -0
  87. data/lib/shamu/rack.rb +12 -0
  88. data/lib/shamu/rails/controller.rb +131 -0
  89. data/lib/shamu/rails/entity.rb +168 -0
  90. data/lib/shamu/rails/features.rb +13 -0
  91. data/lib/shamu/rails/railtie.rb +30 -0
  92. data/lib/shamu/rails.rb +10 -0
  93. data/lib/shamu/rspec/matchers.rb +44 -0
  94. data/lib/shamu/rspec.rb +1 -0
  95. data/lib/shamu/security/README.md +0 -0
  96. data/lib/shamu/security/active_record_policy.rb +106 -0
  97. data/lib/shamu/security/error.rb +65 -0
  98. data/lib/shamu/security/hashed_value.rb +71 -0
  99. data/lib/shamu/security/no_policy.rb +15 -0
  100. data/lib/shamu/security/policy.rb +289 -0
  101. data/lib/shamu/security/policy_refinement.rb +50 -0
  102. data/lib/shamu/security/policy_rule.rb +59 -0
  103. data/lib/shamu/security/principal.rb +72 -0
  104. data/lib/shamu/security/roles.rb +62 -0
  105. data/lib/shamu/security/roles_service.rb +30 -0
  106. data/lib/shamu/security/support.rb +83 -0
  107. data/lib/shamu/security.rb +43 -0
  108. data/lib/shamu/services/README.md +2 -0
  109. data/lib/shamu/services/active_record.rb +58 -0
  110. data/lib/shamu/services/active_record_crud.rb +378 -0
  111. data/lib/shamu/services/error.rb +24 -0
  112. data/lib/shamu/services/lazy_association.rb +31 -0
  113. data/lib/shamu/services/lazy_transform.rb +97 -0
  114. data/lib/shamu/services/request.rb +122 -0
  115. data/lib/shamu/services/request_support.rb +124 -0
  116. data/lib/shamu/services/result.rb +75 -0
  117. data/lib/shamu/services/service.rb +355 -0
  118. data/lib/shamu/services.rb +12 -0
  119. data/lib/shamu/sessions/README.md +2 -0
  120. data/lib/shamu/sessions/cookie_store.rb +79 -0
  121. data/lib/shamu/sessions/session_store.rb +42 -0
  122. data/lib/shamu/sessions.rb +8 -0
  123. data/lib/shamu/to_bool_extension.rb +57 -0
  124. data/lib/shamu/to_model_id_extension.rb +50 -0
  125. data/lib/shamu/version.rb +10 -4
  126. data/lib/shamu.rb +18 -6
  127. data/shamu.gemspec +21 -10
  128. data/spec/internal/README.md +4 -0
  129. data/spec/internal/config/database.yml +3 -0
  130. data/spec/internal/config/routes.rb +3 -0
  131. data/spec/internal/db/schema.rb +3 -0
  132. data/spec/internal/log/.gitignore +1 -0
  133. data/spec/internal/public/favicon.ico +0 -0
  134. data/spec/lib/shamu/active_record_support.rb +32 -0
  135. data/spec/lib/shamu/attributes/assignment_spec.rb +129 -0
  136. data/spec/lib/shamu/attributes/equality_spec.rb +63 -0
  137. data/spec/lib/shamu/attributes/fluid_assignment_spec.rb +31 -0
  138. data/spec/lib/shamu/attributes/validation_spec.rb +53 -0
  139. data/spec/lib/shamu/attributes_spec.rb +331 -0
  140. data/spec/lib/shamu/auditing/logging_auditing_service_spec.rb +18 -0
  141. data/spec/lib/shamu/auditing/support_spec.rb +41 -0
  142. data/spec/lib/shamu/entities/active_record_soft_destroy_spec.rb +82 -0
  143. data/spec/lib/shamu/entities/active_record_spec.rb +66 -0
  144. data/spec/lib/shamu/entities/entity_path_spec.rb +40 -0
  145. data/spec/lib/shamu/entities/entity_spec.rb +56 -0
  146. data/spec/lib/shamu/entities/identity_cache_spec.rb +69 -0
  147. data/spec/lib/shamu/entities/list_scope/dates_spec.rb +47 -0
  148. data/spec/lib/shamu/entities/list_scope/paging_spec.rb +41 -0
  149. data/spec/lib/shamu/entities/list_scope/scoped_paging_spec.rb +40 -0
  150. data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +59 -0
  151. data/spec/lib/shamu/entities/list_scope_spec.rb +127 -0
  152. data/spec/lib/shamu/entities/list_spec.rb +60 -0
  153. data/spec/lib/shamu/entities/null_entity_spec.rb +94 -0
  154. data/spec/lib/shamu/events/active_record/migration_spec.rb +11 -0
  155. data/spec/lib/shamu/events/active_record/service_spec.rb +139 -0
  156. data/spec/lib/shamu/events/events_service_spec.rb +57 -0
  157. data/spec/lib/shamu/events/in_memory/async_service_spec.rb +37 -0
  158. data/spec/lib/shamu/events/in_memory/service_spec.rb +36 -0
  159. data/spec/lib/shamu/events/message_spec.rb +7 -0
  160. data/spec/lib/shamu/events/support_spec.rb +44 -0
  161. data/spec/lib/shamu/features/conditions/condition_spec.rb +8 -0
  162. data/spec/lib/shamu/features/conditions/env_spec.rb +29 -0
  163. data/spec/lib/shamu/features/conditions/hosts_spec.rb +21 -0
  164. data/spec/lib/shamu/features/conditions/matching_spec.rb +23 -0
  165. data/spec/lib/shamu/features/conditions/percentage_spec.rb +71 -0
  166. data/spec/lib/shamu/features/conditions/proc_spec.rb +28 -0
  167. data/spec/lib/shamu/features/env_store_spec.rb +48 -0
  168. data/spec/lib/shamu/features/features.yml +34 -0
  169. data/spec/lib/shamu/features/features_service_spec.rb +109 -0
  170. data/spec/lib/shamu/features/secondary.yml +5 -0
  171. data/spec/lib/shamu/features/selector_spec.rb +17 -0
  172. data/spec/lib/shamu/features/support_spec.rb +45 -0
  173. data/spec/lib/shamu/features/toggle_codec_spec.rb +28 -0
  174. data/spec/lib/shamu/features/toggle_spec.rb +42 -0
  175. data/spec/lib/shamu/rack/cookies_middleware_spec.rb +33 -0
  176. data/spec/lib/shamu/rack/cookies_spec.rb +43 -0
  177. data/spec/lib/shamu/rack/query_params_middleware_spec.rb +33 -0
  178. data/spec/lib/shamu/rack/query_params_spec.rb +23 -0
  179. data/spec/lib/shamu/rails/controller_spec.rb +74 -0
  180. data/spec/lib/shamu/rails/entity_spec.rb +150 -0
  181. data/spec/lib/shamu/rails/features.yml +13 -0
  182. data/spec/lib/shamu/rails/features_spec.rb +45 -0
  183. data/spec/lib/shamu/security/active_record_policy_spec.rb +38 -0
  184. data/spec/lib/shamu/security/hashed_value_spec.rb +41 -0
  185. data/spec/lib/shamu/security/policy_refinement_spec.rb +61 -0
  186. data/spec/lib/shamu/security/policy_rule_spec.rb +60 -0
  187. data/spec/lib/shamu/security/policy_spec.rb +158 -0
  188. data/spec/lib/shamu/security/roles_spec.rb +46 -0
  189. data/spec/lib/shamu/services/active_record_crud_spec.rb +460 -0
  190. data/spec/lib/shamu/services/active_record_spec.rb +92 -0
  191. data/spec/lib/shamu/services/lazy_association_spec.rb +31 -0
  192. data/spec/lib/shamu/services/lazy_transform_spec.rb +96 -0
  193. data/spec/lib/shamu/services/request_spec.rb +58 -0
  194. data/spec/lib/shamu/services/request_support_spec.rb +129 -0
  195. data/spec/lib/shamu/services/result_spec.rb +37 -0
  196. data/spec/lib/shamu/services/service_spec.rb +307 -0
  197. data/spec/lib/shamu/sessions/cookie_store_spec.rb +44 -0
  198. data/spec/lib/shamu/to_bool_extension_spec.rb +67 -0
  199. data/spec/lib/shamu/to_model_id_extension_spec.rb +54 -0
  200. data/spec/rails_helper.rb +13 -0
  201. data/spec/spec_helper.rb +17 -12
  202. data/spec/support/active_record.rb +17 -0
  203. data/spec/support/database.rb +14 -0
  204. data/spec/support/logger.rb +0 -0
  205. metadata +383 -9
  206. data/spec/lib/shamu_spec.rb +0 -5
  207. /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
@@ -0,0 +1,2 @@
1
+ Services expose a clearly defined interface for managing {Entities::Entity} objects and
2
+ external resources.