sequel-privacy 0.5.2 → 0.5.4
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 +4 -4
- data/README.md +28 -20
- data/lib/sequel/plugins/privacy.rb +40 -138
- data/lib/sequel/privacy/cache.rb +4 -6
- data/lib/sequel/privacy/enforcer.rb +6 -22
- data/lib/sequel/privacy/i_actor.rb +1 -2
- data/lib/sequel/privacy/policy.rb +10 -13
- data/lib/sequel/privacy/policy_dsl.rb +5 -13
- data/lib/sequel/privacy/version.rb +1 -1
- data/lib/sequel/privacy/viewer_context.rb +7 -13
- data/lib/sequel-privacy.rb +3 -5
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 54198780e9744c541e3915d3ac5722ccb16b119a77729b5013b854e698604511
|
|
4
|
+
data.tar.gz: 5219bec34c9542ce88f928bc39c1998586e6ed2c15637296dead03d958e12db1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8673ccf1e4e6cb592299c9a930ee7ff779e0343e66753a245f71f4aa26d21906c486dc1435d55af53c1c8b09c8166f24fec2e8036ce3f962598df4d8a29cadf9
|
|
7
|
+
data.tar.gz: 34bc11e989421e4cd7d5207fc531d7bcf17cf681c8b10c3104918c12d5d2a9eb8cdf68274dd64397c8b49ba51b6c59b3de4fd4ef73e8acbf06b1fdd671039cce
|
data/README.md
CHANGED
|
@@ -110,13 +110,12 @@ member.phone # => nil if :view_phone denies
|
|
|
110
110
|
|
|
111
111
|
## Policy Definition
|
|
112
112
|
|
|
113
|
-
Policies are lambdas that execute in the context of an `Actions`
|
|
113
|
+
Policies are lambdas that execute in the context of an `Actions` class, giving access to `allow`, `deny`, and `pass` outcome methods, as well as the `all` combinator. `allow` and `deny` will end evaluation of the chain of policies, whereas `pass` will continue to the next policy in the chain.
|
|
114
114
|
|
|
115
|
-
Policies are **actor-first**. Arities map to:
|
|
116
115
|
- 0 args — global decision (`-> { allow if Time.now.sunday? }`)
|
|
117
|
-
- 1 arg — `(actor)`: role / identity checks
|
|
118
|
-
- 2 args — `(actor, subject)`:
|
|
119
|
-
- 3 args — `(actor, subject, direct_object)`: "
|
|
116
|
+
- 1 arg — `(actor)`: Useful for role / identity checks
|
|
117
|
+
- 2 args — `(actor, subject)`: General purpose relationship checks
|
|
118
|
+
- 3 args — `(actor, subject, direct_object)`: "Allow members to remove themselves from a group they're in"
|
|
120
119
|
|
|
121
120
|
Policies of arity ≥ 1 auto-deny for anonymous viewers (nil actor). Use `allow_anonymous: true` to opt out — meant for state-gate policies that examine only the subject.
|
|
122
121
|
|
|
@@ -175,7 +174,7 @@ policy :MyPolicy, ->() { ... },
|
|
|
175
174
|
|
|
176
175
|
**`cacheable: true`** (default): Results are cached for the duration of the request, keyed by policy + arguments. Use for policies that don't depend on mutable state.
|
|
177
176
|
|
|
178
|
-
**`single_match: true`**: Optimization for policies for which there is only one matching Actor possible for a given Subject. For example in `AllowAuthors`, since a `Post` can have only one
|
|
177
|
+
**`single_match: true`**: Optimization for policies for which there is only one matching Actor possible for a given Subject. For example in `AllowAuthors`, since a `Post` can have only one author, it's not worth a potentially expensive check on other combinations once you've found the winner.
|
|
179
178
|
|
|
180
179
|
**`cache_by:`** (Symbol or Array of `:actor`, `:subject`, `:direct_object`): Override the cache-key dimensions. By default the key uses every input the policy receives. Pass a subset when the policy ignores some of its inputs — e.g. `AllowAdmins` takes `(actor, subject)` but only examines actor, so `cache_by: :actor` shares one entry across subjects.
|
|
181
180
|
|
|
@@ -217,9 +216,28 @@ All-Powerful VCs bypass all privacy checks and are used in situations where the
|
|
|
217
216
|
to models. In a production setting, your application should prohibit raw Database access outside of the privacy-aware
|
|
218
217
|
system, so these VCs give you an escape hatch for things like scripts while also keeping an audit trail.
|
|
219
218
|
|
|
220
|
-
`omniscient` and `all_powerful` require a reason
|
|
219
|
+
`omniscient` and `all_powerful` require a reason, given as a Symbol, for audit logging.
|
|
221
220
|
You could also create lint rules that prevent the casual creation of these viewer contexts.
|
|
222
221
|
|
|
222
|
+
```ruby
|
|
223
|
+
# Standard viewer (most common)
|
|
224
|
+
users_groups = Group.for_vc(current_vc).where(creator: current_user).all
|
|
225
|
+
|
|
226
|
+
# Anonymous viewer (logged-out users)
|
|
227
|
+
logged_out_vc = Sequel::Privacy::ViewerContext.anonymous
|
|
228
|
+
posts = Post.for_vc(logged_out_vc).where(published: true).all
|
|
229
|
+
|
|
230
|
+
# All-powerful ViewerContexts dangerously bypass all read and write checks.
|
|
231
|
+
admin_vc = Sequel::Privacy::ViewerContext.all_powerful(:admin_migration)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Login, Sessions & `current_user` and `current_vc`
|
|
235
|
+
|
|
236
|
+
Unless you allow unsafe access to your User (or equivalent) model, you will need
|
|
237
|
+
a way to load it and create a ViewerContext for them. An Omniscient ViewerContext
|
|
238
|
+
is useful for this. Be sure to properly set to an Actor VC after you've logged-in
|
|
239
|
+
or materialized a user from the session.
|
|
240
|
+
|
|
223
241
|
```ruby
|
|
224
242
|
# You can use an omniscient viewer context to load the user from a session
|
|
225
243
|
# or however you store them. Discard this viewer context when you're done with it.
|
|
@@ -235,18 +253,9 @@ def current_user
|
|
|
235
253
|
end
|
|
236
254
|
|
|
237
255
|
def current_vc
|
|
238
|
-
current_user&.
|
|
256
|
+
current_user&.viewer_context || Sequel::Privacy::ViewerContext.anonymous()
|
|
239
257
|
end
|
|
240
258
|
|
|
241
|
-
# Standard viewer (most common)
|
|
242
|
-
users_groups = Group.for_vc(current_vc).where(creator: current_user).all
|
|
243
|
-
|
|
244
|
-
# Anonymous viewer (logged-out users)
|
|
245
|
-
logged_out_vc = Sequel::Privacy::ViewerContext.anonymous
|
|
246
|
-
posts = Post.for_vc(logged_out_vc).where(published: true).all
|
|
247
|
-
|
|
248
|
-
# All-powerful ViewerContexts dangerously bypass all read and write checks.
|
|
249
|
-
admin_vc = Sequel::Privacy::ViewerContext.all_powerful(:admin_migration)
|
|
250
259
|
```
|
|
251
260
|
|
|
252
261
|
## Mutation Enforcement
|
|
@@ -378,11 +387,10 @@ class PrivacyCacheMiddleware
|
|
|
378
387
|
end
|
|
379
388
|
```
|
|
380
389
|
|
|
381
|
-
Or manually:
|
|
390
|
+
Or somewhere manually, like at the top of your Roda or Sinatra app:
|
|
382
391
|
|
|
383
392
|
```ruby
|
|
384
|
-
Sequel::Privacy.
|
|
385
|
-
Sequel::Privacy.single_matches.clear
|
|
393
|
+
Sequel::Privacy.clear_cache!
|
|
386
394
|
```
|
|
387
395
|
|
|
388
396
|
## Actor Interface
|
|
@@ -36,7 +36,6 @@ module Sequel
|
|
|
36
36
|
module Privacy
|
|
37
37
|
extend T::Sig
|
|
38
38
|
|
|
39
|
-
# Called once when plugin first loads on a model
|
|
40
39
|
sig { params(model: T.class_of(Sequel::Model), _opts: T::Hash[Symbol, T.untyped]).void }
|
|
41
40
|
def self.apply(model, _opts = {})
|
|
42
41
|
model.instance_variable_set(:@privacy_policies, {})
|
|
@@ -46,13 +45,9 @@ module Sequel
|
|
|
46
45
|
model.instance_variable_set(:@allow_unsafe_access, false)
|
|
47
46
|
end
|
|
48
47
|
|
|
49
|
-
# Called every time plugin loads (for per-model configuration)
|
|
50
48
|
sig { params(model: T.class_of(Sequel::Model), opts: T::Hash[Symbol, T.untyped]).void }
|
|
51
|
-
def self.configure(model, opts = {})
|
|
52
|
-
# Currently no per-model configuration needed
|
|
53
|
-
end
|
|
49
|
+
def self.configure(model, opts = {}); end
|
|
54
50
|
|
|
55
|
-
# DSL class for defining association-level privacy policies
|
|
56
51
|
class AssociationPrivacyDSL
|
|
57
52
|
extend T::Sig
|
|
58
53
|
|
|
@@ -67,7 +62,6 @@ module Sequel
|
|
|
67
62
|
@pending_policies = T.let({}, T::Hash[Symbol, T::Array[T.untyped]])
|
|
68
63
|
end
|
|
69
64
|
|
|
70
|
-
# Define policies for association actions (:add, :remove, :remove_all)
|
|
71
65
|
sig { params(action: Symbol, policies: T.untyped).void }
|
|
72
66
|
def can(action, *policies)
|
|
73
67
|
unless %i[add remove remove_all].include?(action)
|
|
@@ -80,17 +74,15 @@ module Sequel
|
|
|
80
74
|
T.must(@pending_policies[action]).concat(resolved)
|
|
81
75
|
end
|
|
82
76
|
|
|
83
|
-
# Called after the association block is evaluated to register all policies at once
|
|
84
77
|
sig { void }
|
|
85
78
|
def finalize_association!
|
|
86
79
|
@pending_policies.each do |action, policies|
|
|
87
|
-
@model_class.register_association_policies(@assoc_name, action, policies
|
|
80
|
+
@model_class.register_association_policies(@assoc_name, action, policies)
|
|
88
81
|
end
|
|
89
82
|
@model_class.setup_association_privacy(@assoc_name)
|
|
90
83
|
end
|
|
91
84
|
end
|
|
92
85
|
|
|
93
|
-
# DSL class for defining privacy policies in a block
|
|
94
86
|
class PrivacyDSL
|
|
95
87
|
extend T::Sig
|
|
96
88
|
|
|
@@ -99,14 +91,12 @@ module Sequel
|
|
|
99
91
|
@model_class = model_class
|
|
100
92
|
end
|
|
101
93
|
|
|
102
|
-
# Define policies for an action
|
|
103
94
|
sig { params(action: Symbol, policies: T.untyped).void }
|
|
104
95
|
def can(action, *policies)
|
|
105
96
|
resolved = resolve_policies(policies)
|
|
106
97
|
@model_class.register_policies(action, resolved)
|
|
107
98
|
end
|
|
108
99
|
|
|
109
|
-
# Define a protected field with its policies
|
|
110
100
|
sig { params(name: Symbol, policies: T.untyped).void }
|
|
111
101
|
def field(name, *policies)
|
|
112
102
|
resolved = resolve_policies(policies)
|
|
@@ -115,14 +105,6 @@ module Sequel
|
|
|
115
105
|
@model_class.register_protected_field(name, policy_name)
|
|
116
106
|
end
|
|
117
107
|
|
|
118
|
-
# Define policies for an association
|
|
119
|
-
#
|
|
120
|
-
# Example:
|
|
121
|
-
# association :members do
|
|
122
|
-
# can :add, AllowGroupAdmin, AllowSelfJoin
|
|
123
|
-
# can :remove, AllowGroupAdmin, AllowSelfRemove
|
|
124
|
-
# can :remove_all, AllowGroupAdmin
|
|
125
|
-
# end
|
|
126
108
|
sig { params(name: Symbol, block: T.proc.bind(AssociationPrivacyDSL).void).void }
|
|
127
109
|
def association(name, &block)
|
|
128
110
|
resolver = ->(policies) { resolve_policies(policies) }
|
|
@@ -131,7 +113,6 @@ module Sequel
|
|
|
131
113
|
dsl.finalize_association!
|
|
132
114
|
end
|
|
133
115
|
|
|
134
|
-
# Finalize privacy settings (no more changes allowed)
|
|
135
116
|
sig { void }
|
|
136
117
|
def finalize!
|
|
137
118
|
@model_class.finalize_privacy!
|
|
@@ -158,7 +139,6 @@ module Sequel
|
|
|
158
139
|
|
|
159
140
|
requires_ancestor { T.class_of(Sequel::Model) }
|
|
160
141
|
|
|
161
|
-
# Register inherited instance variables for proper subclass handling
|
|
162
142
|
Sequel::Plugins.inherited_instance_variables(
|
|
163
143
|
self,
|
|
164
144
|
:@privacy_policies => :dup,
|
|
@@ -168,12 +148,8 @@ module Sequel
|
|
|
168
148
|
:@allow_unsafe_access => nil
|
|
169
149
|
)
|
|
170
150
|
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
174
|
-
|
|
175
|
-
# Allow this model to be accessed without a ViewerContext.
|
|
176
|
-
# Use during migration to gradually enable strict mode.
|
|
151
|
+
# Allows the model to be accessed without a ViewerContext,
|
|
152
|
+
# useful when you're migrating an existing codebase or adopting gradually.
|
|
177
153
|
sig { void }
|
|
178
154
|
def allow_unsafe_access!
|
|
179
155
|
@allow_unsafe_access = T.let(true, T.nilable(T::Boolean))
|
|
@@ -185,20 +161,15 @@ module Sequel
|
|
|
185
161
|
@allow_unsafe_access == true
|
|
186
162
|
end
|
|
187
163
|
|
|
188
|
-
#
|
|
164
|
+
# Per-class thread-local key carrying the current VC during row
|
|
165
|
+
# materialization.
|
|
189
166
|
sig { returns(Symbol) }
|
|
190
167
|
def privacy_vc_key
|
|
191
168
|
:"#{self}_privacy_vc"
|
|
192
169
|
end
|
|
193
170
|
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
# Model.first, Model.all, associations, ...). This checks that a
|
|
197
|
-
# VC is in scope (or that the class opted out via
|
|
198
|
-
# allow_unsafe_access!) and defers VC attachment and :view
|
|
199
|
-
# filtering to DatasetMethods#row_proc — those are per-row
|
|
200
|
-
# concerns and have context-dependent bypasses (policy
|
|
201
|
-
# evaluation, eager-load attachment) that don't belong here.
|
|
171
|
+
# The primary integration point; every Sequel::Model materialization
|
|
172
|
+
# flows through here.
|
|
202
173
|
sig { params(values: T.untyped).returns(T.nilable(Sequel::Model)) }
|
|
203
174
|
def call(values)
|
|
204
175
|
vc = Thread.current[privacy_vc_key]
|
|
@@ -211,10 +182,6 @@ module Sequel
|
|
|
211
182
|
super
|
|
212
183
|
end
|
|
213
184
|
|
|
214
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
215
|
-
# Policy Definition DSL
|
|
216
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
217
|
-
|
|
218
185
|
sig { returns(T::Hash[Symbol, T::Array[T.untyped]]) }
|
|
219
186
|
def privacy_policies
|
|
220
187
|
@privacy_policies ||= T.let({}, T.nilable(T::Hash[Symbol, T::Array[T.untyped]]))
|
|
@@ -225,7 +192,6 @@ module Sequel
|
|
|
225
192
|
@privacy_fields ||= T.let({}, T.nilable(T::Hash[Symbol, Symbol]))
|
|
226
193
|
end
|
|
227
194
|
|
|
228
|
-
# Returns association policies: { assoc_name => { action => [policies] } }
|
|
229
195
|
sig { returns(T::Hash[Symbol, T::Hash[Symbol, T::Array[T.untyped]]]) }
|
|
230
196
|
def privacy_association_policies
|
|
231
197
|
@privacy_association_policies ||= T.let({}, T.nilable(T::Hash[Symbol, T::Hash[Symbol, T::Array[T.untyped]]]))
|
|
@@ -236,11 +202,9 @@ module Sequel
|
|
|
236
202
|
@privacy_finalized == true
|
|
237
203
|
end
|
|
238
204
|
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
# @yield Block evaluated in context of PrivacyDSL
|
|
205
|
+
# Entry point for the privacy DSL. The block is evaluated in the
|
|
206
|
+
# context of a `PrivacyDSL` instance:
|
|
242
207
|
#
|
|
243
|
-
# Example:
|
|
244
208
|
# privacy do
|
|
245
209
|
# can :view, P::AllowMembers
|
|
246
210
|
# can :edit, P::AllowSelf, P::AllowAdmins
|
|
@@ -256,7 +220,6 @@ module Sequel
|
|
|
256
220
|
dsl.instance_eval(&block)
|
|
257
221
|
end
|
|
258
222
|
|
|
259
|
-
# Register policies for an action (called by PrivacyDSL)
|
|
260
223
|
sig { params(action: Symbol, policies: T::Array[T.untyped]).void }
|
|
261
224
|
def register_policies(action, policies)
|
|
262
225
|
if privacy_finalized?
|
|
@@ -267,7 +230,6 @@ module Sequel
|
|
|
267
230
|
T.must(privacy_policies[action]).concat(policies)
|
|
268
231
|
end
|
|
269
232
|
|
|
270
|
-
# Register a protected field (called by PrivacyDSL)
|
|
271
233
|
sig { params(field: Symbol, policy_name: Symbol).void }
|
|
272
234
|
def register_protected_field(field, policy_name)
|
|
273
235
|
if privacy_finalized?
|
|
@@ -276,13 +238,9 @@ module Sequel
|
|
|
276
238
|
|
|
277
239
|
privacy_fields[field] = policy_name
|
|
278
240
|
|
|
279
|
-
# Store original method
|
|
280
241
|
original_method = instance_method(field)
|
|
281
242
|
|
|
282
|
-
# Override the field getter
|
|
283
243
|
define_method(field) do
|
|
284
|
-
# During nested policy evaluation, return raw value without
|
|
285
|
-
# checking the field's view policy.
|
|
286
244
|
return original_method.bind(self).() if Sequel::Privacy::Enforcer.in_policy_eval?
|
|
287
245
|
|
|
288
246
|
vc = instance_variable_get(:@viewer_context)
|
|
@@ -301,23 +259,20 @@ module Sequel
|
|
|
301
259
|
end
|
|
302
260
|
end
|
|
303
261
|
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
sig { params(assoc_name: Symbol, action: Symbol, policies: T::Array[T.untyped]
|
|
307
|
-
def register_association_policies(assoc_name, action, policies
|
|
262
|
+
# The caller is responsible for invoking `setup_association_privacy`
|
|
263
|
+
# once all actions have been registered.
|
|
264
|
+
sig { params(assoc_name: Symbol, action: Symbol, policies: T::Array[T.untyped]).void }
|
|
265
|
+
def register_association_policies(assoc_name, action, policies)
|
|
308
266
|
Kernel.raise "Privacy policies have been finalized for #{self}" if privacy_finalized?
|
|
309
267
|
|
|
310
268
|
privacy_association_policies[assoc_name] ||= {}
|
|
311
269
|
assoc_hash = T.must(privacy_association_policies[assoc_name])
|
|
312
270
|
assoc_hash[action] ||= []
|
|
313
271
|
T.must(assoc_hash[action]).concat(policies)
|
|
314
|
-
|
|
315
|
-
# Set up the association method overrides if the association exists (unless deferred)
|
|
316
|
-
setup_association_privacy(assoc_name) if !defer_setup && association_reflection(assoc_name)
|
|
317
272
|
end
|
|
318
273
|
|
|
319
|
-
#
|
|
320
|
-
#
|
|
274
|
+
# Wraps add_*/remove_*/remove_all_* methods on an association
|
|
275
|
+
# with privacy checks. Idempotent.
|
|
321
276
|
sig { params(assoc_name: Symbol).void }
|
|
322
277
|
def setup_association_privacy(assoc_name)
|
|
323
278
|
assoc_policies = privacy_association_policies[assoc_name]
|
|
@@ -326,47 +281,40 @@ module Sequel
|
|
|
326
281
|
reflection = association_reflection(assoc_name)
|
|
327
282
|
return unless reflection
|
|
328
283
|
|
|
329
|
-
# Track which associations have been wrapped to avoid double-wrapping
|
|
330
284
|
@_wrapped_associations ||= T.let({}, T.nilable(T::Hash[Symbol, T::Boolean]))
|
|
331
285
|
return if @_wrapped_associations[assoc_name]
|
|
332
286
|
|
|
333
287
|
@_wrapped_associations[assoc_name] = true
|
|
334
288
|
|
|
335
|
-
#
|
|
336
|
-
#
|
|
337
|
-
#
|
|
289
|
+
# Sequel derives mutator names by stripping a trailing 's' from
|
|
290
|
+
# the association name: many_to_many :members → add_member,
|
|
291
|
+
# one_to_many :memberships → add_membership.
|
|
292
|
+
#
|
|
293
|
+
# TODO: I'm not sure if this will break sometimes.
|
|
338
294
|
singular_name = reflection[:name].to_s.chomp('s').to_sym
|
|
339
295
|
|
|
340
|
-
# Wrap add_* method if :add policy exists
|
|
341
296
|
add_policies = assoc_policies[:add]
|
|
342
297
|
if add_policies && method_defined?(:"add_#{singular_name}")
|
|
343
298
|
_wrap_association_add(assoc_name, singular_name, add_policies)
|
|
344
299
|
end
|
|
345
300
|
|
|
346
|
-
# Wrap remove_* method if :remove policy exists
|
|
347
301
|
remove_policies = assoc_policies[:remove]
|
|
348
302
|
if remove_policies && method_defined?(:"remove_#{singular_name}")
|
|
349
303
|
_wrap_association_remove(assoc_name, singular_name, remove_policies)
|
|
350
304
|
end
|
|
351
305
|
|
|
352
|
-
# Wrap remove_all_* method if :remove_all policy exists
|
|
353
306
|
remove_all_policies = assoc_policies[:remove_all]
|
|
354
307
|
return unless remove_all_policies && method_defined?(:"remove_all_#{reflection[:name]}")
|
|
355
308
|
|
|
356
309
|
_wrap_association_remove_all(assoc_name, reflection[:name], remove_all_policies)
|
|
357
310
|
end
|
|
358
311
|
|
|
359
|
-
#
|
|
360
|
-
# TODO: Explore automatic finalization on first query
|
|
312
|
+
# TODO: explore automatic finalization on first query.
|
|
361
313
|
sig { void }
|
|
362
314
|
def finalize_privacy!
|
|
363
315
|
@privacy_finalized = T.let(true, T.nilable(T::Boolean))
|
|
364
316
|
end
|
|
365
317
|
|
|
366
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
367
|
-
# Deprecated Methods (for backwards compatibility)
|
|
368
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
369
|
-
|
|
370
318
|
# @deprecated Use `privacy do; can :action, ...; end` instead
|
|
371
319
|
sig { params(action: Symbol, policy_chain: T.untyped).void }
|
|
372
320
|
def policies(action, *policy_chain)
|
|
@@ -379,7 +327,6 @@ module Sequel
|
|
|
379
327
|
def protect_field(field, policy: nil)
|
|
380
328
|
Kernel.warn "DEPRECATED: #{self}.protect_field is deprecated. Use `privacy do; field :#{field}, ...; end` instead"
|
|
381
329
|
policy_name = policy || :"view_#{field}"
|
|
382
|
-
# Need to also register the policy if not already defined
|
|
383
330
|
register_protected_field(field, policy_name)
|
|
384
331
|
end
|
|
385
332
|
|
|
@@ -389,19 +336,11 @@ module Sequel
|
|
|
389
336
|
dataset.for_vc(vc)
|
|
390
337
|
end
|
|
391
338
|
|
|
392
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
393
|
-
# Association Privacy (hooks into association creation)
|
|
394
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
395
|
-
|
|
396
|
-
# Override Sequel's associate method to wrap associations with privacy checks
|
|
397
339
|
sig { params(type: Symbol, name: Symbol, opts: T.untyped, block: T.untyped).returns(T.untyped) }
|
|
398
340
|
def associate(type, name, opts = {}, &block)
|
|
399
341
|
opts = _inject_privacy_eager_block(opts)
|
|
400
|
-
|
|
401
|
-
# Call original to create the association
|
|
402
342
|
result = super
|
|
403
343
|
|
|
404
|
-
# Wrap the association method with privacy checks
|
|
405
344
|
case type
|
|
406
345
|
when :many_to_one, :one_to_one
|
|
407
346
|
_override_singular_association(name)
|
|
@@ -409,18 +348,15 @@ module Sequel
|
|
|
409
348
|
when :one_to_many, :many_to_many
|
|
410
349
|
_override_plural_association(name)
|
|
411
350
|
_override_association_dataset(name)
|
|
412
|
-
# Check if there are already privacy policies defined for this association
|
|
413
351
|
setup_association_privacy(name) if privacy_association_policies[name]
|
|
414
352
|
end
|
|
415
353
|
|
|
416
354
|
result
|
|
417
355
|
end
|
|
418
356
|
|
|
419
|
-
#
|
|
420
|
-
#
|
|
421
|
-
#
|
|
422
|
-
# so that it's only applied during eager loading, not during the
|
|
423
|
-
# lazy association reader path (which has its own handling).
|
|
357
|
+
# Inject an :eager_block that wraps the eager-load dataset with
|
|
358
|
+
# `for_vc` when a VC is propagated via EAGER_VC_KEY (see
|
|
359
|
+
# DatasetMethods#post_load). Preserves any user-supplied block.
|
|
424
360
|
sig { params(opts: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
425
361
|
def _inject_privacy_eager_block(opts)
|
|
426
362
|
original = opts[:eager_block]
|
|
@@ -465,15 +401,13 @@ module Sequel
|
|
|
465
401
|
def _override_singular_association(name)
|
|
466
402
|
original = instance_method(name)
|
|
467
403
|
assoc_reflection = association_reflection(name)
|
|
404
|
+
# Resolve lazily to handle forward references between models.
|
|
468
405
|
assoc_class = T.let(nil, T.nilable(T.class_of(Sequel::Model)))
|
|
469
406
|
|
|
470
407
|
define_method(name) do
|
|
471
408
|
vc = instance_variable_get(:@viewer_context)
|
|
472
|
-
|
|
473
|
-
# Determine associated class (lazily, to handle forward references)
|
|
474
409
|
assoc_class ||= assoc_reflection.associated_class
|
|
475
410
|
|
|
476
|
-
# Load association with VC context set if available
|
|
477
411
|
obj = if vc && assoc_class.respond_to?(:privacy_vc_key)
|
|
478
412
|
vc_key = assoc_class.privacy_vc_key
|
|
479
413
|
old_vc = Thread.current[vc_key]
|
|
@@ -510,11 +444,8 @@ module Sequel
|
|
|
510
444
|
|
|
511
445
|
define_method(name) do
|
|
512
446
|
vc = instance_variable_get(:@viewer_context)
|
|
513
|
-
|
|
514
|
-
# Determine associated class (lazily, to handle forward references)
|
|
515
447
|
assoc_class ||= assoc_reflection.associated_class
|
|
516
448
|
|
|
517
|
-
# Load association with VC context set if available
|
|
518
449
|
objs = if vc && assoc_class.respond_to?(:privacy_vc_key)
|
|
519
450
|
vc_key = assoc_class.privacy_vc_key
|
|
520
451
|
old_vc = Thread.current[vc_key]
|
|
@@ -564,7 +495,6 @@ module Sequel
|
|
|
564
495
|
"Cannot #{method_name} with OmniscientVC"
|
|
565
496
|
end
|
|
566
497
|
|
|
567
|
-
# Check policy with 3-arity: (subject=self, actor, direct_object=obj)
|
|
568
498
|
allowed = Sequel::Privacy::Enforcer.enforce(policies, self, vc, obj)
|
|
569
499
|
|
|
570
500
|
unless allowed
|
|
@@ -596,7 +526,6 @@ module Sequel
|
|
|
596
526
|
"Cannot #{method_name} with OmniscientVC"
|
|
597
527
|
end
|
|
598
528
|
|
|
599
|
-
# Check policy with 3-arity: (subject=self, actor, direct_object=obj)
|
|
600
529
|
allowed = Sequel::Privacy::Enforcer.enforce(policies, self, vc, obj)
|
|
601
530
|
|
|
602
531
|
unless allowed
|
|
@@ -628,7 +557,6 @@ module Sequel
|
|
|
628
557
|
"Cannot #{method_name} with OmniscientVC"
|
|
629
558
|
end
|
|
630
559
|
|
|
631
|
-
# Check policy with 2-arity: (subject=self, actor) - no direct object for remove_all
|
|
632
560
|
allowed = Sequel::Privacy::Enforcer.enforce(policies, self, vc)
|
|
633
561
|
|
|
634
562
|
unless allowed
|
|
@@ -658,19 +586,12 @@ module Sequel
|
|
|
658
586
|
@viewer_context = T.let(vc, T.nilable(Sequel::Privacy::ViewerContext))
|
|
659
587
|
end
|
|
660
588
|
|
|
661
|
-
# Attach a viewer context to this model instance
|
|
662
589
|
sig { params(vc: Sequel::Privacy::ViewerContext).returns(T.self_type) }
|
|
663
590
|
def for_vc(vc)
|
|
664
591
|
@viewer_context = T.let(vc, T.nilable(Sequel::Privacy::ViewerContext))
|
|
665
592
|
self
|
|
666
593
|
end
|
|
667
594
|
|
|
668
|
-
# Check if the viewer is allowed to perform an action.
|
|
669
|
-
#
|
|
670
|
-
# @param vc [ViewerContext] The viewer context
|
|
671
|
-
# @param action [Symbol] The action to check (:view, :edit, :create, etc.)
|
|
672
|
-
# @param direct_object [Sequel::Model, nil] Optional additional context
|
|
673
|
-
# @return [Boolean]
|
|
674
595
|
sig do
|
|
675
596
|
params(
|
|
676
597
|
vc: Sequel::Privacy::ViewerContext,
|
|
@@ -688,7 +609,6 @@ module Sequel
|
|
|
688
609
|
Sequel::Privacy::Enforcer.enforce(policies, self, vc, direct_object)
|
|
689
610
|
end
|
|
690
611
|
|
|
691
|
-
# Override save to check privacy policies
|
|
692
612
|
sig { params(opts: T.untyped).returns(T.nilable(T.self_type)) }
|
|
693
613
|
def save(*opts)
|
|
694
614
|
vc = viewer_context
|
|
@@ -716,7 +636,6 @@ module Sequel
|
|
|
716
636
|
super
|
|
717
637
|
end
|
|
718
638
|
|
|
719
|
-
# Override update to check privacy policies
|
|
720
639
|
sig { params(hash: T::Hash[Symbol, T.untyped]).returns(T.self_type) }
|
|
721
640
|
def update(hash)
|
|
722
641
|
vc = viewer_context
|
|
@@ -739,15 +658,13 @@ module Sequel
|
|
|
739
658
|
|
|
740
659
|
private
|
|
741
660
|
|
|
742
|
-
#
|
|
743
|
-
#
|
|
744
|
-
# InstanceMethods also extends ClassMethods (via mixes_in_class_methods).
|
|
661
|
+
# Every class that includes InstanceMethods also extends ClassMethods
|
|
662
|
+
# via `mixes_in_class_methods`, so this should always work.
|
|
745
663
|
sig { returns(ClassMethods) }
|
|
746
664
|
def _privacy_class
|
|
747
665
|
T.cast(self.class, ClassMethods)
|
|
748
666
|
end
|
|
749
667
|
|
|
750
|
-
# Override delete to block OmniscientVC
|
|
751
668
|
sig { returns(T.self_type) }
|
|
752
669
|
def delete
|
|
753
670
|
if viewer_context.is_a?(Sequel::Privacy::OmniscientVC)
|
|
@@ -769,23 +686,17 @@ module Sequel
|
|
|
769
686
|
# datasets via the :eager_block injected in ClassMethods#associate.
|
|
770
687
|
EAGER_VC_KEY = :sequel_privacy_eager_vc
|
|
771
688
|
|
|
772
|
-
# Attach viewer context to dataset for privacy enforcement on materialization
|
|
773
689
|
sig { params(vc: Sequel::Privacy::ViewerContext).returns(Sequel::Dataset) }
|
|
774
690
|
def for_vc(vc)
|
|
775
691
|
clone(viewer_context: vc)
|
|
776
692
|
end
|
|
777
693
|
|
|
778
|
-
#
|
|
779
|
-
#
|
|
780
|
-
#
|
|
781
|
-
#
|
|
782
|
-
#
|
|
783
|
-
#
|
|
784
|
-
# need raw rows so their checks (e.g. membership) aren't
|
|
785
|
-
# short-circuited by recursive :view filtering.
|
|
786
|
-
# - EAGER_VC_KEY: Sequel's eager-load attachment block
|
|
787
|
-
# dereferences each record to bucket by FK and would crash
|
|
788
|
-
# on nils; the association reader filters at read time.
|
|
694
|
+
# Stores the ViewerContext in a Thread-local that Model.call
|
|
695
|
+
# can retreive. Materializes the model, and then checks the view
|
|
696
|
+
# policy. If the model is being materialized within the context of
|
|
697
|
+
# checking a policy this is bypassed, because policies often need to
|
|
698
|
+
# check data that a VC might not have permission to see. The check is also
|
|
699
|
+
# bypassed for eager loads, and checked on the association.
|
|
789
700
|
sig { returns(T.untyped) }
|
|
790
701
|
def row_proc
|
|
791
702
|
vc = opts[:viewer_context]
|
|
@@ -817,25 +728,18 @@ module Sequel
|
|
|
817
728
|
end
|
|
818
729
|
end
|
|
819
730
|
|
|
820
|
-
# Override all to filter out nil results from privacy checks
|
|
821
731
|
sig { returns(T::Array[T.attached_class]) }
|
|
822
732
|
def all
|
|
823
733
|
results = super
|
|
824
734
|
opts[:viewer_context] ? results.compact : results
|
|
825
735
|
end
|
|
826
736
|
|
|
827
|
-
# Sequel
|
|
828
|
-
#
|
|
829
|
-
#
|
|
830
|
-
#
|
|
831
|
-
#
|
|
832
|
-
#
|
|
833
|
-
# attachment block doesn't choke on nils; the accessor wrapper
|
|
834
|
-
# filters at read time.
|
|
835
|
-
#
|
|
836
|
-
# Parents filtered to nil by the :view policy must be dropped
|
|
837
|
-
# before eager_load runs — its attachment code dereferences each
|
|
838
|
-
# record, which nil would break.
|
|
737
|
+
# Sequel's Model#post_load triggers eager_load. We expose the VC
|
|
738
|
+
# via EAGER_VC_KEY around that call so the :eager_block injected
|
|
739
|
+
# in ClassMethods#associate can wrap each child dataset with
|
|
740
|
+
# for_vc. Parents already filtered to nil by row_proc must be
|
|
741
|
+
# compacted first — eager_load's attachment code can't handle
|
|
742
|
+
# nil records.
|
|
839
743
|
sig { params(all_records: T.untyped).returns(T.untyped) }
|
|
840
744
|
def post_load(all_records)
|
|
841
745
|
vc = opts[:viewer_context]
|
|
@@ -852,7 +756,6 @@ module Sequel
|
|
|
852
756
|
end
|
|
853
757
|
end
|
|
854
758
|
|
|
855
|
-
# Create a new model instance with the viewer context attached
|
|
856
759
|
sig { params(values: T::Hash[Symbol, T.untyped]).returns(T.attached_class) }
|
|
857
760
|
def new(values = {})
|
|
858
761
|
instance = T.unsafe(model).new(values)
|
|
@@ -862,7 +765,6 @@ module Sequel
|
|
|
862
765
|
instance
|
|
863
766
|
end
|
|
864
767
|
|
|
865
|
-
# Create and save a new model instance with the viewer context attached
|
|
866
768
|
sig { params(values: T::Hash[Symbol, T.untyped]).returns(T.attached_class) }
|
|
867
769
|
def create(values = {})
|
|
868
770
|
T.cast(new(values), Sequel::Model).save
|
data/lib/sequel/privacy/cache.rb
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
module Sequel
|
|
5
5
|
module Privacy
|
|
6
|
-
# In-memory cache for policy evaluation results.
|
|
7
|
-
#
|
|
6
|
+
# In-memory cache for policy evaluation results. Clear between
|
|
7
|
+
# requests (e.g. via Rack middleware).
|
|
8
8
|
class << self
|
|
9
9
|
extend T::Sig
|
|
10
10
|
|
|
@@ -13,15 +13,13 @@ module Sequel
|
|
|
13
13
|
@cache ||= T.let({}, T.nilable(T::Hash[Integer, Symbol]))
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
# Key: [policy, actor, viewer_context].hash
|
|
18
|
-
# Value: subject.hash that matched
|
|
16
|
+
# Tracks single-match optimization state.
|
|
17
|
+
# Key: [policy, actor, viewer_context].hash → Value: subject.hash
|
|
19
18
|
sig { returns(T::Hash[Integer, Integer]) }
|
|
20
19
|
def single_matches
|
|
21
20
|
@single_matches ||= T.let({}, T.nilable(T::Hash[Integer, Integer]))
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
# Clear all caches. Call this between requests.
|
|
25
23
|
sig { void }
|
|
26
24
|
def clear_cache!
|
|
27
25
|
@cache = {}
|
|
@@ -16,7 +16,6 @@ module Sequel
|
|
|
16
16
|
class << self
|
|
17
17
|
extend T::Sig
|
|
18
18
|
|
|
19
|
-
# Returns the centralized logger from Sequel::Privacy.logger
|
|
20
19
|
sig { returns(T.untyped) }
|
|
21
20
|
def logger
|
|
22
21
|
Sequel::Privacy.logger
|
|
@@ -28,13 +27,8 @@ module Sequel
|
|
|
28
27
|
Thread.current[EVAL_KEY] == true
|
|
29
28
|
end
|
|
30
29
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# @param policies [Array<Policy, Proc>] The policy chain to evaluate
|
|
34
|
-
# @param subject [Sequel::Model] The object being accessed
|
|
35
|
-
# @param viewer_context [ViewerContext] Who is accessing the object
|
|
36
|
-
# @param direct_object [Sequel::Model, nil] Optional additional context object
|
|
37
|
-
# @return [Boolean] true if access is allowed, false otherwise
|
|
30
|
+
# Evaluates a policy chain against (subject, viewer_context, direct_object)
|
|
31
|
+
# and returns whether access is allowed.
|
|
38
32
|
sig do
|
|
39
33
|
params(
|
|
40
34
|
policies: TPolicyArray,
|
|
@@ -48,7 +42,6 @@ module Sequel
|
|
|
48
42
|
Thread.current[EVAL_KEY] = true
|
|
49
43
|
|
|
50
44
|
begin
|
|
51
|
-
# All-powerful and omniscient contexts bypass all checks
|
|
52
45
|
if viewer_context.is_a?(AllPowerfulVC)
|
|
53
46
|
logger&.warn('BYPASS: All-powerful viewer context bypasses all privacy rules.')
|
|
54
47
|
return true
|
|
@@ -66,7 +59,7 @@ module Sequel
|
|
|
66
59
|
policies = [BuiltInPolicies::AlwaysDeny]
|
|
67
60
|
end
|
|
68
61
|
|
|
69
|
-
#
|
|
62
|
+
# Fail-secure: every chain ends with AlwaysDeny.
|
|
70
63
|
unless policies.last == BuiltInPolicies::AlwaysDeny
|
|
71
64
|
logger&.warn { 'Policy chain should end with AlwaysDeny. Appending it.' }
|
|
72
65
|
policies = policies.dup << BuiltInPolicies::AlwaysDeny
|
|
@@ -148,7 +141,6 @@ module Sequel
|
|
|
148
141
|
:pass
|
|
149
142
|
end
|
|
150
143
|
|
|
151
|
-
# Evaluate a single policy and return its result
|
|
152
144
|
sig do
|
|
153
145
|
params(
|
|
154
146
|
uncasted_policy: T.any(TPolicy, Proc),
|
|
@@ -164,7 +156,6 @@ module Sequel
|
|
|
164
156
|
|
|
165
157
|
policy = T.cast(uncasted_policy, TPolicy, checked: false)
|
|
166
158
|
|
|
167
|
-
# Single-match optimization
|
|
168
159
|
if policy.single_match?
|
|
169
160
|
match_key = [policy, actor, viewer_context].hash
|
|
170
161
|
if (matched = Sequel::Privacy.single_matches[match_key]) && matched != subject.hash
|
|
@@ -173,7 +164,6 @@ module Sequel
|
|
|
173
164
|
end
|
|
174
165
|
end
|
|
175
166
|
|
|
176
|
-
# Check cache
|
|
177
167
|
cache_key = compute_cache_key(policy, subject, actor, viewer_context, direct_object)
|
|
178
168
|
if !skipped_from_single_match && policy.cacheable? && Sequel::Privacy.cache.key?(cache_key)
|
|
179
169
|
from_cache = true
|
|
@@ -181,20 +171,15 @@ module Sequel
|
|
|
181
171
|
Kernel.raise InvalidPolicyOutcomeError unless result && valid_outcome?(result)
|
|
182
172
|
end
|
|
183
173
|
|
|
184
|
-
# Execute policy if not cached
|
|
185
174
|
result ||= execute_policy(policy, subject, actor, direct_object)
|
|
186
175
|
result ||= :pass
|
|
187
176
|
|
|
188
|
-
# Handle combinator results
|
|
189
177
|
result = evaluate_child_policies(result, subject, actor, viewer_context, direct_object) if result.is_a?(Array)
|
|
190
178
|
|
|
191
|
-
# Cache result
|
|
192
179
|
Sequel::Privacy.cache[cache_key] = result if policy.cacheable? && !from_cache
|
|
193
180
|
|
|
194
|
-
# Log result
|
|
195
181
|
log_result(policy, result, actor, subject, from_cache, skipped_from_single_match)
|
|
196
182
|
|
|
197
|
-
# Record single-match
|
|
198
183
|
if policy.single_match? && result == :allow
|
|
199
184
|
Sequel::Privacy.single_matches[[policy, actor, viewer_context].hash] = subject.hash
|
|
200
185
|
end
|
|
@@ -215,10 +200,9 @@ module Sequel
|
|
|
215
200
|
).returns(T.untyped)
|
|
216
201
|
end
|
|
217
202
|
def self.execute_policy(policy, subject, actor, direct_object)
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
# only the subject).
|
|
203
|
+
# Arity ≥ 1 policies expect an actor as the first arg; an
|
|
204
|
+
# anonymous viewer auto-denies unless the policy opts in with
|
|
205
|
+
# `allow_anonymous: true` (for subject-only state gates).
|
|
222
206
|
return :deny if !actor && policy.arity >= 1 && !policy.allow_anonymous?
|
|
223
207
|
|
|
224
208
|
case policy.arity
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
module Sequel
|
|
5
5
|
module Privacy
|
|
6
|
-
# Interface
|
|
7
|
-
# to be used with the privacy system.
|
|
6
|
+
# Interface for actors used in viewer contexts (typically User/Member).
|
|
8
7
|
module IActor
|
|
9
8
|
extend T::Sig
|
|
10
9
|
extend T::Helpers
|
|
@@ -27,8 +27,6 @@ module Sequel
|
|
|
27
27
|
|
|
28
28
|
VALID_CACHE_BY = T.let(%i[actor subject direct_object].freeze, T::Array[Symbol])
|
|
29
29
|
|
|
30
|
-
# Factory method for creating policies. Accepts procs of any arity
|
|
31
|
-
# (0–3 args) returning :allow, :deny, :pass, or an Array of policies.
|
|
32
30
|
sig do
|
|
33
31
|
params(
|
|
34
32
|
policy_name: Symbol,
|
|
@@ -52,24 +50,23 @@ module Sequel
|
|
|
52
50
|
)
|
|
53
51
|
end
|
|
54
52
|
|
|
55
|
-
# Configure the policy after creation
|
|
53
|
+
# Configure the policy after creation, normally done with the shorthand `policy` call.
|
|
56
54
|
#
|
|
57
55
|
# @param policy_name [Symbol, nil] Human-readable name for logging
|
|
58
56
|
# @param comment [String, nil] Description of what this policy does
|
|
59
57
|
# @param cacheable [Boolean] Whether results can be cached (default: true)
|
|
60
58
|
# @param single_match [Boolean] Whether only one subject/actor pair can match (default: false)
|
|
61
59
|
# @param cache_by [Symbol, Array<Symbol>, nil] Override the cache-key
|
|
62
|
-
# dimensions. By default the key is derived from the policy's arity
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
# "is-admin" check that takes `(actor, subject)` but only examines
|
|
60
|
+
# dimensions. By default the key is derived from the policy's arity,
|
|
61
|
+
# but you might want to pass a subset of `:actor, :subject, :direct_object`
|
|
62
|
+
# to cache by only those; useful when the policy ignores inputs (e.g. an
|
|
63
|
+
# "is-admin" check that takes `(actor, subject)` but only looks at
|
|
67
64
|
# actor should use `cache_by: :actor` to share a single entry across
|
|
68
65
|
# subjects).
|
|
69
66
|
# @param allow_anonymous [Boolean] If true, skip the auto-deny that
|
|
70
67
|
# normally fires when a policy of arity >= 1 is evaluated for an
|
|
71
|
-
# anonymous viewer (nil actor).
|
|
72
|
-
#
|
|
68
|
+
# anonymous viewer (nil actor). This is a bit inelegant; it'd be great
|
|
69
|
+
# if we could tell that an argument isn't used at all.
|
|
73
70
|
def setup(policy_name: nil, comment: nil, cacheable: true, single_match: false, cache_by: nil,
|
|
74
71
|
allow_anonymous: false)
|
|
75
72
|
raise 'Privacy Policy is frozen' if @frozen
|
|
@@ -89,8 +86,9 @@ module Sequel
|
|
|
89
86
|
@cacheable || false
|
|
90
87
|
end
|
|
91
88
|
|
|
92
|
-
#
|
|
93
|
-
#
|
|
89
|
+
# When set, once the policy allows for a given actor it short-circuits
|
|
90
|
+
# to :pass on every other subject — useful when only one subject can
|
|
91
|
+
# ever match (e.g. AllowSelf).
|
|
94
92
|
sig { returns(T::Boolean) }
|
|
95
93
|
def single_match?
|
|
96
94
|
@single_match || false
|
|
@@ -125,7 +123,6 @@ module Sequel
|
|
|
125
123
|
end
|
|
126
124
|
end
|
|
127
125
|
|
|
128
|
-
# Type aliases for use throughout the gem
|
|
129
126
|
module Sequel
|
|
130
127
|
module Privacy
|
|
131
128
|
TPolicy = T.type_alias { Sequel::Privacy::Policy }
|
|
@@ -20,21 +20,13 @@ module Sequel
|
|
|
20
20
|
extend T::Sig
|
|
21
21
|
extend T::Helpers
|
|
22
22
|
|
|
23
|
-
#
|
|
24
|
-
#
|
|
23
|
+
# `extend`ed onto another Module, so `self` at runtime is always a
|
|
24
|
+
# Module that responds to `const_set`.
|
|
25
25
|
requires_ancestor { Module }
|
|
26
26
|
|
|
27
|
-
# Define a
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# @param lam [Proc] The policy lambda (0-3 args: actor, subject, direct_object)
|
|
31
|
-
# @param comment [String, nil] Human-readable description
|
|
32
|
-
# @param cacheable [Boolean] Whether results can be cached (default: true)
|
|
33
|
-
# @param single_match [Boolean] Whether only one subject/actor can match (default: false)
|
|
34
|
-
# @param cache_by [Symbol, Array<Symbol>, nil] Override cache-key
|
|
35
|
-
# dimensions. See Sequel::Privacy::Policy#setup for details.
|
|
36
|
-
# @param allow_anonymous [Boolean] Skip auto-deny for nil actor.
|
|
37
|
-
# See Sequel::Privacy::Policy#setup for details.
|
|
27
|
+
# Define a policy constant on the extending module. See
|
|
28
|
+
# `Sequel::Privacy::Policy#setup` for the meaning of `cacheable`,
|
|
29
|
+
# `single_match`, `cache_by`, and `allow_anonymous`.
|
|
38
30
|
sig do
|
|
39
31
|
params(
|
|
40
32
|
name: Symbol,
|
|
@@ -3,43 +3,40 @@
|
|
|
3
3
|
|
|
4
4
|
module Sequel
|
|
5
5
|
module Privacy
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# Represents who is viewing/accessing data. All privacy checks require
|
|
7
|
+
# a ViewerContext.
|
|
8
8
|
class ViewerContext
|
|
9
9
|
extend T::Sig
|
|
10
10
|
extend T::Helpers
|
|
11
11
|
abstract!
|
|
12
12
|
|
|
13
|
-
# Create a standard viewer context for an actor
|
|
14
13
|
sig { params(actor: IActor).returns(ActorVC) }
|
|
15
14
|
def self.for_actor(actor)
|
|
16
15
|
ActorVC.new(actor)
|
|
17
16
|
end
|
|
18
17
|
|
|
19
|
-
# Create an API-specific viewer context
|
|
20
18
|
sig { params(actor: IActor).returns(APIVC) }
|
|
21
19
|
def self.for_api_actor(actor)
|
|
22
20
|
APIVC.new(actor)
|
|
23
21
|
end
|
|
24
22
|
|
|
25
|
-
#
|
|
26
|
-
# Use sparingly
|
|
23
|
+
# Bypasses all privacy checks; requires a reason for audit logging.
|
|
24
|
+
# Use sparingly.
|
|
27
25
|
sig { params(reason: Symbol).returns(AllPowerfulVC) }
|
|
28
26
|
def self.all_powerful(reason)
|
|
29
27
|
Sequel::Privacy.logger&.info("Creating all-powerful viewer context: #{reason}")
|
|
30
28
|
AllPowerfulVC.new(reason)
|
|
31
29
|
end
|
|
32
30
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
31
|
+
# Reads any record but cannot mutate. For system operations like
|
|
32
|
+
# authentication lookups.
|
|
35
33
|
sig { params(reason: Symbol).returns(OmniscientVC) }
|
|
36
34
|
def self.omniscient(reason)
|
|
37
35
|
Sequel::Privacy.logger&.debug("Creating omniscient viewer context: #{reason}")
|
|
38
36
|
OmniscientVC.new(reason)
|
|
39
37
|
end
|
|
40
38
|
|
|
41
|
-
#
|
|
42
|
-
# Subject to normal policy evaluation with no actor.
|
|
39
|
+
# No actor; subject to normal policy evaluation. For logged-out users.
|
|
43
40
|
sig { returns(AnonymousVC) }
|
|
44
41
|
def self.anonymous
|
|
45
42
|
AnonymousVC.new
|
|
@@ -94,8 +91,6 @@ module Sequel
|
|
|
94
91
|
attr_reader :reason
|
|
95
92
|
end
|
|
96
93
|
|
|
97
|
-
# Anonymous viewer context for logged-out users.
|
|
98
|
-
# Has no actor - policies with arity >= 1 will auto-deny.
|
|
99
94
|
class AnonymousVC < ViewerContext
|
|
100
95
|
extend T::Sig
|
|
101
96
|
|
|
@@ -105,7 +100,6 @@ module Sequel
|
|
|
105
100
|
end
|
|
106
101
|
end
|
|
107
102
|
|
|
108
|
-
# Type alias for viewer contexts
|
|
109
103
|
TViewerContext = T.type_alias { ViewerContext }
|
|
110
104
|
end
|
|
111
105
|
end
|
data/lib/sequel-privacy.rb
CHANGED
|
@@ -9,15 +9,13 @@ module Sequel
|
|
|
9
9
|
class << self
|
|
10
10
|
extend T::Sig
|
|
11
11
|
|
|
12
|
-
#
|
|
13
|
-
# Set this to your application's logger (e.g., SemanticLogger).
|
|
12
|
+
# Set this to your application's logger (e.g. SemanticLogger).
|
|
14
13
|
sig { returns(T.untyped) }
|
|
15
14
|
attr_accessor :logger
|
|
16
15
|
end
|
|
17
16
|
end
|
|
18
17
|
end
|
|
19
18
|
|
|
20
|
-
# Core privacy infrastructure
|
|
21
19
|
require_relative 'sequel/privacy/version'
|
|
22
20
|
require_relative 'sequel/privacy/errors'
|
|
23
21
|
require_relative 'sequel/privacy/i_actor'
|
|
@@ -29,5 +27,5 @@ require_relative 'sequel/privacy/enforcer'
|
|
|
29
27
|
require_relative 'sequel/privacy/built_in_policies'
|
|
30
28
|
require_relative 'sequel/privacy/policy_dsl'
|
|
31
29
|
|
|
32
|
-
# The plugin
|
|
33
|
-
#
|
|
30
|
+
# The plugin itself lives in lib/sequel/plugins/privacy.rb and is
|
|
31
|
+
# auto-loaded by Sequel on `plugin :privacy`.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sequel-privacy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Austin Bales
|
|
@@ -130,7 +130,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
130
130
|
requirements:
|
|
131
131
|
- - ">="
|
|
132
132
|
- !ruby/object:Gem::Version
|
|
133
|
-
version: 3.
|
|
133
|
+
version: 3.2.0
|
|
134
134
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
135
|
requirements:
|
|
136
136
|
- - ">="
|