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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f2538fcf5aee16c7b624701e81d936df211b20fa007cf9fa1fffc893bda0105
4
- data.tar.gz: e1df8a2b58c2e6851447e2874c837a865641be73ed0adc25c255bcb9076d4087
3
+ metadata.gz: 7dd8660d0cc93e21c96080ba66f777e9b39f00ec7a5ca4f8fa211739a97dbe62
4
+ data.tar.gz: 85952e038e33a535adf8adaed566c08310e003f2540a406457029d678e889009
5
5
  SHA512:
6
- metadata.gz: 68996d09fde85691254e51d6edb40f4a32007c07a232b768a705d29b7b4ea37ebe95a2ac092c18f201a7516b53108d4a8ff8356a0f860c288a18903648b5c1b6
7
- data.tar.gz: d1b9364e64145dbe2993c18bb8db458826b2480e9e827f589ed6cecefdbb1e2c82e0c2fdd72a3542946fb4bea70e2d58c37f81889067b3b5e7c5ce7912032477
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` struct, 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.
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)`: ownership, membership
119
- - 3 args — `(actor, subject, direct_object)`: "can actor do X to subject with 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 other, it's not worth a potentially expensive check on other combinations once you've found the winner.
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 (symbol) for audit logging.
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&.vc || Sequel::Privacy::ViewerContext.anonymous()
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.cache.clear
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
- # useful when you're migrating an existing codebase or adopting gradually.
153
- sig { void }
154
- def allow_unsafe_access!
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
- sig { returns(T::Boolean) }
160
- def allow_unsafe_access?
161
- @allow_unsafe_access == true
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(_assoc_name: Symbol, singular_name: Symbol, policies: T::Array[T.untyped]).void }
479
- def _wrap_association_add(_assoc_name, singular_name, policies)
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(_assoc_name: Symbol, singular_name: Symbol, policies: T::Array[T.untyped]).void }
510
- def _wrap_association_remove(_assoc_name, singular_name, policies)
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(_assoc_name: Symbol, plural_name: Symbol, policies: T::Array[T.untyped]).void }
541
- def _wrap_association_remove_all(_assoc_name, plural_name, policies)
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"
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Sequel
5
5
  module Privacy
6
- VERSION = '0.5.3'
6
+ VERSION = '0.5.5'
7
7
  end
8
8
  end
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.3
4
+ version: 0.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Austin Bales