sequel-privacy 0.5.4 → 0.5.6

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: 54198780e9744c541e3915d3ac5722ccb16b119a77729b5013b854e698604511
4
- data.tar.gz: 5219bec34c9542ce88f928bc39c1998586e6ed2c15637296dead03d958e12db1
3
+ metadata.gz: 2b47c85175a70082489fe5307e91094ea6a60dfde2d7f28ee814206a3029a892
4
+ data.tar.gz: a14f83b40de9d66df9390b5d2e797356bebd98f7f4c03947123b910fa0290f11
5
5
  SHA512:
6
- metadata.gz: 8673ccf1e4e6cb592299c9a930ee7ff779e0343e66753a245f71f4aa26d21906c486dc1435d55af53c1c8b09c8166f24fec2e8036ce3f962598df4d8a29cadf9
7
- data.tar.gz: 34bc11e989421e4cd7d5207fc531d7bcf17cf681c8b10c3104918c12d5d2a9eb8cdf68274dd64397c8b49ba51b6c59b3de4fd4ef73e8acbf06b1fdd671039cce
6
+ metadata.gz: 941314443e93f294bb0de8017ca4abf7181b9298de65ab4b020f2811c48b6f486dc65514c442b764a03f2c6138e51b5ac52a3bb1cf66b0fa976be7bb081ad075
7
+ data.tar.gz: 372ca5512a9a8a215ab354ab4fa878334aef8edfec000524b40b36d8a9cac690af2e44ed0a9e0e7e9a0d539a843f9b34a9ad422b283c49a0a45aa2e0e202dc3b
data/README.md CHANGED
@@ -180,6 +180,33 @@ policy :MyPolicy, ->() { ... },
180
180
 
181
181
  **`allow_anonymous: true`**: Skip the auto-deny for nil actor. Use for state-gate policies that examine only the subject (e.g. "post is published").
182
182
 
183
+ ### Policy Factories
184
+
185
+ Use `policy_factory` when a policy needs definition-time arguments, while still receiving the normal runtime policy arguments (`actor`, `subject`, `direct_object`) during enforcement.
186
+
187
+ ```ruby
188
+ module P
189
+ extend Sequel::Privacy::PolicyDSL
190
+
191
+ policy_factory :AllowIfActorMeetsFieldVisibility, ->(visibility_field) {
192
+ ->(actor, subject) {
193
+ allow if actor.meets_visibility?(subject.public_send(visibility_field))
194
+ }
195
+ }
196
+ end
197
+
198
+ class Member < Sequel::Model
199
+ plugin :privacy
200
+
201
+ privacy do
202
+ can :view, P::AllowMembers
203
+ field :phone, P::AllowIfActorMeetsFieldVisibility(:phone_visibility)
204
+ end
205
+ end
206
+ ```
207
+
208
+ Policy factories accept the same options as `policy`. The factory must return a Proc, and each call returns a concrete `Policy` instance with its own cache identity.
209
+
183
210
  ### Policy Combinators
184
211
 
185
212
  Use `all()` to require multiple conditions:
@@ -126,6 +126,8 @@ module Sequel
126
126
  case p
127
127
  when Sequel::Privacy::Policy, Proc
128
128
  p
129
+ when Sequel::Privacy::PolicyFactory
130
+ Kernel.raise ArgumentError, "Policy factory #{p.factory_name} must be called with arguments"
129
131
  else
130
132
  Kernel.raise ArgumentError, "Invalid policy: #{p.inspect}"
131
133
  end
@@ -148,17 +150,24 @@ module Sequel
148
150
  :@allow_unsafe_access => nil
149
151
  )
150
152
 
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!
153
+ # Allows the model to be accessed without a ViewerContext, useful when
154
+ # you're migrating an existing codebase or adopting gradually.
155
+ # You can prevent this from applying to certain fields or associations by
156
+ # passing `except:`.
157
+ sig { params(except: T::Array[Symbol]).void }
158
+ def allow_unsafe_access!(except: [])
155
159
  @allow_unsafe_access = T.let(true, T.nilable(T::Boolean))
160
+ @unsafe_access_except = T.let(except.map(&:to_sym), T.nilable(T::Array[Symbol]))
156
161
  Sequel::Privacy.logger&.warn("#{self} allows unsafe access - migrate to use for_vc()")
157
162
  end
158
163
 
159
- sig { returns(T::Boolean) }
160
- def allow_unsafe_access?
161
- @allow_unsafe_access == true
164
+ # Checks if the model or a field/association allows unsafe access.
165
+ sig { params(name: T.nilable(Symbol)).returns(T::Boolean) }
166
+ def allow_unsafe_access?(name = nil)
167
+ return false unless @allow_unsafe_access == true
168
+ return true if name.nil?
169
+
170
+ !(@unsafe_access_except || []).include?(name)
162
171
  end
