sequel-privacy 0.4 → 0.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: 651aa5392073590a594f91df25458c4e2594ae6de3d8157b53525bfe210ebeef
4
- data.tar.gz: 6addd80f151b6a1b272e5f70376ac41e18fd84e7a60cc77d68017d3c6b646dc6
3
+ metadata.gz: 8307cc7016667794361e143ba1a17735d2c189079af884a9928df3f67450fed1
4
+ data.tar.gz: f6ae84ecd2b0627deeb998d69cc7f38ec73313b5851b4e84b4367335ab6e7b06
5
5
  SHA512:
6
- metadata.gz: cb083193065e4d0929beb64dc3f51029e375861d42f5800da2218a3699b1adcd37c16aea5dc55f3cce0d250322967bed98f3c2e53119a693d8e7e9f0111bdab6
7
- data.tar.gz: 83840dfe5cded9036995df6371c083c9ab72e983f0c3de1eee69ff7a6b56b0b4e00c3ede0c6225e71669ce3bb14d981f08095c49153c5e75bacf6ee6896c072f
6
+ metadata.gz: ff39c00cfd1df723a53bca2a75a3ab6fd3657e1475f9f53c48f369de686c3c81facea4af80325b6acfa23e409e6e5fe71b5794ed9c31fad7300f19727f25e7e7
7
+ data.tar.gz: fa3124720cb8a285a1033fc3923d3df5f61d04685585394d94614d9bc7331ada3f8863335e91c0198229bfc7238e827b004de2cbed2c88a06fd762328753f8b2
@@ -191,12 +191,16 @@ module Sequel
191
191
  :"#{self}_privacy_vc"
192
192
  end
193
193
 
194
- # Override Sequel's call method - this is the lowest-level instantiation point
195
- # for ALL database-loaded records. Every path goes through here:
196
- # - Model[id], Model.first, Model.all, associations, etc.
194
+ # Override Sequel's call method to act as a strict-mode gate.
195
+ # Every database-loaded record flows through here (Model[id],
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.
197
202
  sig { params(values: T.untyped).returns(T.nilable(Sequel::Model)) }
198
203
  def call(values)
199
- # Check if we're in a VC context (thread-local set by for_vc)
200
204
  vc = Thread.current[privacy_vc_key]
201
205
 
202
206
  unless vc || allow_unsafe_access?
@@ -204,25 +208,7 @@ module Sequel
204
208
  "#{self} requires a ViewerContext. Use #{self}.for_vc(vc) or call #{self}.allow_unsafe_access!"
205
209
  end
206
210
 
207
- # Create the instance via parent chain
208
- instance = super
209
-
210
- # Attach VC if present
211
- if vc && instance
212
- instance.instance_variable_set(:@viewer_context, vc)
213
-
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
222
- end
223
- end
224
-
225
- instance
211
+ super
226
212
  end
227
213
 
228
214
  # ─────────────────────────────────────────────────────────────────────
@@ -410,6 +396,8 @@ module Sequel
410
396
  # Override Sequel's associate method to wrap associations with privacy checks
411
397
  sig { params(type: Symbol, name: Symbol, opts: T.untyped, block: T.untyped).returns(T.untyped) }
412
398
  def associate(type, name, opts = {}, &block)
399
+ opts = _inject_privacy_eager_block(opts)
400
+
413
401
  # Call original to create the association
414
402
  result = super
415
403
 
@@ -426,6 +414,26 @@ module Sequel
426
414
  result
427
415
  end
428
416
 
