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 +4 -4
- data/lib/sequel/plugins/privacy.rb +88 -26
- 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: 8307cc7016667794361e143ba1a17735d2c189079af884a9928df3f67450fed1
|
|
4
|
+
data.tar.gz: f6ae84ecd2b0627deeb998d69cc7f38ec73313b5851b4e84b4367335ab6e7b06
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
195
|
-
#
|
|
196
|
-
#
|
|
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
|
-
|
|
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
|
|
742
|
-
#
|
|
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 = {})
|