sequel-privacy 0.5.3 → 0.5.5
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 +36 -17
- data/lib/sequel/privacy/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7dd8660d0cc93e21c96080ba66f777e9b39f00ec7a5ca4f8fa211739a97dbe62
|
|
4
|
+
data.tar.gz: 85952e038e33a535adf8adaed566c08310e003f2540a406457029d678e889009
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9276223bd19d31daaabbaa34076e15c6dce4ce2439daadc30bee5e021fc7a88571323ce213ab2840afbe533b53d73129625817177a44b7a178f1fc93b6f21c37
|
|
7
|
+
data.tar.gz: d6c2e72d8eea11c83b42be27a64dd5076e0e320a7d15efe7a6092ca543714203cf643ad14eee190b986eda570a331fca2b8d519126494ff35d5caef421f2fb9a
|
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
|
|
@@ -148,17 +148,24 @@ module Sequel
|
|
|
148
148
|
:@allow_unsafe_access => nil
|
|
149
149
|
)
|
|
150
150
|
|
|
151
|
-
# Allows the model to be accessed without a ViewerContext,
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
# Allows the model to be accessed without a ViewerContext, useful when
|
|
152
|
+
# you're migrating an existing codebase or adopting gradually.
|
|
153
|
+
# You can prevent this from applying to certain fields or associations by
|
|
154
|
+
# passing `except:`.
|
|
155
|
+
sig { params(except: T::Array[Symbol]).void }
|
|
156
|
+
def allow_unsafe_access!(except: [])
|
|
155
157
|
@allow_unsafe_access = T.let(true, T.nilable(T::Boolean))
|
|
158
|
+
@unsafe_access_except = T.let(except.map(&:to_sym), T.nilable(T::Array[Symbol]))
|
|
156
159
|
Sequel::Privacy.logger&.warn("#{self} allows unsafe access - migrate to use for_vc()")
|
|
157
160
|
end
|
|
158
161
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
+
# Checks if the model or a field/association allows unsafe access.
|
|
163
|
+
sig { params(name: T.nilable(Symbol)).returns(T::Boolean) }
|
|
164
|
+
def allow_unsafe_access?(name = nil)
|
|
165
|
+
return false unless @allow_unsafe_access == true
|
|
166
|
+
return true if name.nil?
|
|
167
|
+
|
|
168
|
+
!(@unsafe_access_except || []).include?(name)
|
|
162
169
|
end
|
|
163
170
|
|
|
164
171
|
# Per-class thread-local key carrying the current VC during row
|
|
@@ -246,7 +253,7 @@ module Sequel
|
|
|
246
253
|
vc = instance_variable_get(:@viewer_context)
|
|
247
254
|
|
|
248
255
|
unless vc
|
|
249
|
-
return original_method.bind(self).() if T.unsafe(self.class).allow_unsafe_access?
|
|
256
|
+
return original_method.bind(self).() if T.unsafe(self.class).allow_unsafe_access?(field)
|
|
250
257
|
|
|
251
258
|
Kernel.raise Sequel::Privacy::MissingViewerContext,
|
|
252
259
|
"#{self.class}##{field} requires a ViewerContext"
|
|
@@ -406,6 +413,12 @@ module Sequel
|
|
|
406
413
|
|
|
407
414
|
define_method(name) do
|
|
408
415
|
vc = instance_variable_get(:@viewer_context)
|
|
416
|
+
|
|
417
|
+
if vc.nil? && !T.unsafe(self.class).allow_unsafe_access?(name)
|
|
418
|
+
Kernel.raise Sequel::Privacy::MissingViewerContext,
|
|
419
|
+
"#{self.class}##{name} requires a ViewerContext"
|
|
420
|
+
end
|
|
421
|
+
|
|
409
422
|
assoc_class ||= assoc_reflection.associated_class
|
|
410
423
|
|
|
411
424
|
obj = if vc && assoc_class.respond_to?(:privacy_vc_key)
|
|
@@ -444,6 +457,12 @@ module Sequel
|
|
|
444
457
|
|
|
445
458
|
define_method(name) do
|
|
446
459
|
vc = instance_variable_get(:@viewer_context)
|
|
460
|
+
|
|
461
|
+
if vc.nil? && !T.unsafe(self.class).allow_unsafe_access?(name)
|
|
462
|
+
Kernel.raise Sequel::Privacy::MissingViewerContext,
|
|
463
|
+
"#{self.class}##{name} requires a ViewerContext"
|
|
464
|
+
end
|
|
465
|
+
|
|
447
466
|
assoc_class ||= assoc_reflection.associated_class
|
|
448
467
|
|
|
449
468
|
objs = if vc && assoc_class.respond_to?(:privacy_vc_key)
|
|
@@ -475,8 +494,8 @@ module Sequel
|
|
|
475
494
|
end
|
|
476
495
|
end
|
|
477
496
|
|
|
478
|
-
sig { params(
|
|
479
|
-
def _wrap_association_add(
|
|
497
|
+
sig { params(assoc_name: Symbol, singular_name: Symbol, policies: T::Array[T.untyped]).void }
|
|
498
|
+
def _wrap_association_add(assoc_name, singular_name, policies)
|
|
480
499
|
method_name = :"add_#{singular_name}"
|
|
481
500
|
original = instance_method(method_name)
|
|
482
501
|
|
|
@@ -484,7 +503,7 @@ module Sequel
|
|
|
484
503
|
vc = instance_variable_get(:@viewer_context)
|
|
485
504
|
|
|
486
505
|
unless vc
|
|
487
|
-
return original.bind(self).(obj) if T.unsafe(self.class).allow_unsafe_access?
|
|
506
|
+
return original.bind(self).(obj) if T.unsafe(self.class).allow_unsafe_access?(assoc_name)
|
|
488
507
|
|
|
489
508
|
Kernel.raise Sequel::Privacy::MissingViewerContext,
|
|
490
509
|
"Cannot #{method_name} without a viewer context"
|
|
@@ -506,8 +525,8 @@ module Sequel
|
|
|
506
525
|
end
|
|
507
526
|
end
|
|
508
527
|
|
|
509
|
-
sig { params(
|
|
510
|
-
def _wrap_association_remove(
|
|
528
|
+
sig { params(assoc_name: Symbol, singular_name: Symbol, policies: T::Array[T.untyped]).void }
|
|
529
|
+
def _wrap_association_remove(assoc_name, singular_name, policies)
|
|
511
530
|
method_name = :"remove_#{singular_name}"
|
|
512
531
|
original = instance_method(method_name)
|
|
513
532
|
|
|
@@ -515,7 +534,7 @@ module Sequel
|
|
|
515
534
|
vc = instance_variable_get(:@viewer_context)
|
|
516
535
|
|
|
517
536
|
unless vc
|
|
518
|
-
return original.bind(self).(obj) if T.unsafe(self.class).allow_unsafe_access?
|
|
537
|
+
return original.bind(self).(obj) if T.unsafe(self.class).allow_unsafe_access?(assoc_name)
|
|
519
538
|
|
|
520
539
|
Kernel.raise Sequel::Privacy::MissingViewerContext,
|
|
521
540
|
"Cannot #{method_name} without a viewer context"
|
|
@@ -537,8 +556,8 @@ module Sequel
|
|
|
537
556
|
end
|
|
538
557
|
end
|
|
539
558
|
|
|
540
|
-
sig { params(
|
|
541
|
-
def _wrap_association_remove_all(
|
|
559
|
+
sig { params(assoc_name: Symbol, plural_name: Symbol, policies: T::Array[T.untyped]).void }
|
|
560
|
+
def _wrap_association_remove_all(assoc_name, plural_name, policies)
|
|
542
561
|
method_name = :"remove_all_#{plural_name}"
|
|
543
562
|
original = instance_method(method_name)
|
|
544
563
|
|
|
@@ -546,7 +565,7 @@ module Sequel
|
|
|
546
565
|
vc = instance_variable_get(:@viewer_context)
|
|
547
566
|
|
|
548
567
|
unless vc
|
|
549
|
-
return original.bind(self).() if T.unsafe(self.class).allow_unsafe_access?
|
|
568
|
+
return original.bind(self).() if T.unsafe(self.class).allow_unsafe_access?(assoc_name)
|
|
550
569
|
|
|
551
570
|
Kernel.raise Sequel::Privacy::MissingViewerContext,
|
|
552
571
|
"Cannot #{method_name} without a viewer context"
|