163
172
 
164
173
  # Per-class thread-local key carrying the current VC during row
@@ -246,7 +255,7 @@ module Sequel
246
255
  vc = instance_variable_get(:@viewer_context)
247
256
 
248
257
  unless vc
249
- return original_method.bind(self).() if T.unsafe(self.class).allow_unsafe_access?
258
+ return original_method.bind(self).() if T.unsafe(self.class).allow_unsafe_access?(field)
250
259
 
251
260
  Kernel.raise Sequel::Privacy::MissingViewerContext,
252
261
  "#{self.class}##{field} requires a ViewerContext"
@@ -406,6 +415,12 @@ module Sequel
406
415
 
407
416
  define_method(name) do
408
417
  vc = instance_variable_get(:@viewer_context)
418
+
419
+ if vc.nil? && !T.unsafe(self.class).allow_unsafe_access?(name)
420
+ Kernel.raise Sequel::Privacy::MissingViewerContext,
421
+ "#{self.class}##{name} requires a ViewerContext"
422
+ end
423
+
409
424
  assoc_class ||= assoc_reflection.associated_class
410
425
 
411
426
  obj = if vc && assoc_class.respond_to?(:privacy_vc_key)
@@ -444,6 +459,12 @@ module Sequel
444
459
 
445
460
  define_method(name) do
446
461
  vc = instance_variable_get(:@viewer_context)
462
+
463
+ if vc.nil? && !T.unsafe(self.class).allow_unsafe_access?(name)
464
+ Kernel.raise Sequel::Privacy::MissingViewerContext,
465
+ "#{self.class}##{name} requires a ViewerContext"
466
+ end
467
+
447
468
  assoc_class ||= assoc_reflection.associated_class
448
469
 
449
470
  objs = if vc && assoc_class.respond_to?(:privacy_vc_key)
@@ -475,8 +496,8 @@ module Sequel
475
496
  end
476
497
  end
477
498
 
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)
499
+ sig { params(assoc_name: Symbol, singular_name: Symbol, policies: T::Array[T.untyped]).void }
500
+ def _wrap_association_add(assoc_name, singular_name, policies)
480
501
  method_name = :"add_#{singular_name}"
481
502
  original = instance_method(method_name)
482
503
 
@@ -484,7 +505,7 @@ module Sequel
484
505
  vc = instance_variable_get(:@viewer_context)
485
506
 
486
507
  unless vc
487
- return original.bind(self).(obj) if T.unsafe(self.class).allow_unsafe_access?
508
+ return original.bind(self).(obj) if T.unsafe(self.class).allow_unsafe_access?(assoc_name)
488
509
 
489
510
  Kernel.raise Sequel::Privacy::MissingViewerContext,
490
511
  "Cannot #{method_name} without a viewer context"
@@ -506,8 +527,8 @@ module Sequel
506
527
  end
507
528
  end
508
529
 
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)
530
+ sig { params(assoc_name: Symbol, singular_name: Symbol, policies: T::Array[T.untyped]).void }
531
+ def _wrap_association_remove(assoc_name, singular_name, policies)
511
532
  method_name = :"remove_#{singular_name}"
512
533
  original = instance_method(method_name)
513
534
 
@@ -515,7 +536,7 @@ module Sequel
515
536
  vc = instance_variable_get(:@viewer_context)
516
537
 
517
538
  unless vc
518
- return original.bind(self).(obj) if T.unsafe(self.class).allow_unsafe_access?
539
+ return original.bind(self).(obj) if T.unsafe(self.class).allow_unsafe_access?(assoc_name)
519
540
 
520
541
  Kernel.raise Sequel::Privacy::MissingViewerContext,
521
542
  "Cannot #{method_name} without a viewer context"
@@ -537,8 +558,8 @@ module Sequel
537
558
  end
538
559
  end
539
560
 
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)
561
+ sig { params(assoc_name: Symbol, plural_name: Symbol, policies: T::Array[T.untyped]).void }
562
+ def _wrap_association_remove_all(assoc_name, plural_name, policies)
542
563
  method_name = :"remove_all_#{plural_name}"
543
564
  original = instance_method(method_name)
544
565
 
@@ -546,7 +567,7 @@ module Sequel
546
567
  vc = instance_variable_get(:@viewer_context)
547
568
 
548
569
  unless vc
549
- return original.bind(self).() if T.unsafe(self.class).allow_unsafe_access?
570
+ return original.bind(self).() if T.unsafe(self.class).allow_unsafe_access?(assoc_name)
550
571
 
551
572
  Kernel.raise Sequel::Privacy::MissingViewerContext,
552
573
  "Cannot #{method_name} without a viewer context"
@@ -50,6 +50,35 @@ module Sequel
50
50
  )
51
51
  const_set(name, p)
