sequel-privacy 0.1.0 → 0.2.0

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: 644015302ea6057f9b102b24803bafb4c67059e51a6a66c02d825216404ae05f
4
- data.tar.gz: 8504cb1ca168b0f34b2fa7705a782ca74567aa1839d4f13816f69aaa5e27546e
3
+ metadata.gz: 62f27f7d396ed16fefe728a75cd9de079eb55cc1e37c4fb7c56a3e444bf5724c
4
+ data.tar.gz: 4ac98ed1bbbe88e4fac544fbfb70e7c080914106b765ca00a7d32f85188a516a
5
5
  SHA512:
6
- metadata.gz: 8a714fa412538a84dbed42f938eb419a85c626a045a5fc6fa8865014b039bc49eee75eb429c5adeb6d946908ab3e09c04bf5b8240c231090e955b6f8dbed5d4c
7
- data.tar.gz: d82cd3eaf425d213c12d99a813a6de7656b377241a64c0c82bd6ba098e42e69334567f8b1e03d8e1ff209932b4e8b0310725b57be0a91eb720d30b752e63ca06
6
+ metadata.gz: 9efc8715d5ca42841968eaa18ea0c611c57bc08734d6ebff1fdd79e0139acbabe2a50416fedd431e38f2ea74007c6c854387dd96cfc08dd6a36affd1d9808929
7
+ data.tar.gz: aca4c137a6550bb30ef6d5c0254ddba6bde197b647c3a0c94f73a0a9090dd6d1256a46da17c6ba9091b197ff14c6b1695eb5361901c4eebe6ed3aeba235100f0
data/README.md CHANGED
@@ -58,6 +58,9 @@ end
58
58
  ```ruby
59
59
  class Member < Sequel::Model
60
60
  plugin :privacy
61
+
62
+ # Include this module if this model can be used to create a viewer context.
63
+ include Sequel::Privacy::IActor
61
64
 
62
65
  privacy do
63
66
  # Define who can view this model; be strategic about the order of your policies so that You
@@ -77,29 +80,36 @@ The `privacy` block provides:
77
80
  - `field :name, *policies` - Protect a field (auto-creates `:view_#{field}` policy)
78
81
  - `finalize!` - Prevent further modifications to privacy settings
79
82
 
80
- `AlwaysDeny` is automatically appended to all policy chains (fail-secure by default).
83
+ `AlwaysDeny` is automatically appended to all policy chains if you don't include it, but it's better to add it explictly.
84
+ This behavior may change.
81
85
 
82
86
  ### 3. Query with Privacy Enforcement
83
87
 
84
88
  ```ruby
85
- # Create a viewer context
86
- vc = Sequel::Privacy::ViewerContext.for_actor(current_user)
89
+ # Create a viewer context; one viewer context for each request.
90
+ # In a Roda app, you could do this at the top of your routing tree,
91
+ # in Sinatra you could do this in a `before` filter. See the dedicated
92
+ # section below for more information about VCs.
93
+ current_vc = Sequel::Privacy::ViewerContext.for_actor(current_user)
87
94
 
88
- # Query - results are automatically filtered by :view policy
89
- members = Member.for_vc(vc).where(org_id: 1).all
95
+ # Results will filter out records that your VC can't see.
96
+ members = Member.for_vc(vc).where(org_id: current_user.org_id).all
90
97
 
91
- # Check permissions explicitly
92
- member.allow?(vc, :view) # => true/false
93
- member.allow?(vc, :edit) # => true/false
98
+ # But DON'T rely on the privacy checker in place of refining your query
99
+ # with privacy/permissions in-mind.
100
+ my_groups = Group.for_vc(vc).all # DONT: This results on tons of records being returned, processed and filtered for no reason.
101
+ my_groups = Group.for_vc(vc).where(creator: current_user).all # DO
94
102
 
95
- # Protected fields return nil if denied
103
+ # Field-level privacy policies will be enforced on access.
96
104
  member.email # => nil if :view_email denies
97
105
  member.phone # => nil if :view_phone denies
98
106
  ```
99
107
 
100
108
  ## Policy Definition
101
109
 
102
- 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. Policies accept up to three parameters: `actor`, `subject` & `actor` or `subject`, `actor` and `direct_object`.
110
+ 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.
111
+
112
+ Policies accept up to three parameters: `actor`, `subject` & `actor` or `subject`, `actor` and `direct_object`.
103
113
 
104
114
 
105
115
  ```ruby
@@ -114,24 +124,34 @@ policy :AllowAdmins, ->(_subject, actor) {
114
124
  allow if actor.is_role?(:admin)
115
125
  }
116
126
 
117
- policy :AllowOwner, ->(subject, actor) {
127
+ policy :AllowOwner, ->(_subject, actor) {
118
128
  allow if subject.owner_id == actor.id
119
129
  }
120
130
 
121
- policy :AllowSelfJoin, ->(_group, actor, target_user) {
122
- allow if actor.id == target_user.id
131
+ policy :AllowIfDirectObjectIsActor, ->(_subject, actor, direct_object) {
132
+ allow if actor.id == direct_object.id
123
133
  }
124
134
 
125
- policy :AllowSelfRemove, ->(_group, actor, target_user) {
126
- allow if actor.id == target_user.id
127
- }
128
135
  ```
129
136
 
130
- ### Policy Return Values
137
+ If you have lots of different objects and want to make your policies more specific, you can define policies in
138
+ different modules.
131
139
 
132
- - `allow` - Permits the action, stops evaluation
133
- - `deny` - Rejects the action, stops evaluation
134
- - `pass` (or no explicit return) - Continues to the next policy in the chain
140
+ ```ruby
141
+ module P
142
+ module Groups
143
+ extend Sequel::Privacy::PolicyDSL
144
+
145
+ policy :AllowIfOpen, -> (subject, _actor) {
146
+ allow if subject.open?
147
+ }
148
+
149
+ policy :AllowIfMember, -> (subject, actor) {
150
+ allow if subject.includes_member? actor
151
+ }
152
+ end
153
+ end
154
+ ```
135
155
 
136
156
  ### Policy Options
137
157
 
@@ -151,7 +171,13 @@ policy :MyPolicy, ->() { ... },
151
171
  Use `all()` to require multiple conditions:
152
172
 
