sequel-privacy 0.4 → 0.5.1
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 +113 -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: af7fc02c1fb57d47ba358babdebb3289c2d8eb6ec43d2fce25a2c3477a3f2ccc
|
|
4
|
+
data.tar.gz: a18b09c73b5b03540d2a74f985d32c99b6b2a090a714346f2c87ef5a2997252c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f46a3f0f253061be75dcd2fb80cf797c1be229d5fd83b1c9f1c86860621dae4536ea60b06df4e38d2d6b1e66f0e90533f0d3a75dddc48be388898ac1d8517d4
|
|
7
|
+
data.tar.gz: f4aee413ab143a08366b223e2c34d62165c69f1e63871eee400d6f82af3c096266e0646f3b4d3d4649a761c913e2c07cef8f852586f0c535d0af3daf1d02f9a6
|
|
@@ -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
|
|
|
@@ -417,8 +405,10 @@ module Sequel
|
|
|
417
405
|
case type
|
|
418
406
|
when :many_to_one, :one_to_one
|
|
419
407
|
_override_singular_association(name)
|
|
408
|
+
_override_association_dataset(name)
|
|
420
409
|
when :one_to_many, :many_to_many
|
|
421
410
|
_override_plural_association(name)
|
|
411
|
+
_override_association_dataset(name)
|
|
422
412
|
# Check if there are already privacy policies defined for this association
|
|
423
413
|
setup_association_privacy(name) if privacy_association_policies[name]
|
|
424
414
|
end
|
|
@@ -426,8 +416,51 @@ module Sequel
|
|
|
426
416
|
result
|
|
427
417
|
end
|
|
428
418
|
|
|
419
|
+
# Wrap the association's eager-load dataset with for_vc() so the
|
|
420
|
+
# child rows are materialized with the current viewer context.
|
|
421
|
+
# The VC is propagated via a thread-local set by DatasetMethods#all
|
|
422
|
+
# so that it's only applied during eager loading, not during the
|
|
423
|
+
# lazy association reader path (which has its own handling).
|
|
424
|
+
sig { params(opts: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
425
|
+
def _inject_privacy_eager_block(opts)
|
|
426
|
+
original = opts[:eager_block]
|
|
427
|
+
wrapped = proc do |ds|
|
|
428
|
+
ds = original.call(ds) if original
|
|
429
|
+
vc = Thread.current[DatasetMethods::EAGER_VC_KEY]
|
|
430
|
+
if vc && T.unsafe(ds).model.respond_to?(:privacy_vc_key)
|
|
431
|
+
T.unsafe(ds).for_vc(vc)
|
|
432
|
+
else
|
|
433
|
+
ds
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
opts.merge(eager_block: wrapped)
|
|
437
|
+
end
|
|
438
|
+
|
|
429
439
|
private
|
|
430
440
|
|
|
441
|
+
sig { params(name: Symbol).void }
|
|
442
|
+
def _override_association_dataset(name)
|
|
443
|
+
dataset_method = :"#{name}_dataset"
|
|
444
|
+
return unless method_defined?(dataset_method)
|
|
445
|
+
|
|
446
|
+
original = instance_method(dataset_method)
|
|
447
|
+
assoc_reflection = association_reflection(name)
|
|
448
|
+
assoc_class = T.let(nil, T.nilable(T.class_of(Sequel::Model)))
|
|
449
|
+
|
|
450
|
+
define_method(dataset_method) do |*args|
|
|
451
|
+
ds = original.bind(self).(*args)
|
|
452
|
+
vc = instance_variable_get(:@viewer_context)
|
|
453
|
+
return ds unless vc
|
|
454
|
+
|
|
455
|
+
assoc_class ||= assoc_reflection.associated_class
|
|
456
|
+
if assoc_class.respond_to?(:privacy_vc_key) && ds.respond_to?(:for_vc)
|
|
457
|
+
T.unsafe(ds).for_vc(vc)
|
|
458
|
+
else
|
|
459
|
+
ds
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
431
464
|
sig { params(name: Symbol).void }
|
|
432
465
|
def _override_singular_association(name)
|
|
433
466
|
original = instance_method(name)
|
|
@@ -732,14 +765,27 @@ module Sequel
|
|
|
732
765
|
has_attached_class!(:out)
|
|
733
766
|
requires_ancestor { Sequel::Dataset }
|
|
734
767
|
|
|
768
|
+
# Thread-local key for propagating the current VC to eager-load
|
|
769
|
+
# datasets via the :eager_block injected in ClassMethods#associate.
|
|
770
|
+
EAGER_VC_KEY = :sequel_privacy_eager_vc
|
|
771
|
+
|
|
735
772
|
# Attach viewer context to dataset for privacy enforcement on materialization
|
|
736
773
|
sig { params(vc: Sequel::Privacy::ViewerContext).returns(Sequel::Dataset) }
|
|
737
774
|
def for_vc(vc)
|
|
738
775
|
clone(viewer_context: vc)
|
|
739
776
|
end
|
|
740
777
|
|
|
741
|
-
# Override row_proc to wrap Model.call with
|
|
742
|
-
#
|
|
778
|
+
# Override row_proc to wrap Model.call with the full per-row
|
|
779
|
+
# privacy pipeline: set the thread-local VC so Model.call's
|
|
780
|
+
# strict-mode gate passes, attach the VC to the instance, and
|
|
781
|
+
# apply the :view filter — with two bypasses for materialization
|
|
782
|
+
# contexts where filtering would be wrong or break callers:
|
|
783
|
+
# - in_policy_eval?: policies that traverse protected data
|
|
784
|
+
# need raw rows so their checks (e.g. membership) aren't
|
|
785
|
+
# short-circuited by recursive :view filtering.
|
|
786
|
+
# - EAGER_VC_KEY: Sequel's eager-load attachment block
|
|
787
|
+
# dereferences each record to bucket by FK and would crash
|
|
788
|
+
# on nils; the association reader filters at read time.
|
|
743
789
|
sig { returns(T.untyped) }
|
|
744
790
|
def row_proc
|
|
745
791
|
vc = opts[:viewer_context]
|
|
@@ -751,10 +797,23 @@ module Sequel
|
|
|
751
797
|
old_vc = Thread.current[vc_key]
|
|
752
798
|
Thread.current[vc_key] = vc
|
|
753
799
|
begin
|
|
754
|
-
model_class.(values)
|
|
800
|
+
instance = model_class.(values)
|
|
755
801
|
ensure
|
|
756
802
|
Thread.current[vc_key] = old_vc
|
|
757
803
|
end
|
|
804
|
+
|
|
805
|
+
next nil if instance.nil?
|
|
806
|
+
|
|
807
|
+
instance.instance_variable_set(:@viewer_context, vc)
|
|
808
|
+
next instance if Sequel::Privacy::Enforcer.in_policy_eval?
|
|
809
|
+
next instance if Thread.current[EAGER_VC_KEY]
|
|
810
|
+
|
|
811
|
+
if T.cast(instance, InstanceMethods).allow?(vc, :view)
|
|
812
|
+
instance
|
|
813
|
+
else
|
|
814
|
+
Sequel::Privacy.logger&.debug { "Privacy denied :view on #{model_class}[#{instance.pk}]" }
|
|
815
|
+
nil
|
|
816
|
+
end
|
|
758
817
|
end
|
|
759
818
|
end
|
|
760
819
|
|
|
@@ -765,6 +824,34 @@ module Sequel
|
|
|
765
824
|
opts[:viewer_context] ? results.compact : results
|
|
766
825
|
end
|
|
767
826
|
|
|
827
|
+
# Sequel calls post_load after rows are fetched but before any
|
|
828
|
+
# user block. Model's override of post_load triggers eager_load
|
|
829
|
+
# here. Set the thread-local VC around that call so each
|
|
830
|
+
# association's injected :eager_block can wrap its child dataset
|
|
831
|
+
# with for_vc. Children are then materialized with VC attached
|
|
832
|
+
# but without :view filtering (see Model.call), so Sequel's
|
|
833
|
+
# attachment block doesn't choke on nils; the accessor wrapper
|
|
834
|
+
# filters at read time.
|
|
835
|
+
#
|
|
836
|
+
# Parents filtered to nil by the :view policy must be dropped
|
|
837
|
+
# before eager_load runs — its attachment code dereferences each
|
|
838
|
+
# record, which nil would break.
|
|
839
|
+
sig { params(all_records: T.untyped).returns(T.untyped) }
|
|
840
|
+
def post_load(all_records)
|
|
841
|
+
vc = opts[:viewer_context]
|
|
842
|
+
return super unless vc && opts[:eager]
|
|
843
|
+
|
|
844
|
+
all_records.compact!
|
|
845
|
+
|
|
846
|
+
old = Thread.current[EAGER_VC_KEY]
|
|
847
|
+
Thread.current[EAGER_VC_KEY] = vc
|
|
848
|
+
begin
|
|
849
|
+
super
|
|
850
|
+
ensure
|
|
851
|
+
Thread.current[EAGER_VC_KEY] = old
|
|
852
|
+
end
|
|
853
|
+
end
|
|
854
|
+
|
|
768
855
|
# Create a new model instance with the viewer context attached
|
|
769
856
|
sig { params(values: T::Hash[Symbol, T.untyped]).returns(T.attached_class) }
|
|
770
857
|
def new(values = {})
|