52
52
  end
53
+
54
+ sig do
55
+ params(
56
+ name: Symbol,
57
+ factory: Proc,
58
+ comment: T.nilable(String),
59
+ cacheable: T::Boolean,
60
+ single_match: T::Boolean,
61
+ cache_by: T.nilable(T.any(Symbol, T::Array[Symbol])),
62
+ allow_anonymous: T::Boolean
63
+ ).void
64
+ end
65
+ def policy_factory(name, factory, comment = nil, cacheable: true, single_match: false, cache_by: nil,
66
+ allow_anonymous: false)
67
+ policy_factory = PolicyFactory.new(
68
+ name,
69
+ factory,
70
+ comment: comment,
71
+ cacheable: cacheable,
72
+ single_match: single_match,
73
+ cache_by: cache_by,
74
+ allow_anonymous: allow_anonymous
75
+ )
76
+
77
+ const_set(name, policy_factory)
78
+ define_singleton_method(name) do |*args|
79
+ policy_factory.call(*args)
80
+ end
81
+ end
53
82
  end
54
83
  end
55
84
  end
@@ -0,0 +1,68 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Sequel
5
+ module Privacy
6
+ # A PolicyFactory captures definition-time arguments and returns concrete
7
+ # Policy instances that can be registered in a privacy policy chain.
8
+ class PolicyFactory
9
+ extend T::Sig
10
+
11
+ sig { returns(String) }
12
+ attr_reader :factory_name
13
+
14
+ sig do
15
+ params(
16
+ factory_name: Symbol,
17
+ factory: Proc,
18
+ comment: T.nilable(String),
19
+ cacheable: T::Boolean,
20
+ single_match: T::Boolean,
21
+ cache_by: T.nilable(T.any(Symbol, T::Array[Symbol])),
22
+ allow_anonymous: T::Boolean
23
+ ).void
24
+ end
25
+ def initialize(factory_name, factory, comment: nil, cacheable: true, single_match: false, cache_by: nil,
26
+ allow_anonymous: false)
27
+ @factory_name = T.let(factory_name.to_s, String)
28
+ @factory = T.let(factory, Proc)
29
+ @comment = T.let(comment, T.nilable(String))
30
+ @cacheable = T.let(cacheable, T::Boolean)
31
+ @single_match = T.let(single_match, T::Boolean)
32
+ @cache_by = T.let(cache_by, T.nilable(T.any(Symbol, T::Array[Symbol])))
33
+ @allow_anonymous = T.let(allow_anonymous, T::Boolean)
34
+ end
35
+
36
+ sig { params(args: T.untyped).returns(Policy) }
37
+ def call(*args)
38
+ lam = T.unsafe(@factory).call(*args)
39
+ unless lam.is_a?(Proc)
40
+ Kernel.raise ArgumentError,
41
+ "Policy factory #{@factory_name} must return a Proc, got #{lam.inspect}"
42
+ end
43
+
44
+ T.cast(
45
+ Policy.create(
46
+ policy_name_for(args),
47
+ lam,
48
+ @comment,
49
+ cacheable: @cacheable,
50
+ single_match: @single_match,
51
+ cache_by: @cache_by,
52
+ allow_anonymous: @allow_anonymous
53
+ ),
54
+ Policy
55
+ )
56
+ end
57
+
58
+ private
59
+
60
+ sig { params(args: T::Array[T.untyped]).returns(Symbol) }
61
+ def policy_name_for(args)
62
+ return @factory_name.to_sym if args.empty?
63
+
64
+ :"#{@factory_name}(#{args.map(&:inspect).join(', ')})"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Sequel
5
5
  module Privacy
6
- VERSION = '0.5.4'
6
+ VERSION = '0.5.6'
7
7
  end
8
8
  end
@@ -20,6 +20,7 @@ require_relative 'sequel/privacy/version'
20
20
  require_relative 'sequel/privacy/errors'
21
21
  require_relative 'sequel/privacy/i_actor'
22
22
  require_relative 'sequel/privacy/policy'
23
+ require_relative 'sequel/privacy/policy_factory'
23
24
  require_relative 'sequel/privacy/cache'
24
25
  require_relative 'sequel/privacy/actions'
25
26
  require_relative 'sequel/privacy/viewer_context'
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
4
+ version: 0.5.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Austin Bales
@@ -114,6 +114,7 @@ files:
114
114
  - lib/sequel/privacy/i_actor.rb
115
115
  - lib/sequel/privacy/policy.rb
116
116
  - lib/sequel/privacy/policy_dsl.rb
117
+ - lib/sequel/privacy/policy_factory.rb
117
118
  - lib/sequel/privacy/version.rb
118
119
  - lib/sequel/privacy/viewer_context.rb
119
120
  homepage: https://github.com/arbales/sequel-privacy