sequel-privacy 0.6 → 0.7.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 +4 -4
- data/CHANGELOG.md +6 -5
- data/README.md +18 -14
- data/lib/sequel/plugins/privacy.rb +43 -37
- data/lib/sequel/privacy/enforcer.rb +2 -0
- data/lib/sequel/privacy/errors.rb +3 -0
- data/lib/sequel/privacy/version.rb +1 -1
- data/lib/sequel/privacy/viewer_context.rb +52 -2
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7ef079bb3498aa1006aa327a138bcb1735b78ae02a65b11d2ee4976d9871978
|
|
4
|
+
data.tar.gz: 95000199db0cbb08908c1a261152c641b71ceb7898875a9f732936662df9334e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ea2e6410976a3e325264faa698a2faaf323c1aafdfe9eb3444c8cc18f51c887155227a1fe68ec119408d9679b1c4a39cc70e69611db4e6ed94edf9b93857ef2
|
|
7
|
+
data.tar.gz: 0271f5b12a6d376e04b5feae0eb9ee604da1258871092578878571c877b86b9ef388a60441962b31b36588c9c3e4fc9642c9b71cd0f90a8890030903e2a1bb21
|
data/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
3
|
|
|
8
4
|
## [Unreleased]
|
|
9
5
|
|
|
10
|
-
## [0.
|
|
6
|
+
## [0.7.0] – 2026-08-26
|
|
7
|
+
- Removed `#for_vc` on `Sequel::Model` instances. Instead, call `#reset_viewer_context(vc, reason)`.
|
|
8
|
+
- Removed `#viewer_context=` from `Sequel::Model` instances.
|
|
9
|
+
- Added `#use(&block)` to Omniscient and AllPowerful viewer contexts. The block receives the viewer context and the reason it was created.
|
|
10
|
+
|
|
11
|
+
## [0.1.0]
|
|
11
12
|
|
|
12
13
|
### Added
|
|
13
14
|
- Initial release
|
data/README.md
CHANGED
|
@@ -260,31 +260,35 @@ admin_vc = Sequel::Privacy::ViewerContext.all_powerful(:admin_migration)
|
|
|
260
260
|
|
|
261
261
|
### Login, Sessions & `current_user` and `current_vc`
|
|
262
262
|
|
|
263
|
-
Unless you allow unsafe access to your User (or equivalent) model, you will need
|
|
264
|
-
a way to load it and create a ViewerContext for them. An Omniscient ViewerContext
|
|
265
|
-
is useful for this.
|
|
266
|
-
or materialized a user from the session.
|
|
263
|
+
Unless you allow unsafe access to your User (or equivalent) model, you will need
|
|
264
|
+
a way to load it and create a ViewerContext for them. An Omniscient ViewerContext
|
|
265
|
+
is useful for this.
|
|
267
266
|
|
|
268
267
|
```ruby
|
|
269
|
-
|
|
270
|
-
# or however you store them. Discard this viewer context when you're done with it.
|
|
271
|
-
def current_user
|
|
268
|
+
def current_user
|
|
272
269
|
return @current_user if @current_user
|
|
273
|
-
login_vc = Sequel::Privacy::ViewerContext.omniscient(:login)
|
|
274
|
-
user = User.for_vc(login_vc)[session_user_id]
|
|
275
|
-
return nil unless user
|
|
276
270
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
271
|
+
@current_user = Sequel::Privacy::ViewerContext.omniscient(:login).use do |vc, reason|
|
|
272
|
+
user = User.for_vc(vc)[session_user_id]
|
|
273
|
+
next nil unless user
|
|
274
|
+
|
|
275
|
+
# Give the row its own ActorVC, so its fields and associations respect
|
|
276
|
+
# privacy from here on — and so it can be written to. An OmniscientVC reads
|
|
277
|
+
# anything but refuses to mutate.
|
|
278
|
+
user.reset_viewer_context(Sequel::Privacy::ViewerContext.for_actor(user), reason)
|
|
279
|
+
end
|
|
280
280
|
end
|
|
281
281
|
|
|
282
282
|
def current_vc
|
|
283
283
|
current_user&.viewer_context || Sequel::Privacy::ViewerContext.anonymous()
|
|
284
284
|
end
|
|
285
|
-
|
|
286
285
|
```
|
|
287
286
|
|
|
287
|
+
*Warning:* Skipping the reset leaves you holding an object that cannot be
|
|
288
|
+
saved, but any association or field reads on it will be allowed. So you should always
|
|
289
|
+
discard the OmniVC / APVCs used for things like this and call `reset_viewer_context`
|
|
290
|
+
with a more appropriate one as soon as you can.
|
|
291
|
+
|
|
288
292
|
## Mutation Enforcement
|
|
289
293
|
|
|
290
294
|
When a viewer context is attached, mutations are automatically checked:
|
|
@@ -371,13 +371,13 @@ module Sequel
|
|
|
371
371
|
|
|
372
372
|
sig { params(original: T.untyped).returns(Proc) }
|
|
373
373
|
def _privacy_eager_block(original)
|
|
374
|
-
wrapped = proc do |
|
|
375
|
-
|
|
374
|
+
wrapped = proc do |dataset|
|
|
375
|
+
dataset = original.call(dataset) if original
|
|
376
376
|
vc = Thread.current[DatasetMethods::EAGER_VC_KEY]
|
|
377
|
-
if vc && T.unsafe(
|
|
378
|
-
T.unsafe(
|
|
377
|
+
if vc && T.unsafe(dataset).model.respond_to?(:privacy_vc_key)
|
|
378
|
+
T.unsafe(dataset).for_vc(vc).clone(privacy_eager_load: true)
|
|
379
379
|
else
|
|
380
|
-
|
|
380
|
+
dataset
|
|
381
381
|
end
|
|
382
382
|
end
|
|
383
383
|
wrapped
|
|
@@ -385,6 +385,16 @@ module Sequel
|
|
|
385
385
|
|
|
386
386
|
private
|
|
387
387
|
|
|
388
|
+
sig { returns(T::Module[T.anything]) }
|
|
389
|
+
def privacy_association_wrapper
|
|
390
|
+
wrapper = @privacy_association_wrapper ||= T.let(
|
|
391
|
+
Module.new,
|
|
392
|
+
T.nilable(T::Module[T.anything])
|
|
393
|
+
)
|
|
394
|
+
prepend(wrapper) unless ancestors.include?(wrapper)
|
|
395
|
+
wrapper
|
|
396
|
+
end
|
|
397
|
+
|
|
388
398
|
sig { params(type: Symbol, name: Symbol, inject_eager_block: T::Boolean).void }
|
|
389
399
|
def _setup_privacy_association_readers(type, name, inject_eager_block)
|
|
390
400
|
reflection = association_reflection(name)
|
|
@@ -419,12 +429,11 @@ module Sequel
|
|
|
419
429
|
dataset_method = :"#{name}_dataset"
|
|
420
430
|
return unless method_defined?(dataset_method)
|
|
421
431
|
|
|
422
|
-
original = instance_method(dataset_method)
|
|
423
432
|
assoc_reflection = association_reflection(name)
|
|
424
433
|
assoc_class = T.let(nil, T.nilable(T.class_of(Sequel::Model)))
|
|
425
434
|
|
|
426
|
-
define_method(dataset_method) do |*args|
|
|
427
|
-
ds =
|
|
435
|
+
privacy_association_wrapper.define_method(dataset_method) do |*args, &block|
|
|
436
|
+
ds = super(*args, &block)
|
|
428
437
|
vc = instance_variable_get(:@viewer_context)
|
|
429
438
|
return ds unless vc
|
|
430
439
|
|
|
@@ -439,12 +448,11 @@ module Sequel
|
|
|
439
448
|
|
|
440
449
|
sig { params(name: Symbol).void }
|
|
441
450
|
def _override_singular_association(name)
|
|
442
|
-
original = instance_method(name)
|
|
443
451
|
assoc_reflection = association_reflection(name)
|
|
444
452
|
# Resolve lazily to handle forward references between models.
|
|
445
453
|
assoc_class = T.let(nil, T.nilable(T.class_of(Sequel::Model)))
|
|
446
454
|
|
|
447
|
-
define_method(name) do
|
|
455
|
+
privacy_association_wrapper.define_method(name) do |*args, &block|
|
|
448
456
|
vc = instance_variable_get(:@viewer_context)
|
|
449
457
|
|
|
450
458
|
if vc.nil? && !T.unsafe(self.class).allow_unsafe_access?(name)
|
|
@@ -459,12 +467,12 @@ module Sequel
|
|
|
459
467
|
old_vc = Thread.current[vc_key]
|
|
460
468
|
Thread.current[vc_key] = vc
|
|
461
469
|
begin
|
|
462
|
-
|
|
470
|
+
super(*args, &block)
|
|
463
471
|
ensure
|
|
464
472
|
Thread.current[vc_key] = old_vc
|
|
465
473
|
end
|
|
466
474
|
else
|
|
467
|
-
|
|
475
|
+
super(*args, &block)
|
|
468
476
|
end
|
|
469
477
|
|
|
470
478
|
return nil unless obj
|
|
@@ -484,11 +492,10 @@ module Sequel
|
|
|
484
492
|
|
|
485
493
|
sig { params(name: Symbol).void }
|
|
486
494
|
def _override_plural_association(name)
|
|
487
|
-
original = instance_method(name)
|
|
488
495
|
assoc_reflection = association_reflection(name)
|
|
489
496
|
assoc_class = T.let(nil, T.nilable(T.class_of(Sequel::Model)))
|
|
490
497
|
|
|
491
|
-
define_method(name) do
|
|
498
|
+
privacy_association_wrapper.define_method(name) do |*args, &block|
|
|
492
499
|
vc = instance_variable_get(:@viewer_context)
|
|
493
500
|
|
|
494
501
|
if vc.nil? && !T.unsafe(self.class).allow_unsafe_access?(name)
|
|
@@ -503,12 +510,12 @@ module Sequel
|
|
|
503
510
|
old_vc = Thread.current[vc_key]
|
|
504
511
|
Thread.current[vc_key] = vc
|
|
505
512
|
begin
|
|
506
|
-
|
|
513
|
+
super(*args, &block)
|
|
507
514
|
ensure
|
|
508
515
|
Thread.current[vc_key] = old_vc
|
|
509
516
|
end
|
|
510
517
|
else
|
|
511
|
-
|
|
518
|
+
super(*args, &block)
|
|
512
519
|
end
|
|
513
520
|
|
|
514
521
|
return objs unless vc
|
|
@@ -530,13 +537,13 @@ module Sequel
|
|
|
530
537
|
sig { params(assoc_name: Symbol, singular_name: Symbol, policies: T::Array[T.untyped]).void }
|
|
531
538
|
def _wrap_association_add(assoc_name, singular_name, policies)
|
|
532
539
|
method_name = :"add_#{singular_name}"
|
|
533
|
-
original = instance_method(method_name)
|
|
534
540
|
|
|
535
|
-
define_method(method_name) do |
|
|
541
|
+
privacy_association_wrapper.define_method(method_name) do |*args, &block|
|
|
542
|
+
obj = args.first
|
|
536
543
|
vc = instance_variable_get(:@viewer_context)
|
|
537
544
|
|
|
538
545
|
unless vc
|
|
539
|
-
return
|
|
546
|
+
return super(*args, &block) if T.unsafe(self.class).allow_unsafe_access?(assoc_name)
|
|
540
547
|
|
|
541
548
|
Kernel.raise Sequel::Privacy::MissingViewerContext,
|
|
542
549
|
"Cannot #{method_name} without a viewer context"
|
|
@@ -554,20 +561,20 @@ module Sequel
|
|
|
554
561
|
"Cannot #{method_name} on #{self.class}"
|
|
555
562
|
end
|
|
556
563
|
|
|
557
|
-
|
|
564
|
+
super(*args, &block)
|
|
558
565
|
end
|
|
559
566
|
end
|
|
560
567
|
|
|
561
568
|
sig { params(assoc_name: Symbol, singular_name: Symbol, policies: T::Array[T.untyped]).void }
|
|
562
569
|
def _wrap_association_remove(assoc_name, singular_name, policies)
|
|
563
570
|
method_name = :"remove_#{singular_name}"
|
|
564
|
-
original = instance_method(method_name)
|
|
565
571
|
|
|
566
|
-
define_method(method_name) do |
|
|
572
|
+
privacy_association_wrapper.define_method(method_name) do |*args, &block|
|
|
573
|
+
obj = args.first
|
|
567
574
|
vc = instance_variable_get(:@viewer_context)
|
|
568
575
|
|
|
569
576
|
unless vc
|
|
570
|
-
return
|
|
577
|
+
return super(*args, &block) if T.unsafe(self.class).allow_unsafe_access?(assoc_name)
|
|
571
578
|
|
|
572
579
|
Kernel.raise Sequel::Privacy::MissingViewerContext,
|
|
573
580
|
"Cannot #{method_name} without a viewer context"
|
|
@@ -585,20 +592,19 @@ module Sequel
|
|
|
585
592
|
"Cannot #{method_name} on #{self.class}"
|
|
586
593
|
end
|
|
587
594
|
|
|
588
|
-
|
|
595
|
+
super(*args, &block)
|
|
589
596
|
end
|
|
590
597
|
end
|
|
591
598
|
|
|
592
599
|
sig { params(assoc_name: Symbol, plural_name: Symbol, policies: T::Array[T.untyped]).void }
|
|
593
600
|
def _wrap_association_remove_all(assoc_name, plural_name, policies)
|
|
594
601
|
method_name = :"remove_all_#{plural_name}"
|
|
595
|
-
original = instance_method(method_name)
|
|
596
602
|
|
|
597
|
-
define_method(method_name) do
|
|
603
|
+
privacy_association_wrapper.define_method(method_name) do |*args, &block|
|
|
598
604
|
vc = instance_variable_get(:@viewer_context)
|
|
599
605
|
|
|
600
606
|
unless vc
|
|
601
|
-
return
|
|
607
|
+
return super(*args, &block) if T.unsafe(self.class).allow_unsafe_access?(assoc_name)
|
|
602
608
|
|
|
603
609
|
Kernel.raise Sequel::Privacy::MissingViewerContext,
|
|
604
610
|
"Cannot #{method_name} without a viewer context"
|
|
@@ -616,7 +622,7 @@ module Sequel
|
|
|
616
622
|
"Cannot #{method_name} on #{self.class}"
|
|
617
623
|
end
|
|
618
624
|
|
|
619
|
-
|
|
625
|
+
super(*args, &block)
|
|
620
626
|
end
|
|
621
627
|
end
|
|
622
628
|
end
|
|
@@ -633,13 +639,12 @@ module Sequel
|
|
|
633
639
|
@viewer_context = T.let(@viewer_context, T.nilable(Sequel::Privacy::ViewerContext))
|
|
634
640
|
end
|
|
635
641
|
|
|
636
|
-
sig { params(vc: T.nilable(Sequel::Privacy::ViewerContext)).returns(T.nilable(Sequel::Privacy::ViewerContext)) }
|
|
637
|
-
def viewer_context=(vc)
|
|
638
|
-
@viewer_context = T.let(vc, T.nilable(Sequel::Privacy::ViewerContext))
|
|
639
|
-
end
|
|
640
642
|
|
|
641
|
-
sig { params(vc: Sequel::Privacy::ViewerContext).returns(T.self_type) }
|
|
642
|
-
def
|
|
643
|
+
sig { params(vc: Sequel::Privacy::ViewerContext, reason: Symbol).returns(T.self_type) }
|
|
644
|
+
def reset_viewer_context(vc, reason)
|
|
645
|
+
Sequel::Privacy.logger&.debug do
|
|
646
|
+
"Resetting viewer context on #{self.class}[#{pk}] to #{vc.class.name.to_s.split('::').last} (#{reason})"
|
|
647
|
+
end
|
|
643
648
|
@viewer_context = T.let(vc, T.nilable(Sequel::Privacy::ViewerContext))
|
|
644
649
|
self
|
|
645
650
|
end
|
|
@@ -747,8 +752,9 @@ module Sequel
|
|
|
747
752
|
# can retreive. Materializes the model, and then checks the view
|
|
748
753
|
# policy. If the model is being materialized within the context of
|
|
749
754
|
# checking a policy this is bypassed, because policies often need to
|
|
750
|
-
# check data that a VC might not have permission to see.
|
|
751
|
-
#
|
|
755
|
+
# check data that a VC might not have permission to see. For eager
|
|
756
|
+
# association loads, enforcement is deferred until association access so
|
|
757
|
+
# Sequel can finish attachment and populate reciprocal caches first.
|
|
752
758
|
sig { returns(T.untyped) }
|
|
753
759
|
def row_proc
|
|
754
760
|
vc = opts[:viewer_context]
|
|
@@ -768,8 +774,8 @@ module Sequel
|
|
|
768
774
|
next nil if instance.nil?
|
|
769
775
|
|
|
770
776
|
instance.instance_variable_set(:@viewer_context, vc)
|
|
777
|
+
next instance if opts[:privacy_eager_load]
|
|
771
778
|
next instance if Sequel::Privacy::Enforcer.in_policy_eval?
|
|
772
|
-
next instance if Thread.current[EAGER_VC_KEY]
|
|
773
779
|
|
|
774
780
|
if T.cast(instance, InstanceMethods).allow?(vc, :view)
|
|
775
781
|
instance
|
|
@@ -15,6 +15,9 @@ module Sequel
|
|
|
15
15
|
# Raised when an invalid viewer context is used
|
|
16
16
|
class InvalidViewerContextError < StandardError; end
|
|
17
17
|
|
|
18
|
+
# Raised if #invalidates was called on this VC.
|
|
19
|
+
class InvalidatedViewerContext < InvalidViewerContextError; end
|
|
20
|
+
|
|
18
21
|
class MissingViewerContext < StandardError; end
|
|
19
22
|
|
|
20
23
|
# Raised when attempting to modify privacy settings after finalization
|
|
@@ -41,6 +41,54 @@ module Sequel
|
|
|
41
41
|
def self.anonymous
|
|
42
42
|
AnonymousVC.new
|
|
43
43
|
end
|
|
44
|
+
|
|
45
|
+
sig { returns(T::Boolean) }
|
|
46
|
+
def invalidated?
|
|
47
|
+
@invalidated = T.let(@invalidated, T.nilable(T::Boolean))
|
|
48
|
+
@invalidated || false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sig { void }
|
|
52
|
+
def assert_usable!
|
|
53
|
+
return unless invalidated?
|
|
54
|
+
|
|
55
|
+
Kernel.raise InvalidatedViewerContext,
|
|
56
|
+
"#{self.class.name.to_s.split('::').last} was invalidated and cannot be used again"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Made available on OmniscientVC and AllPowerfulVCs. Transient contexts are
|
|
61
|
+
# invalidated when the use block exists. To help you keep these around for
|
|
62
|
+
# as short a time as possible.
|
|
63
|
+
module TransientViewerContext
|
|
64
|
+
extend T::Sig
|
|
65
|
+
extend T::Helpers
|
|
66
|
+
|
|
67
|
+
abstract!
|
|
68
|
+
requires_ancestor { ViewerContext }
|
|
69
|
+
|
|
70
|
+
sig { abstract.returns(Symbol) }
|
|
71
|
+
def reason; end
|
|
72
|
+
|
|
73
|
+
sig do
|
|
74
|
+
type_parameters(:U)
|
|
75
|
+
.params(block: T.proc.params(vc: T.untyped, reason: Symbol).returns(T.type_parameter(:U)))
|
|
76
|
+
.returns(T.type_parameter(:U))
|
|
77
|
+
end
|
|
78
|
+
def use(&block)
|
|
79
|
+
block.call(self, reason)
|
|
80
|
+
ensure
|
|
81
|
+
invalidate!
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sig { returns(T.self_type) }
|
|
85
|
+
def invalidate!
|
|
86
|
+
unless invalidated?
|
|
87
|
+
Sequel::Privacy.logger&.debug("Invalidating viewer context: #{reason}")
|
|
88
|
+
@invalidated = T.let(true, T.nilable(T::Boolean))
|
|
89
|
+
end
|
|
90
|
+
self
|
|
91
|
+
end
|
|
44
92
|
end
|
|
45
93
|
|
|
46
94
|
# Standard viewer context with an actor (user/member)
|
|
@@ -65,6 +113,7 @@ module Sequel
|
|
|
65
113
|
# Requires a reason for audit logging.
|
|
66
114
|
class AllPowerfulVC < ViewerContext
|
|
67
115
|
extend T::Sig
|
|
116
|
+
include TransientViewerContext
|
|
68
117
|
|
|
69
118
|
sig { params(reason: Symbol).void }
|
|
70
119
|
def initialize(reason)
|
|
@@ -72,7 +121,7 @@ module Sequel
|
|
|
72
121
|
super()
|
|
73
122
|
end
|
|
74
123
|
|
|
75
|
-
sig { returns(Symbol) }
|
|
124
|
+
sig { override.returns(Symbol) }
|
|
76
125
|
attr_reader :reason
|
|
77
126
|
end
|
|
78
127
|
|
|
@@ -80,6 +129,7 @@ module Sequel
|
|
|
80
129
|
# Used for system operations like authentication lookups.
|
|
81
130
|
class OmniscientVC < ViewerContext
|
|
82
131
|
extend T::Sig
|
|
132
|
+
include TransientViewerContext
|
|
83
133
|
|
|
84
134
|
sig { params(reason: Symbol).void }
|
|
85
135
|
def initialize(reason)
|
|
@@ -87,7 +137,7 @@ module Sequel
|
|
|
87
137
|
super()
|
|
88
138
|
end
|
|
89
139
|
|
|
90
|
-
sig { returns(Symbol) }
|
|
140
|
+
sig { override.returns(Symbol) }
|
|
91
141
|
attr_reader :reason
|
|
92
142
|
end
|
|
93
143
|
|
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:
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Austin Bales
|
|
@@ -93,6 +93,20 @@ dependencies:
|
|
|
93
93
|
- - "~>"
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '0.17'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rubocop
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
96
110
|
description: A Sequel plugin that provides declarative privacy policies and automatic
|
|
97
111
|
enforcement at field access and query boundaries.
|
|
98
112
|
email:
|
|
@@ -138,7 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
138
152
|
- !ruby/object:Gem::Version
|
|
139
153
|
version: '0'
|
|
140
154
|
requirements: []
|
|
141
|
-
rubygems_version: 4.0.
|
|
155
|
+
rubygems_version: 4.0.14
|
|
142
156
|
specification_version: 4
|
|
143
157
|
summary: Privacy enforcement plugin for Sequel models
|
|
144
158
|
test_files: []
|