153
173
  ```ruby
154
- policy :AllowMemberToRemoveSelf, ->(subject, actor, direct_object) {
174
+ policy :AllowAddSelfToOpenGroup, ->(subject, actor, direct_object) {
175
+ all(
176
+ P::AllowIfGroupIsOpen
177
+ P::AllowIfDirectObjectIsActor
178
+ )
179
+ }
180
+ policy :AllowRemoveSelf, ->(subject, actor, direct_object) {
155
181
  all(
156
182
  P::AllowIfIncludesMember,
157
183
  P::AllowIfDirectObjectIsActor
@@ -170,13 +196,14 @@ Anonymous VCs are useful for logged out users, and can check that their access i
170
196
  that are meant to be fully public.
171
197
 
172
198
  Omniscient VCs are most useful when your application needs to see an object that a user cannot for some reason.
173
- Handle them with care. Login is the most salient example.
199
+ Handle them with care. Login is the most salient example (see note below for more detail).
174
200
 
175
201
  All-Powerful VCs bypass all privacy checks and are used in situations where the system needs unfettered access
176
202
  to models. In a production setting, your application should prohibit raw Database access outside of the privacy-aware
177
203
  system, so these VCs give you an escape hatch for things like scripts while also keeping an audit trail.
178
204
 
179
205
  `omniscient` and `all_powerful` require a reason (symbol) for audit logging.
206
+ You could also create lint rules that prevent the casual creation of these viewer contexts.
180
207
 
181
208
  ```ruby
182
209
  # Standard viewer (most common)
@@ -199,6 +226,36 @@ current_vc = Sequel::Privacy::ViewerContext.for_actor(current_user)
199
226
  admin_vc = Sequel::Privacy::ViewerContext.all_powerful(:admin_migration)
200
227
  ```
201
228
 
229
+ ### A Note Login & Authenticated Users
230
+
231
+ If your User or equivalent model is privacy-aware *and* is protected by
232
+ policies that would complicating fetching (or login), then you will have
233
+ trouble creating a `current_user` for an `ActorVC`.
234
+
235
+ In both cases you can use an `OmniscientVC` to make your initial User query.
236
+
237
+ ```ruby
238
+ before do
239
+ if session[:user_id]
240
+ current_user = Sequel::Privacy::ViewerContext.omniscient(:session).then {|vc| User.for_vc(vc)[session[:user_id]] }
241
+ current_vc = Sequel::Privacy::ViewerContext.for_actor(current_user)
242
+ else
243
+ current_user = nil
244
+ current_vc = Sequel::Privacy::ViewerContext.anonymous
245
+ end
246
+ end
247
+
248
+ post '/auth/password' do
249
+ user = Sequel::Privacy::ViewerContext.omniscient(:login).then {|vc| User.for_vc(vc).first(email: params[:email]) }
250
+
251
+ pass unless user
252
+ pass unless user.password == params[:password]
253
+
254
+ session[:user_id] = user.id
255
+ redirect '/'
256
+ end
257
+ ```
258
+
202
259
  ## Mutation Enforcement
203
260
 
204
261
  When a viewer context is attached, mutations are automatically checked:
@@ -251,7 +308,7 @@ The `association` block supports three actions:
251
308
  - `:remove` - Wraps `remove_*` method (e.g., `remove_member`)
252
309
  - `:remove_all` - Wraps `remove_all_*` method (e.g., `remove_all_members`)
253
310
 
254
- Association policies use 3-arity, receiving `(subject, actor, direct_object)`:
311
+ Association policies receive `(subject, actor, direct_object)`:
255
312
  - `subject` - The model instance (e.g., the group)
256
313
  - `actor` - The current user from the viewer context
257
314
  - `direct_object` - The object being added/removed (e.g., the user being added to the group)
@@ -260,17 +317,17 @@ For `remove_all`, the direct object is `nil` since there's no specific target.
260
317
 
261
318
  ```ruby
262
319
  # Allow users to add/remove themselves