417
+ # Wrap the association's eager-load dataset with for_vc() so the
418
+ # child rows are materialized with the current viewer context.
419
+ # The VC is propagated via a thread-local set by DatasetMethods#all
420
+ # so that it's only applied during eager loading, not during the
421
+ # lazy association reader path (which has its own handling).
422
+ sig { params(opts: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
423
+ def _inject_privacy_eager_block(opts)
424
+ original = opts[:eager_block]
425
+ wrapped = proc do |ds|
426
+ ds = original.call(ds) if original
427
+ vc = Thread.current[DatasetMethods::EAGER_VC_KEY]
428
+ if vc && T.unsafe(ds).model.respond_to?(:privacy_vc_key)
429
+ T.unsafe(ds).for_vc(vc)
430
+ else
431
+ ds
432
+ end
433
+ end
434
+ opts.merge(eager_block: wrapped)
435
+ end
436
+
429
437
  private
430
438
 
431
439
  sig { params(name: Symbol).void }
@@ -732,14 +740,27 @@ module Sequel
732
740
  has_attached_class!(:out)
733
741
  requires_ancestor { Sequel::Dataset }
734
742
 
743
+ # Thread-local key for propagating the current VC to eager-load
744
+ # datasets via the :eager_block injected in ClassMethods#associate.
745
+ EAGER_VC_KEY = :sequel_privacy_eager_vc
746
+
735
747
  # Attach viewer context to dataset for privacy enforcement on materialization
736
748
  sig { params(vc: Sequel::Privacy::ViewerContext).returns(Sequel::Dataset) }
737
749
  def for_vc(vc)
738
750
  clone(viewer_context: vc)
739
751
  end
740
752
 
741
- # Override row_proc to wrap Model.call with thread-local VC.
742
- # This is the single integration point that covers all iteration methods.
753
+ # Override row_proc to wrap Model.call with the full per-row
754
+ # privacy pipeline: set the thread-local VC so Model.call's
755
+ # strict-mode gate passes, attach the VC to the instance, and
756
+ # apply the :view filter — with two bypasses for materialization
757
+ # contexts where filtering would be wrong or break callers:
758
+ # - in_policy_eval?: policies that traverse protected data
759
+ # need raw rows so their checks (e.g. membership) aren't
760
+ # short-circuited by recursive :view filtering.
761
+ # - EAGER_VC_KEY: Sequel's eager-load attachment block
762
+ # dereferences each record to bucket by FK and would crash
763
+ # on nils; the association reader filters at read time.
743
764
  sig { returns(T.untyped) }
744
765
  def row_proc
745
766
  vc = opts[:viewer_context]
@@ -751,10 +772,23 @@ module Sequel
751
772
  old_vc = Thread.current[vc_key]
752
773
  Thread.current[vc_key] = vc
753
774
  begin
754
- model_class.(values)
775
+ instance = model_class.(values)
755
776
  ensure
756
777
  Thread.current[vc_key] = old_vc
757
778
  end
779
+
780
+ next nil if instance.nil?
781
+
782
+ instance.instance_variable_set(:@viewer_context, vc)
783
+ next instance if Sequel::Privacy::Enforcer.in_policy_eval?
784
+ next instance if Thread.current[EAGER_VC_KEY]
785
+
786
+ if T.cast(instance, InstanceMethods).allow?(vc, :view)
787
+ instance
788
+ else
789
+ Sequel::Privacy.logger&.debug { "Privacy denied :view on #{model_class}[#{instance.pk}]" }
790
+ nil
791
+ end
758
792
  end
759
793
  end
760
794
 
@@ -765,6 +799,34 @@ module Sequel
765
799
  opts[:viewer_context] ? results.compact : results
766
800
  end
767
801
 
802
+ # Sequel calls post_load after rows are fetched but before any
803
+ # user block. Model's override of post_load triggers eager_load
804
+ # here. Set the thread-local VC around that call so each
805
+ # association's injected :eager_block can wrap its child dataset
806
+ # with for_vc. Children are then materialized with VC attached
807
+ # but without :view filtering (see Model.call), so Sequel's
808
+ # attachment block doesn't choke on nils; the accessor wrapper
809
+ # filters at read time.
810
+ #
811
+ # Parents filtered to nil by the :view policy must be dropped
812
+ # before eager_load runs — its attachment code dereferences each
813
+ # record, which nil would break.
814
+ sig { params(all_records: T.untyped).returns(T.untyped) }
815
+ def post_load(all_records)
816
+ vc = opts[:viewer_context]
817
+ return super unless vc && opts[:eager]
818
+
819
+ all_records.compact!
820
+
821
+ old = Thread.current[EAGER_VC_KEY]
822
+ Thread.current[EAGER_VC_KEY] = vc
823
+ begin
824
+ super
825
+ ensure
826
+ Thread.current[EAGER_VC_KEY] = old
827
+ end
828
+ end
829
+
768
830
  # Create a new model instance with the viewer context attached
769
831
  sig { params(values: T::Hash[Symbol, T.untyped]).returns(T.attached_class) }
770
832
  def new(values = {})
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Sequel
5
5
  module Privacy
6
- VERSION = '0.4'
6
+ VERSION = '0.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.4'
4
+ version: '0.5'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Austin Bales