263
- policy :AllowSelfJoin, ->(_group, actor, target_user) {
264
- allow if actor.id == target_user.id
320
+ policy :AllowSelfJoin, ->(_subject, actor, direct_object) {
321
+ allow if actor.id == direct_object.id
265
322
  }, single_match: true
266
323
 
267
- policy :AllowSelfRemove, ->(_group, actor, target_user) {
268
- allow if actor.id == target_user.id
324
+ policy :AllowSelfRemove, ->(_subject, actor, direct_object) {
325
+ allow if actor.id == direct_object.id
269
326
  }, single_match: true
270
327
 
271
328
  # Allow group admins to add/remove anyone
272
- policy :AllowGroupAdmin, ->(group, actor, _target_user) {
273
- allow if GroupAdmin.where(group_id: group.id, user_id: actor.id).exists?
329
+ policy :AllowGroupAdmin, ->(subject, actor, direct_object) {
330
+ allow if subject.includes_admin?(actor)
274
331
  }
275
332
  ```
276
333
 
@@ -292,11 +349,6 @@ group.remove_all_members
292
349
  group.add_member(other_user) # Raises Sequel::Privacy::Unauthorized
293
350
  ```
294
351
 
295
- Association privacy methods:
296
- - Require a viewer context (raises `MissingViewerContext` if missing)
297
- - Deny operations with `OmniscientVC` (read-only context cannot mutate)
298
- - Work with both `one_to_many` and `many_to_many` associations
299
-
300
352
  ### Exception Types
301
353
 
302
354
  - `Sequel::Privacy::Unauthorized` - Action denied at the record level
@@ -305,7 +357,9 @@ Association privacy methods:
305
357
 
306
358
  ## Logging
307
359
 
308
- Configure a logger to see policy evaluation:
360
+ Configure a logger to see policy evaluation. It will show the evaluation results
361
+ (ALLOW, DENY, PASS) as well as cache hits/optimizations and note when privacy
362
+ is bypassed by an APVC or an OmniVC.
309
363
 
310
364
  ```ruby
311
365
  Sequel::Privacy.logger = Logger.new(STDOUT)
@@ -313,12 +367,6 @@ Sequel::Privacy.logger = Logger.new(STDOUT)
313
367
  Sequel::Privacy.logger = SemanticLogger['Privacy']
314
368
  ```
315
369
 
316
- Log output shows:
317
- - Policy evaluation results (ALLOW/DENY/PASS)
318
- - Cache hits
319
- - Single-match optimizations
320
- - All-powerful/omniscient context bypasses
321
-
322
370
  ## Cache Management
323
371
 
324
372
  Policy results are cached per-request to avoid redundant evaluation. Clear between requests:
@@ -346,7 +394,8 @@ Sequel::Privacy.single_matches.clear
346
394
 
347
395
  ## Actor Interface
348
396
 
349
- Your user/member model must implement `Sequel::Privacy::IActor`:
397
+ Your user/member model must include and implement `Sequel::Privacy::IActor`.
398
+ This will be runtime checked by Sorbet.
350
399
 
351
400
  ```ruby
352
401
  class Member < Sequel::Model
@@ -358,17 +407,13 @@ class Member < Sequel::Model
358
407
  end
359
408
  ```
360
409
 
361
- The interface requires:
362
- - `id` - Returns the actor's unique identifier
363
-
364
- You can add additional methods like `is_role?` for use in your policies, but they are not required by the interface.
365
-
366
410
  ## Policy Inheritance
367
411
 
368
412
  Child classes inherit privacy policies from their parents:
369
413
 
370
414
  ```ruby
371
415
  class User < Sequel::Model
416
+ include Sequel::Privacy::IActor
372
417
  plugin :privacy
373
418
 
374
419
  privacy do
@@ -386,14 +431,25 @@ end
386
431
 
387
432
  ## Built-in Policies
388
433
 
389
- - `Sequel::Privacy::BuiltInPolicies::AlwaysDeny` - Always denies (fail-secure default)
434
+ - `Sequel::Privacy::BuiltInPolicies::AlwaysDeny` - Always denies; add it to the end of your policy chains.
390
435
  - `Sequel::Privacy::BuiltInPolicies::AlwaysAllow` - Always allows
391
436
  - `Sequel::Privacy::BuiltInPolicies::PassAndLog` - Passes with a log message (useful for debugging)
392
437
 
393
438
 
394
439
  ## Type Safety (Sorbet)
395
440
 
396
- The gem is mostly fully typed with Sorbet. Type definitions are provided for all public APIs.
441
+ The gem is mostly fully typed with Sorbet. Type definitions are provided for all public APIs. To ensure
442
+ that Tapioca imports the required definitions, you may need to add this to your `sorbet/tapioca/require.rb`:
443
+
444
+ ```ruby
445
+ require "sequel-privacy"
446
+ require "sequel/plugins/privacy"
447
+
448
+ # Force Tapioca to see the plugin modules by applying them to a dummy class
449
+ Class.new(Sequel::Model) do
450
+ plugin :privacy
451
+ end
452
+ ```
397
453
 
398
454
  ## AI Statement
399
455
 
@@ -401,12 +457,6 @@ The core of this project was written by me (arbales) over the course of 2025 for
401
457
  manages mailing lists and member information for a social group. Claude assisted substantially with
402
458
  extracting it into a Gem and wrote the tests in their entirety.
403
459
 
404
- ## TODO
405
-
406
- I'd like to support generation of Sorbet signatures on models that use the plugin so that
407
- Sorbet users don't have to shim their models to use it. In my own work, I've shimmed my
408
- base model class, but this isn't ideal.
409
-
410
460
  ## License
411
461
 
412
462
  MIT
@@ -37,8 +37,8 @@ module Sequel
37
37
  extend T::Sig
38
38
 
39
39
  # Called once when plugin first loads on a model
40
- sig { params(model: T.class_of(Sequel::Model), opts: T::Hash[Symbol, T.untyped]).void }
41
- def self.apply(model, opts = {})
40
+ sig { params(model: T.class_of(Sequel::Model), _opts: T::Hash[Symbol, T.untyped]).void }
41
+ def self.apply(model, _opts = {})
42
42
  model.instance_variable_set(:@privacy_policies, {})
43
43
  model.instance_variable_set(:@privacy_fields, {})
44
44
  model.instance_variable_set(:@privacy_association_policies, {})
@@ -56,7 +56,10 @@ module Sequel
56
56
  class AssociationPrivacyDSL
57
57
  extend T::Sig
58
58
 
59
- sig { params(model_class: T.untyped, assoc_name: Symbol, policy_resolver: T.proc.params(policies: T::Array[T.untyped]).returns(T::Array[T.untyped])).void }
59
+ sig {
60
+ params(model_class: ClassMethods, assoc_name: Symbol,
61
+ policy_resolver: T.proc.params(policies: T::Array[T.untyped]).returns(T::Array[T.untyped])).void
62
+ }
60
63
  def initialize(model_class, assoc_name, policy_resolver)
61
64
  @model_class = model_class
62
65
  @assoc_name = assoc_name
@@ -68,10 +71,11 @@ module Sequel
68
71
  sig { params(action: Symbol, policies: T.untyped).void }
69
72
  def can(action, *policies)
70
73
  unless %i[add remove remove_all].include?(action)
71
- Kernel.raise ArgumentError, "Association action must be :add, :remove, or :remove_all, got #{action.inspect}"
74
+ Kernel.raise ArgumentError,
75
+ "Association action must be :add, :remove, or :remove_all, got #{action.inspect}"
72
76
  end
73
77
 
74
- resolved = @policy_resolver.call(policies)
78
+ resolved = @policy_resolver.(policies)
75
79
  @pending_policies[action] ||= []
76
80
  T.must(@pending_policies[action]).concat(resolved)
77
81
  end
@@ -80,10 +84,9 @@ module Sequel
80
84
  sig { void }
81
85
  def finalize_association!
82
86
  @pending_policies.each do |action, policies|
83
- T.unsafe(@model_class).register_association_policies(@assoc_name, action, policies, defer_setup: true)
87
+ @model_class.register_association_policies(@assoc_name, action, policies, defer_setup: true)
84
88
  end
85
- # Now set up the privacy wrappers after all policies are registered
86
- T.unsafe(@model_class).setup_association_privacy(@assoc_name)
89
+ @model_class.setup_association_privacy(@assoc_name)
87
90
  end
88
91
  end
89
92
 
@@ -91,7 +94,7 @@ module Sequel
91
94
  class PrivacyDSL
92
95
  extend T::Sig
93
96
 
94
- sig { params(model_class: T.untyped).void }
97
+ sig { params(model_class: ClassMethods).void }
95
98
  def initialize(model_class)
96
99
  @model_class = model_class
97
100
  end
@@ -100,7 +103,7 @@ module Sequel
100
103
  sig { params(action: Symbol, policies: T.untyped).void }
101
104
  def can(action, *policies)
102
105
  resolved = resolve_policies(policies)
103
- T.unsafe(@model_class).register_policies(action, resolved)
106
+ @model_class.register_policies(action, resolved)
104
107
  end
105
108
 
106
109
  # Define a protected field with its policies
@@ -108,8 +111,8 @@ module Sequel
108
111
  def field(name, *policies)
109
112
  resolved = resolve_policies(policies)
110
113
  policy_name = :"view_#{name}"
111
- T.unsafe(@model_class).register_policies(policy_name, resolved)
112
- T.unsafe(@model_class).register_protected_field(name, policy_name)
114
+ @model_class.register_policies(policy_name, resolved)
115
+ @model_class.register_protected_field(name, policy_name)
113
116
  end
114
117
 
115
118
  # Define policies for an association
@@ -131,7 +134,7 @@ module Sequel
131
134
  # Finalize privacy settings (no more changes allowed)
132
135
  sig { void }
133
136
  def finalize!
134
- T.unsafe(@model_class).finalize_privacy!
137
+ @model_class.finalize_privacy!
135
138
  end
136
139
 
137
140
  private
@@ -198,7 +201,7 @@ module Sequel
198
201
 
199
202
  unless vc || allow_unsafe_access?
200
203
  Kernel.raise Sequel::Privacy::MissingViewerContext,
201
- "#{self} requires a ViewerContext. Use #{self}.for_vc(vc) or call #{self}.allow_unsafe_access!"
204
+ "#{self} requires a ViewerContext. Use #{self}.for_vc(vc) or call #{self}.allow_unsafe_access!"
202
205
  end
203
206
 
204
207
  # Create the instance via parent chain
@@ -208,12 +211,14 @@ module Sequel
208
211
  if vc && instance
209
212
  instance.instance_variable_set(:@viewer_context, vc)
210
213
 
211
- # Check :view policy (skip for InternalPolicyEvaluationVC - used during policy evaluation)
212
- unless vc.is_a?(Sequel::Privacy::InternalPolicyEvaluationVC)
213
- unless instance.allow?(vc, :view)
214
- Sequel::Privacy.logger&.debug { "Privacy denied :view on #{self}[#{instance.pk}]" }
215
- return nil # Filtered out
216
- end
214
+ # During nested policy evaluation, return raw rows so the outer
215
+ # policy can traverse data (e.g. checking membership) without
216
+ # recursive :view filtering.
217
+ return instance if Sequel::Privacy::Enforcer.in_policy_eval?
218
+
219
+ unless T.cast(instance, InstanceMethods).allow?(vc, :view)
220
+ Sequel::Privacy.logger&.debug { "Privacy denied :view on #{self}[#{instance.pk}]" }
221
+ return nil
217
222
  end
218
223
  end
219
224
 
@@ -290,25 +295,21 @@ module Sequel
290
295
 
291
296
  # Override the field getter
292
297
  define_method(field) do
298
+ # During nested policy evaluation, return raw value without
299
+ # checking the field's view policy.
300
+ return original_method.bind(self).() if Sequel::Privacy::Enforcer.in_policy_eval?
301
+
293
302
  vc = instance_variable_get(:@viewer_context)
294
303
 
295
- # Require VC for protected field access
296
304
  unless vc
297
305
  Kernel.raise Sequel::Privacy::MissingViewerContext,
298
- "#{self.class}##{field} requires a ViewerContext"
306
+ "#{self.class}##{field} requires a ViewerContext"
299
307
  end
300
308
 
301
- value = original_method.bind(self).call
309
+ value = original_method.bind(self).()
310
+ return unless T.cast(self, InstanceMethods).allow?(vc, policy_name)
302
311
 
303
- # InternalPolicyEvaluationVC = return raw value (for policy checks)
304
- return value if vc.is_a?(Sequel::Privacy::InternalPolicyEvaluationVC)
305
-
306
- # Check privacy policy
307
- if T.unsafe(self).allow?(vc, policy_name)
308
- value
309
- else
310
- nil
311
- end
312
+ value
312
313
  end
313
314
  end
314
315
 
@@ -316,9 +317,7 @@ module Sequel
316
317
  # @param defer_setup [Boolean] If true, don't set up wrappers yet (caller will call setup_association_privacy)
317
318
  sig { params(assoc_name: Symbol, action: Symbol, policies: T::Array[T.untyped], defer_setup: T::Boolean).void }
318
319
  def register_association_policies(assoc_name, action, policies, defer_setup: false)
319
- if privacy_finalized?
320
- Kernel.raise "Privacy policies have been finalized for #{self}"
321
- end
320
+ Kernel.raise "Privacy policies have been finalized for #{self}" if privacy_finalized?
322
321
 
323
322
  privacy_association_policies[assoc_name] ||= {}
324
323
  assoc_hash = T.must(privacy_association_policies[assoc_name])
@@ -342,6 +341,7 @@ module Sequel
342
341
  # Track which associations have been wrapped to avoid double-wrapping
343
342
  @_wrapped_associations ||= T.let({}, T.nilable(T::Hash[Symbol, T::Boolean]))
344
343
  return if @_wrapped_associations[assoc_name]
344
+
345
345
  @_wrapped_associations[assoc_name] = true
346
346
 
347
347
  # Determine the singular name for method naming
@@ -363,9 +363,9 @@ module Sequel
363
363
 
364
364
  # Wrap remove_all_* method if :remove_all policy exists
365
365
  remove_all_policies = assoc_policies[:remove_all]
366
- if remove_all_policies && method_defined?(:"remove_all_#{reflection[:name]}")
367
- _wrap_association_remove_all(assoc_name, reflection[:name], remove_all_policies)
368
- end
366
+ return unless remove_all_policies && method_defined?(:"remove_all_#{reflection[:name]}")
367
+
368
+ _wrap_association_remove_all(assoc_name, reflection[:name], remove_all_policies)
369
369
  end
370
370
 
371
371
  # Finalize privacy settings (no more changes allowed)
@@ -440,30 +440,26 @@ module Sequel
440
440
 
441
441
  # Load association with VC context set if available
442
442
  obj = if vc && assoc_class.respond_to?(:privacy_vc_key)
443
- vc_key = assoc_class.privacy_vc_key
444
- old_vc = Thread.current[vc_key]
445
- Thread.current[vc_key] = vc
446
- begin
447
- original.bind(self).call
448
- ensure
449
- Thread.current[vc_key] = old_vc
450
- end
451
- else
452
- original.bind(self).call
453
- end
443
+ vc_key = assoc_class.privacy_vc_key
444
+ old_vc = Thread.current[vc_key]
445
+ Thread.current[vc_key] = vc
446
+ begin
447
+ original.bind(self).()
448
+ ensure
449
+ Thread.current[vc_key] = old_vc
450
+ end
451
+ else
452
+ original.bind(self).()
453
+ end
454
454
 
455
455
  return nil unless obj
456
456
  return obj unless vc
457
+ return obj if Sequel::Privacy::Enforcer.in_policy_eval?
457
458
 
458
- # InternalPolicyEvaluationVC = return raw data (for policy checks)
459
- # This allows policies to access associations without filtering
460
- return obj if vc.is_a?(Sequel::Privacy::InternalPolicyEvaluationVC)
461
-
462
- # Attach viewer context to associated object
463
- obj.instance_variable_set(:@viewer_context, vc) if obj.respond_to?(:allow?)
459
+ privacy_aware = obj.is_a?(Sequel::Model) && obj.class.respond_to?(:privacy_vc_key)
460
+ obj.instance_variable_set(:@viewer_context, vc) if privacy_aware
464
461
 
465
- # Check :view policy on associated object
466
- if obj.respond_to?(:allow?) && !obj.allow?(vc, :view)
462
+ if privacy_aware && !T.cast(obj, InstanceMethods).allow?(vc, :view)
467
463
  nil
468
464
  else
469
465
  obj
@@ -485,29 +481,26 @@ module Sequel
485
481
 
486
482
  # Load association with VC context set if available
487
483
  objs = if vc && assoc_class.respond_to?(:privacy_vc_key)
488
- vc_key = assoc_class.privacy_vc_key
489
- old_vc = Thread.current[vc_key]
490
- Thread.current[vc_key] = vc
491
- begin
492
- original.bind(self).call
493
- ensure
494
- Thread.current[vc_key] = old_vc
495
- end
496
- else
497
- original.bind(self).call
498
- end
484
+ vc_key = assoc_class.privacy_vc_key
485
+ old_vc = Thread.current[vc_key]
486
+ Thread.current[vc_key] = vc
487
+ begin
488
+ original.bind(self).()
489
+ ensure
490
+ Thread.current[vc_key] = old_vc
491
+ end
492
+ else
493
+ original.bind(self).()
494
+ end
499
495
 
500
496
  return objs unless vc
497
+ return objs if Sequel::Privacy::Enforcer.in_policy_eval?
501
498
 
502
- # InternalPolicyEvaluationVC = return raw data (for policy checks like includes_member?)
503
- # This allows policies to access associations without filtering
504
- return objs if vc.is_a?(Sequel::Privacy::InternalPolicyEvaluationVC)
505
-
506
- # Filter array, attaching VC and checking :view policy
507
499
  objs.filter_map do |obj|
508
- obj.instance_variable_set(:@viewer_context, vc) if obj.respond_to?(:allow?)
500
+ privacy_aware = obj.is_a?(Sequel::Model) && obj.class.respond_to?(:privacy_vc_key)
501
+ obj.instance_variable_set(:@viewer_context, vc) if privacy_aware
509
502
 
510
- if obj.respond_to?(:allow?) && !obj.allow?(vc, :view)
503
+ if privacy_aware && !T.cast(obj, InstanceMethods).allow?(vc, :view)
511
504
  nil
512
505
  else
513
506
  obj
@@ -516,8 +509,8 @@ module Sequel
516
509
  end
517
510
  end
518
511
 
519
- sig { params(assoc_name: Symbol, singular_name: Symbol, policies: T::Array[T.untyped]).void }
520
- def _wrap_association_add(assoc_name, singular_name, policies)
512
+ sig { params(_assoc_name: Symbol, singular_name: Symbol, policies: T::Array[T.untyped]).void }
513
+ def _wrap_association_add(_assoc_name, singular_name, policies)
521
514
  method_name = :"add_#{singular_name}"
522
515
  original = instance_method(method_name)
523
516
 
@@ -526,12 +519,12 @@ module Sequel
526
519
 
527
520
  unless vc
528
521
  Kernel.raise Sequel::Privacy::MissingViewerContext,
529
- "Cannot #{method_name} without a viewer context"
522
+ "Cannot #{method_name} without a viewer context"
530
523
  end
531
524
 
532
525
  if vc.is_a?(Sequel::Privacy::OmniscientVC)
533
526
  Kernel.raise Sequel::Privacy::Unauthorized,
534
- "Cannot #{method_name} with OmniscientVC"
527
+ "Cannot #{method_name} with OmniscientVC"
535
528
  end
536
529
 
537
530
  # Check policy with 3-arity: (subject=self, actor, direct_object=obj)
@@ -539,15 +532,15 @@ module Sequel
539
532
 
540
533
  unless allowed
541
534
  Kernel.raise Sequel::Privacy::Unauthorized,
542
- "Cannot #{method_name} on #{self.class}"
535
+ "Cannot #{method_name} on #{self.class}"
543
536
  end
544
537
 
545
- original.bind(self).call(obj)
538
+ original.bind(self).(obj)
546
539
  end
547
540
  end
548
541
 
549
- sig { params(assoc_name: Symbol, singular_name: Symbol, policies: T::Array[T.untyped]).void }
550
- def _wrap_association_remove(assoc_name, singular_name, policies)
542
+ sig { params(_assoc_name: Symbol, singular_name: Symbol, policies: T::Array[T.untyped]).void }
543
+ def _wrap_association_remove(_assoc_name, singular_name, policies)
551
544
  method_name = :"remove_#{singular_name}"
552
545
  original = instance_method(method_name)
553
546
 
@@ -556,12 +549,12 @@ module Sequel
556
549
 
557
550
  unless vc
558
551
  Kernel.raise Sequel::Privacy::MissingViewerContext,
559
- "Cannot #{method_name} without a viewer context"
552
+ "Cannot #{method_name} without a viewer context"
560
553
  end
561
554
 
562
555
  if vc.is_a?(Sequel::Privacy::OmniscientVC)
563
556
  Kernel.raise Sequel::Privacy::Unauthorized,
564
- "Cannot #{method_name} with OmniscientVC"
557
+ "Cannot #{method_name} with OmniscientVC"
565
558
  end
566
559
 
567
560
  # Check policy with 3-arity: (subject=self, actor, direct_object=obj)
@@ -569,15 +562,15 @@ module Sequel
569
562
 
570
563
  unless allowed
571
564
  Kernel.raise Sequel::Privacy::Unauthorized,
572
- "Cannot #{method_name} on #{self.class}"
565
+ "Cannot #{method_name} on #{self.class}"
573
566
  end
574
567
 
575
- original.bind(self).call(obj)
568
+ original.bind(self).(obj)
576
569
  end
577
570
  end
578
571
 
579
- sig { params(assoc_name: Symbol, plural_name: Symbol, policies: T::Array[T.untyped]).void }
580
- def _wrap_association_remove_all(assoc_name, plural_name, policies)
572
+ sig { params(_assoc_name: Symbol, plural_name: Symbol, policies: T::Array[T.untyped]).void }
573
+ def _wrap_association_remove_all(_assoc_name, plural_name, policies)
581
574
  method_name = :"remove_all_#{plural_name}"
582
575
  original = instance_method(method_name)
583
576
 
@@ -586,23 +579,23 @@ module Sequel
586
579
 
587
580
  unless vc
588
581
  Kernel.raise Sequel::Privacy::MissingViewerContext,
589
- "Cannot #{method_name} without a viewer context"
582
+ "Cannot #{method_name} without a viewer context"
590
583
  end
591
584
 
592
585
  if vc.is_a?(Sequel::Privacy::OmniscientVC)
593
586
  Kernel.raise Sequel::Privacy::Unauthorized,
594
- "Cannot #{method_name} with OmniscientVC"
587
+ "Cannot #{method_name} with OmniscientVC"
595
588
  end
596
589
 
597
590
  # Check policy with 2-arity: (subject=self, actor) - no direct object for remove_all
598
- allowed = Sequel::Privacy::Enforcer.enforce(policies, self, vc, nil)
591
+ allowed = Sequel::Privacy::Enforcer.enforce(policies, self, vc)
599
592
 
600
593
  unless allowed
601
594
  Kernel.raise Sequel::Privacy::Unauthorized,
602
- "Cannot #{method_name} on #{self.class}"
595
+ "Cannot #{method_name} on #{self.class}"
603
596
  end
604
597
 
605
- original.bind(self).call
598
+ original.bind(self).()
606
599
  end
607
600
  end
608
601
  end
@@ -614,21 +607,20 @@ module Sequel
614
607
  requires_ancestor { Sequel::Model }
615
608
  mixes_in_class_methods(ClassMethods)
616
609
 
617
-
618
610
  sig { returns(T.nilable(Sequel::Privacy::ViewerContext)) }
619
611
  def viewer_context
620
- @viewer_context
612
+ @viewer_context = T.let(@viewer_context, T.nilable(Sequel::Privacy::ViewerContext))
621
613
  end
622
614
 
623
615
  sig { params(vc: T.nilable(Sequel::Privacy::ViewerContext)).returns(T.nilable(Sequel::Privacy::ViewerContext)) }
624
616
  def viewer_context=(vc)
625
- @viewer_context = vc
617
+ @viewer_context = T.let(vc, T.nilable(Sequel::Privacy::ViewerContext))
626
618
  end
627
619
 
628
620
  # Attach a viewer context to this model instance
629
621
  sig { params(vc: Sequel::Privacy::ViewerContext).returns(T.self_type) }
630
622
  def for_vc(vc)
631
- @viewer_context = vc
623
+ @viewer_context = T.let(vc, T.nilable(Sequel::Privacy::ViewerContext))
632
624
  self
633
625
  end
634
626
 
@@ -646,45 +638,31 @@ module Sequel
646
638
  ).returns(T::Boolean)
647
639
  end
648
640
  def allow?(vc, action, direct_object = nil)
649
- policies = T.unsafe(self.class).privacy_policies[action]
641
+ policies = _privacy_class.privacy_policies[action]
650
642
  unless policies
651
643
  Sequel::Privacy.logger&.error("No policies defined for :#{action} on #{self.class}")
652
644
  return false
653
645
  end
654
646
 
655
- # Use InternalPolicyEvaluationVC during policy evaluation.
656
- # This signals to association wrappers that they should return raw data
657
- # without filtering, allowing policies to check things like "is actor a
658
- # member of this list?" by accessing list.members without recursively
659
- # checking each member's :view policy.
660
- saved_vc = @viewer_context
661
- @viewer_context = Sequel::Privacy::InternalPolicyEvaluationVC.new
662
- begin
663
- Sequel::Privacy::Enforcer.enforce(policies, self, vc, direct_object)
664
- ensure
665
- @viewer_context = saved_vc
666
- end
647
+ Sequel::Privacy::Enforcer.enforce(policies, self, vc, direct_object)
667
648
  end
668
649
 
669
650
  # Override save to check privacy policies
670
651
  sig { params(opts: T.untyped).returns(T.nilable(T.self_type)) }
671
652
  def save(*opts)
672
- vc = @viewer_context
653
+ vc = viewer_context
673
654
 
674
655
  if vc.is_a?(Sequel::Privacy::OmniscientVC)
675
- Kernel.raise Sequel::Privacy::Unauthorized, "Cannot mutate with OmniscientVC"
656
+ Kernel.raise Sequel::Privacy::Unauthorized, 'Cannot mutate with OmniscientVC'
676
657
  end
677
658
 
678
659
  if vc
679
660
  action = new? ? :create : :edit
680
661
 
681
- unless allow?(vc, action)
682
- Kernel.raise Sequel::Privacy::Unauthorized, "Cannot #{action} #{self.class}"
683
- end
662
+ Kernel.raise Sequel::Privacy::Unauthorized, "Cannot #{action} #{self.class}" unless allow?(vc, action)
684
663
 
685
- # Check field-level policies on changed fields
686
664
  changed_columns.each do |field|
687
- policy = T.unsafe(self.class).privacy_fields[field]
665
+ policy = _privacy_class.privacy_fields[field]
688
666
  next unless policy
689
667
 
690
668
  unless allow?(vc, policy)
@@ -700,14 +678,12 @@ module Sequel
700
678
  # Override update to check privacy policies
701
679
  sig { params(hash: T::Hash[Symbol, T.untyped]).returns(T.self_type) }
702
680
  def update(hash)
703
- vc = @viewer_context
681
+ vc = viewer_context
704
682
  if vc
705
- unless allow?(vc, :edit)
706
- Kernel.raise Sequel::Privacy::Unauthorized, "Cannot edit #{self.class}"
707
- end
683
+ Kernel.raise Sequel::Privacy::Unauthorized, "Cannot edit #{self.class}" unless allow?(vc, :edit)
708
684
 
709
685
  hash.each_key do |field|
710
- policy = T.unsafe(self.class).privacy_fields[field]
686
+ policy = _privacy_class.privacy_fields[field]
711
687
  next unless policy
712
688
 
713
689
  unless allow?(vc, policy)
@@ -720,11 +696,21 @@ module Sequel
720
696
  super
721
697
  end
722
698
 
699
+ private
700
+
701
+ # Typed view of the model class for accessing methods mixed in by the
702
+ # privacy plugin. The cast is sound because every class that includes
703
+ # InstanceMethods also extends ClassMethods (via mixes_in_class_methods).
704
+ sig { returns(ClassMethods) }
705
+ def _privacy_class
706
+ T.cast(self.class, ClassMethods)
707
+ end
708
+
723
709
  # Override delete to block OmniscientVC
724
710
  sig { returns(T.self_type) }
725
711
  def delete
726
- if @viewer_context.is_a?(Sequel::Privacy::OmniscientVC)
727
- Kernel.raise Sequel::Privacy::Unauthorized, "Cannot delete with OmniscientVC"
712
+ if viewer_context.is_a?(Sequel::Privacy::OmniscientVC)
713
+ Kernel.raise Sequel::Privacy::Unauthorized, 'Cannot delete with OmniscientVC'
728
714
  end
729
715
  super
730
716
  end
@@ -751,13 +737,13 @@ module Sequel
751
737
  vc = opts[:viewer_context]
752
738
  return super unless vc
753
739
 
754
- model_class = T.unsafe(model)
740
+ model_class = T.cast(model, ClassMethods)
755
741
  vc_key = model_class.privacy_vc_key
756
742
  proc do |values|
757
743
  old_vc = Thread.current[vc_key]
758
744
  Thread.current[vc_key] = vc
759
745
  begin
760
- model_class.call(values)
746
+ model_class.(values)
761
747
  ensure
762
748
  Thread.current[vc_key] = old_vc
763
749
  end
@@ -1,40 +1,50 @@
1
- # typed: ignore
1
+ # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Sequel
5
5
  module Privacy
6
6
  # Actions provides the DSL methods available inside policy lambdas.
7
- # When policies are evaluated, they execute in the context of this struct,
7
+ # When policies are evaluated, they execute in the context of this object,
8
8
  # giving them access to allow, deny, pass, and all methods.
9
9
  #
10
10
  # Example:
11
11
  # policy :AllowAdmins, ->(actor) {
12
12
  # allow if actor.is_role?(:admin)
13
13
  # }
14
- Actions = (Struct.new do
15
- extend T::Sig
14
+ class ActionsClass
15
+ extend T::Sig
16
16
 
17
- sig { returns(Symbol) }
18
- def allow
19
- :allow
20
- end
17
+ sig { returns(Symbol) }
18
+ def allow
19
+ :allow
20
+ end
21
21
 
22
- sig { returns(Symbol) }
23
- def deny
24
- :deny
25
- end
22
+ sig { returns(Symbol) }
23
+ def deny
24
+ :deny
25
+ end
26
26
 
27
- sig { returns(Symbol) }
28
- def pass
29
- :pass
30
- end
27
+ sig { returns(Symbol) }
28
+ def pass
29
+ :pass
30
+ end
31
31
 
32
- # Combine multiple policies - all must allow for the result to allow.
33
- # Any deny results in deny. Otherwise passes.
34
- sig { params(policies: T.untyped).returns(T::Array[T.untyped]) }
35
- def all(*policies)
36
- policies
37
- end
38
- end).new
32
+ # Combine multiple policies - all must allow for the result to allow.
33
+ # Any deny results in deny. Otherwise passes.
34
+ sig { params(policies: T.untyped).returns(T::Array[T.untyped]) }
35
+ def all(*policies)
36
+ policies
37
+ end
38
+
39
+ # Evaluate a policy lambda in the DSL context. Wraps instance_exec so
40
+ # callers don't have to fight Sorbet's strict block-shape signatures
41
+ # for arbitrary-arity policies.
42
+ sig { params(args: T.untyped, blk: Proc).returns(T.untyped) }
43
+ def evaluate(*args, &blk)
44
+ T.unsafe(self).instance_exec(*args, &blk)
45
+ end
46
+ end
47
+
48
+ Actions = T.let(ActionsClass.new, ActionsClass)
39
49
  end
40
50
  end
@@ -1,4 +1,4 @@
1
- # typed: false
1
+ # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Sequel
@@ -8,6 +8,13 @@ module Sequel
8
8
  module Enforcer
9
9
  extend T::Sig
10
10
 
11
+ # Thread-local flag set while a policy chain is being evaluated.
12
+ # Implicit :view enforcement (Model.call, field readers, association
13
+ # readers) checks this flag and returns raw data when set, so policies
14
+ # can traverse protected fields and associations without recursive
15
+ # filtering. Explicit `allow?` calls always run regardless.
16
+ EVAL_KEY = :sequel_privacy_in_policy_eval
17
+
11
18
  class << self
12
19
  extend T::Sig
13
20
 
@@ -18,6 +25,11 @@ module Sequel
18
25
  end
19
26
  end
20
27
 
28
+ sig { returns(T::Boolean) }
29
+ def self.in_policy_eval?
30
+ Thread.current[EVAL_KEY] == true
31
+ end
32
+
21
33
  # Main entry point for policy evaluation.
22
34
  #
23
35
  # @param policies [Array<Policy, Proc>] The policy chain to evaluate
@@ -34,39 +46,44 @@ module Sequel
34
46
  ).returns(T::Boolean)
35
47
  end
36
48
  def self.enforce(policies, subject, viewer_context, direct_object = nil)
37
- # All-powerful and omniscient contexts bypass all checks
38
- if viewer_context.is_a?(AllPowerfulVC)
39
- logger&.warn('BYPASS: All-powerful viewer context bypasses all privacy rules.')
40
- return true
41
- end
49
+ saved = Thread.current[EVAL_KEY]
50
+ Thread.current[EVAL_KEY] = true
51
+
52
+ begin
53
+ # All-powerful and omniscient contexts bypass all checks
54
+ if viewer_context.is_a?(AllPowerfulVC)
55
+ logger&.warn('BYPASS: All-powerful viewer context bypasses all privacy rules.')
56
+ return true
57
+ end
42
58
 
43
- if viewer_context.is_a?(OmniscientVC)
44
- logger&.debug { "BYPASS: Omniscient viewer context (#{viewer_context.reason})" }
45
- return true
46
- end
59
+ if viewer_context.is_a?(OmniscientVC)
60
+ logger&.debug { "BYPASS: Omniscient viewer context (#{viewer_context.reason})" }
61
+ return true
62
+ end
47
63
 
48
- actor = viewer_context.is_a?(ActorVC) ? viewer_context.actor : nil
64
+ actor = viewer_context.is_a?(ActorVC) ? viewer_context.actor : nil
49
65
 
50
- # Ensure we have policies to evaluate
51
- if policies.empty?
52
- logger&.error { "No policies for #{subject.class}[#{subject_id(subject)}]. Denying by default." }
53
- policies = [BuiltInPolicies::AlwaysDeny]
54
- end
66
+ if policies.empty?
67
+ logger&.error { "No policies for #{subject.class}[#{subject_id(subject)}]. Denying by default." }
68
+ policies = [BuiltInPolicies::AlwaysDeny]
69
+ end
55
70
 
56
- # Ensure policy chain ends with AlwaysDeny (fail-secure)
57
- unless policies.last == BuiltInPolicies::AlwaysDeny
58
- logger&.warn { 'Policy chain should end with AlwaysDeny. Appending it.' }
59
- policies = policies.dup << BuiltInPolicies::AlwaysDeny
60
- end
71
+ # Ensure policy chain ends with AlwaysDeny (fail-secure)
72
+ unless policies.last == BuiltInPolicies::AlwaysDeny
73
+ logger&.warn { 'Policy chain should end with AlwaysDeny. Appending it.' }
74
+ policies = policies.dup << BuiltInPolicies::AlwaysDeny
75
+ end
61
76
 
62
- # Evaluate policies in order
63
- policies.each do |uncasted_policy|
64
- result = policy_result(uncasted_policy, subject, actor, viewer_context, direct_object)
65
- return true if result == :allow
66
- return false if result == :deny
67
- end
77
+ policies.each do |uncasted_policy|
78
+ result = policy_result(uncasted_policy, subject, actor, viewer_context, direct_object)
79
+ return true if result == :allow
80
+ return false if result == :deny
81
+ end
68
82
 
69
- false
83
+ false
84
+ ensure
85
+ Thread.current[EVAL_KEY] = saved
86
+ end
70
87
  end
71
88
 
72
89
  # Compute cache key based on policy arity
@@ -201,13 +218,13 @@ module Sequel
201
218
 
202
219
  case policy.arity
203
220
  when 0
204
- Actions.instance_exec(&policy)
221
+ Actions.evaluate(&policy)
205
222
  when 1
206
- Actions.instance_exec(subject, &policy)
223
+ Actions.evaluate(subject, &policy)
207
224
  when 2
208
- Actions.instance_exec(subject, T.must(actor), &policy)
225
+ Actions.evaluate(subject, T.must(actor), &policy)
209
226
  else
210
- Actions.instance_exec(subject, T.must(actor), direct_object, &policy)
227
+ Actions.evaluate(subject, T.must(actor), direct_object, &policy)
211
228
  end
212
229
  end
213
230
 
@@ -21,11 +21,12 @@ module Sequel
21
21
  sig { returns(T.nilable(String)) }
22
22
  attr_reader :comment
23
23
 
24
- # Factory method for creating policies
24
+ # Factory method for creating policies. Accepts procs of any arity
25
+ # (0–3 args) returning :allow, :deny, :pass, or an Array of policies.
25
26
  sig do
26
27
  params(
27
28
  policy_name: Symbol,
28
- lam: T.proc.returns(Symbol),
29
+ lam: Proc,
29
30
  comment: T.nilable(String),
30
31
  cacheable: T::Boolean,
31
32
  single_match: T::Boolean
@@ -1,4 +1,4 @@
1
- # typed: false
1
+ # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Sequel
@@ -17,6 +17,13 @@ module Sequel
17
17
  # }, 'Allow admin users', cacheable: true
18
18
  # end
19
19
  module PolicyDSL
20
+ extend T::Sig
21
+ extend T::Helpers
22
+
23
+ # PolicyDSL is meant to be `extend`ed onto another Module, so `self`
24
+ # at runtime is always a Module that responds to `const_set`.
25
+ requires_ancestor { Module }
26
+
20
27
  # Define a new policy constant on the extending module.
21
28
  #
22
29
  # @param name [Symbol] The policy name (will become a constant)
@@ -24,6 +31,15 @@ module Sequel
24
31
  # @param comment [String, nil] Human-readable description
25
32
  # @param cacheable [Boolean] Whether results can be cached (default: true)
26
33
  # @param single_match [Boolean] Whether only one subject/actor can match (default: false)
34
+ sig do
35
+ params(
36
+ name: Symbol,
37
+ lam: Proc,
38
+ comment: T.nilable(String),
39
+ cacheable: T::Boolean,
40
+ single_match: T::Boolean
41
+ ).void
42
+ end
27
43
  def policy(name, lam, comment = nil, cacheable: true, single_match: false)
28
44
  p = Policy.new(&lam).setup(
29
45
  policy_name: name,
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Sequel
5
5
  module Privacy
6
- VERSION = '0.1.0'
6
+ VERSION = '0.2.0'
7
7
  end
8
8
  end
@@ -105,22 +105,6 @@ module Sequel
105
105
  end
106
106
  end
107
107
 
108
- # Internal policy evaluation viewer context.
109
- # Used internally during policy evaluation to allow raw association access
110
- # without triggering recursive privacy checks. For example, when checking
111
- # "is actor a member of this list?", we need to access list.members without
112
- # filtering those members by their own :view policies.
113
- #
114
- # This class is internal to the privacy plugin and should not be used directly.
115
- class InternalPolicyEvaluationVC < ViewerContext
116
- extend T::Sig
117
-
118
- sig { void }
119
- def initialize
120
- super()
121
- end
122
- end
123
-
124
108
  # Type alias for viewer contexts
125
109
  TViewerContext = T.type_alias { ViewerContext }
126
110
  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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Austin Bales
@@ -116,7 +116,6 @@ files:
116
116
  - lib/sequel/privacy/policy_dsl.rb
117
117
  - lib/sequel/privacy/version.rb
118
118
  - lib/sequel/privacy/viewer_context.rb
119
- - rbi/sequel_privacy.rbi
120
119
  homepage: https://github.com/arbales/sequel-privacy
121
120
  licenses:
122
121
  - MIT
@@ -1,66 +0,0 @@
1
- # typed: true
2
-
3
- module Sequel
4
- module Privacy
5
- # Actions is a Struct instance used as the binding context for policy
6
- # evaluation via instance_exec. Defined in actions.rb (typed: ignore).
7
- class Actions
8
- extend T::Sig
9
-
10
- sig { returns(Symbol) }
11
- def allow; end
12
-
13
- sig { returns(Symbol) }
14
- def deny; end
15
-
16
- sig { returns(Symbol) }
17
- def pass; end
18
-
19
- sig { params(policies: T.untyped).returns(T::Array[T.untyped]) }
20
- def all(*policies); end
21
-
22
- sig {
23
- params(
24
- args: T.untyped,
25
- block: Policy
26
- ).returns(T.untyped)
27
- }
28
- def self.instance_exec(*args, &block); end
29
- end
30
-
31
- class PrivacyDSL
32
- extend T::Sig
33
-
34
- sig { params(action: Symbol, policies: T.untyped).void }
35
- def can(action, *policies); end
36
-
37
- sig { params(field_name: Symbol, policies: T.untyped).void }
38
- def field(field_name, *policies); end
39
-
40
- sig { params(association_name: Symbol, blk: T.proc.void).void }
41
- def association(association_name, &blk); end
42
- end
43
- end
44
-
45
- module Plugins
46
- module Privacy
47
- module ClassMethods
48
- # The privacy block is evaluated in the context of PrivacyDSL
49
- sig { params(blk: T.proc.bind(Sequel::Privacy::PrivacyDSL).void).void }
50
- def privacy(&blk); end
51
- end
52
-
53
- module InstanceMethods
54
- # Declare the @viewer_context instance variable for the mixin
55
- sig { returns(T.nilable(Sequel::Privacy::ViewerContext)) }
56
- attr_accessor :viewer_context
57
- end
58
-
59
- module DatasetMethods
60
- # model is inherited from Sequel::Dataset but not visible to Sorbet
61
- sig { returns(T.class_of(Sequel::Model)) }
62
- def model; end
63
- end
64
- end
65
- end
66
- end