sequel-privacy 0.1.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.
@@ -0,0 +1,40 @@
1
+ # typed: ignore
2
+ # frozen_string_literal: true
3
+
4
+ module Sequel
5
+ module Privacy
6
+ # Actions provides the DSL methods available inside policy lambdas.
7
+ # When policies are evaluated, they execute in the context of this struct,
8
+ # giving them access to allow, deny, pass, and all methods.
9
+ #
10
+ # Example:
11
+ # policy :AllowAdmins, ->(actor) {
12
+ # allow if actor.is_role?(:admin)
13
+ # }
14
+ Actions = (Struct.new do
15
+ extend T::Sig
16
+
17
+ sig { returns(Symbol) }
18
+ def allow
19
+ :allow
20
+ end
21
+
22
+ sig { returns(Symbol) }
23
+ def deny
24
+ :deny
25
+ end
26
+
27
+ sig { returns(Symbol) }
28
+ def pass
29
+ :pass
30
+ end
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
39
+ end
40
+ end
@@ -0,0 +1,37 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Sequel
5
+ module Privacy
6
+ # Built-in policies that ship with the gem.
7
+ # Applications should define their own policies using PolicyDSL.
8
+ module BuiltInPolicies
9
+ # Always deny access. Should be the last policy in every chain (fail-secure).
10
+ AlwaysDeny = Policy.create(
11
+ :AlwaysDeny,
12
+ -> { :deny },
13
+ 'In the absence of other rules, this content is hidden.',
14
+ cacheable: true
15
+ )
16
+
17
+ # Always allow access. Use sparingly.
18
+ AlwaysAllow = Policy.create(
19
+ :AlwaysAllow,
20
+ -> { :allow },
21
+ 'Always allow access.',
22
+ cacheable: true
23
+ )
24
+
25
+ # Pass and log - useful for debugging policy chains.
26
+ PassAndLog = Policy.create(
27
+ :PassAndLog,
28
+ ->(subject, actor) {
29
+ Sequel::Privacy::Enforcer.logger&.info("PassAndLog: #{subject.class} for actor #{actor.id}")
30
+ :pass
31
+ },
32
+ 'Log and pass to next policy.',
33
+ cacheable: false
34
+ )
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,33 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Sequel
5
+ module Privacy
6
+ # In-memory cache for policy evaluation results.
7
+ # Should be cleared between requests (e.g., via Rack middleware).
8
+ class << self
9
+ extend T::Sig
10
+
11
+ # Returns the in-memory cache Hash for policy results.
12
+ sig { returns(T::Hash[Integer, Symbol]) }
13
+ def cache
14
+ @cache ||= T.let({}, T.nilable(T::Hash[Integer, Symbol]))
15
+ end
16
+
17
+ # Returns the hash tracking single-match optimizations.
18
+ # Key: [policy, actor, viewer_context].hash
19
+ # Value: subject.hash that matched
20
+ sig { returns(T::Hash[Integer, Integer]) }
21
+ def single_matches
22
+ @single_matches ||= T.let({}, T.nilable(T::Hash[Integer, Integer]))
23
+ end
24
+
25
+ # Clear all caches. Call this between requests.
26
+ sig { void }
27
+ def clear_cache!
28
+ @cache = {}
29
+ @single_matches = {}
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,246 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Sequel
5
+ module Privacy
6
+ # The Enforcer evaluates policy chains to determine if an action is allowed.
7
+ # It handles caching, single-match optimization, and policy combinators.
8
+ module Enforcer
9
+ extend T::Sig
10
+
11
+ class << self
12
+ extend T::Sig
13
+
14
+ # Returns the centralized logger from Sequel::Privacy.logger
15
+ sig { returns(T.untyped) }
16
+ def logger
17
+ Sequel::Privacy.logger
18
+ end
19
+ end
20
+
21
+ # Main entry point for policy evaluation.
22
+ #
23
+ # @param policies [Array<Policy, Proc>] The policy chain to evaluate
24
+ # @param subject [Sequel::Model] The object being accessed
25
+ # @param viewer_context [ViewerContext] Who is accessing the object
26
+ # @param direct_object [Sequel::Model, nil] Optional additional context object
27
+ # @return [Boolean] true if access is allowed, false otherwise
28
+ sig do
29
+ params(
30
+ policies: TPolicyArray,
31
+ subject: TPolicySubject,
32
+ viewer_context: TViewerContext,
33
+ direct_object: T.nilable(Sequel::Model)
34
+ ).returns(T::Boolean)
35
+ end
36
+ 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
42
+
43
+ if viewer_context.is_a?(OmniscientVC)
44
+ logger&.debug { "BYPASS: Omniscient viewer context (#{viewer_context.reason})" }
45
+ return true
46
+ end
47
+
48
+ actor = viewer_context.is_a?(ActorVC) ? viewer_context.actor : nil
49
+
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
55
+
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
61
+
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
68
+
69
+ false
70
+ end
71
+
72
+ # Compute cache key based on policy arity
73
+ sig do
74
+ params(
75
+ policy: Policy,
76
+ subject: TPolicySubject,
77
+ actor: T.nilable(IActor),
78
+ viewer_context: ViewerContext,
79
+ direct_object: T.nilable(Sequel::Model)
80
+ ).returns(Integer)
81
+ end
82
+ def self.compute_cache_key(policy, subject, actor, viewer_context, direct_object)
83
+ case policy.arity
84
+ when 0
85
+ [policy, viewer_context].hash
86
+ when 1
87
+ [policy, subject, viewer_context].hash
88
+ when 2
89
+ [policy, subject, actor, viewer_context].hash
90
+ else
91
+ [policy, subject, actor, direct_object, viewer_context].hash
92
+ end
93
+ end
94
+
95
+ sig { params(outcome: Symbol).returns(T::Boolean) }
96
+ def self.valid_outcome?(outcome)
97
+ %i[allow pass deny].include?(outcome)
98
+ end
99
+
100
+ # Evaluate a combinator (array of policies returned by `all()`)
101
+ # All must allow for the result to be :allow, any :deny results in :deny
102
+ sig do
103
+ params(
104
+ child_policies: TPolicyArray,
105
+ subject: TPolicySubject,
106
+ actor: T.nilable(IActor),
107
+ viewer_context: ViewerContext,
108
+ direct_object: T.nilable(Sequel::Model)
109
+ ).returns(Symbol)
110
+ end
111
+ def self.evaluate_child_policies(child_policies, subject, actor, viewer_context, direct_object)
112
+ unless child_policies.all? { |c| c.is_a?(Proc) }
113
+ Kernel.raise "Policy combinator contains non-policy members"
114
+ end
115
+
116
+ results = child_policies.map do |child_policy|
117
+ policy_result(child_policy, subject, actor, viewer_context, direct_object)
118
+ end
119
+
120
+ return :deny if results.include?(:deny)
121
+ return :allow if results.all? { |r| r == :allow }
122
+
123
+ :pass
124
+ end
125
+
126
+ # Evaluate a single policy and return its result
127
+ sig do
128
+ params(
129
+ uncasted_policy: T.any(TPolicy, Proc),
130
+ subject: TPolicySubject,
131
+ actor: T.nilable(IActor),
132
+ viewer_context: ViewerContext,
133
+ direct_object: T.nilable(Sequel::Model)
134
+ ).returns(Symbol)
135
+ end
136
+ def self.policy_result(uncasted_policy, subject, actor, viewer_context, direct_object)
137
+ from_cache = false
138
+ skipped_from_single_match = false
139
+
140
+ policy = T.cast(uncasted_policy, TPolicy, checked: false)
141
+
142
+ # Single-match optimization
143
+ if policy.single_match?
144
+ match_key = [policy, actor, viewer_context].hash
145
+ if (matched = Sequel::Privacy.single_matches[match_key]) && matched != subject.hash
146
+ skipped_from_single_match = true
147
+ result = :pass
148
+ end
149
+ end
150
+
151
+ # Check cache
152
+ cache_key = compute_cache_key(policy, subject, actor, viewer_context, direct_object)
153
+ if !skipped_from_single_match && policy.cacheable? && Sequel::Privacy.cache.key?(cache_key)
154
+ from_cache = true
155
+ result = Sequel::Privacy.cache[cache_key]
156
+ Kernel.raise InvalidPolicyOutcomeError unless result && valid_outcome?(result)
157
+ end
158
+
159
+ # Execute policy if not cached
160
+ result ||= execute_policy(policy, subject, actor, direct_object)
161
+ result ||= :pass
162
+
163
+ # Handle combinator results
164
+ if result.is_a?(Array)
165
+ result = evaluate_child_policies(result, subject, actor, viewer_context, direct_object)
166
+ end
167
+
168
+ # Cache result
169
+ if policy.cacheable? && !from_cache
170
+ Sequel::Privacy.cache[cache_key] = result
171
+ end
172
+
173
+ # Log result
174
+ log_result(policy, result, actor, subject, from_cache, skipped_from_single_match)
175
+
176
+ # Record single-match
177
+ if policy.single_match? && result == :allow
178
+ Sequel::Privacy.single_matches[[policy, actor, viewer_context].hash] = subject.hash
179
+ end
180
+
181
+ unless valid_outcome?(result)
182
+ Kernel.raise InvalidPolicyOutcomeError, "Policy returned #{result.inspect}, expected :allow, :deny, or :pass"
183
+ end
184
+
185
+ result
186
+ end
187
+
188
+ sig do
189
+ params(
190
+ policy: Policy,
191
+ subject: TPolicySubject,
192
+ actor: T.nilable(IActor),
193
+ direct_object: T.nilable(Sequel::Model)
194
+ ).returns(T.untyped)
195
+ end
196
+ def self.execute_policy(policy, subject, actor, direct_object)
197
+ # 2+ arity policies require actor - auto-deny for anonymous
198
+ if !actor && policy.arity >= 2
199
+ return :deny
200
+ end
201
+
202
+ case policy.arity
203
+ when 0
204
+ Actions.instance_exec(&policy)
205
+ when 1
206
+ Actions.instance_exec(subject, &policy)
207
+ when 2
208
+ Actions.instance_exec(subject, T.must(actor), &policy)
209
+ else
210
+ Actions.instance_exec(subject, T.must(actor), direct_object, &policy)
211
+ end
212
+ end
213
+
214
+ sig do
215
+ params(
216
+ policy: Policy,
217
+ result: Symbol,
218
+ actor: T.nilable(IActor),
219
+ subject: TPolicySubject,
220
+ from_cache: T::Boolean,
221
+ skipped: T::Boolean
222
+ ).void
223
+ end
224
+ def self.log_result(policy, result, actor, subject, from_cache, skipped)
225
+ return unless logger
226
+
227
+ actor_id = actor ? actor.id : 'anonymous'
228
+ logger.debug do
229
+ msg = "#{result.to_s.upcase}: #{policy.policy_name || 'anonymous'} for actor[#{actor_id}] on #{subject.class}[#{subject_id(subject)}]"
230
+ msg += " (cached)" if from_cache
231
+ msg += " (skipped: single_match)" if skipped
232
+ msg
233
+ end
234
+
235
+ if policy.comment && %i[deny allow].include?(result)
236
+ logger.debug { " ⮑ #{policy.comment}" }
237
+ end
238
+ end
239
+
240
+ sig { params(subject: TPolicySubject).returns(T.untyped) }
241
+ def self.subject_id(subject)
242
+ subject.respond_to?(:pk) ? subject.pk : subject.object_id
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,23 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Sequel
5
+ module Privacy
6
+ # Raised when a viewer is not authorized to perform an action (view, edit, create)
7
+ class Unauthorized < StandardError; end
8
+
9
+ # Raised when a viewer is not authorized to access or modify a specific field
10
+ class FieldUnauthorized < StandardError; end
11
+
12
+ # Raised when a policy returns an invalid outcome
13
+ class InvalidPolicyOutcomeError < StandardError; end
14
+
15
+ # Raised when an invalid viewer context is used
16
+ class InvalidViewerContextError < StandardError; end
17
+
18
+ class MissingViewerContext < StandardError; end
19
+
20
+ # Raised when attempting to modify privacy settings after finalization
21
+ class PrivacyAlreadyFinalizedError < StandardError; end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Sequel
5
+ module Privacy
6
+ # Interface that actors (typically User/Member models) must implement
7
+ # to be used with the privacy system.
8
+ module IActor
9
+ extend T::Sig
10
+ extend T::Helpers
11
+ interface!
12
+
13
+ sig { abstract.returns(Integer) }
14
+ def id; end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,82 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Sequel
5
+ module Privacy
6
+ # A Policy wraps a Proc/lambda with metadata about how it should be evaluated.
7
+ #
8
+ # Policies take 0-3 arguments depending on what context they need:
9
+ # - 0 args: -> { allow } # Global decision
10
+ # - 1 arg: ->(actor) { allow if actor.is_role?(:admin) }
11
+ # - 2 args: ->(subject, actor) { allow if subject.owner_id == actor.id }
12
+ # - 3 args: ->(subject, actor, direct_object) { ... }
13
+ #
14
+ # Policies must return :allow, :deny, :pass, or an array of policies (for combinators).
15
+ class Policy < Proc
16
+ extend T::Sig
17
+
18
+ sig { returns(T.nilable(String)) }
19
+ attr_reader :policy_name
20
+
21
+ sig { returns(T.nilable(String)) }
22
+ attr_reader :comment
23
+
24
+ # Factory method for creating policies
25
+ sig do
26
+ params(
27
+ policy_name: Symbol,
28
+ lam: T.proc.returns(Symbol),
29
+ comment: T.nilable(String),
30
+ cacheable: T::Boolean,
31
+ single_match: T::Boolean
32
+ ).returns(T.self_type)
33
+ end
34
+ def self.create(policy_name, lam, comment = nil, cacheable: true, single_match: false)
35
+ new(&lam).setup(
36
+ policy_name: policy_name,
37
+ comment: comment,
38
+ cacheable: cacheable,
39
+ single_match: single_match
40
+ )
41
+ end
42
+
43
+ # Configure the policy after creation
44
+ #
45
+ # @param policy_name [Symbol, nil] Human-readable name for logging
46
+ # @param comment [String, nil] Description of what this policy does
47
+ # @param cacheable [Boolean] Whether results can be cached (default: true)
48
+ # @param single_match [Boolean] Whether only one subject/actor pair can match (default: false)
49
+ def setup(policy_name: nil, comment: nil, cacheable: true, single_match: false)
50
+ raise 'Privacy Policy is frozen' if @frozen
51
+
52
+ @cacheable = cacheable
53
+ @policy_name = policy_name.to_s
54
+ @comment = comment
55
+ @frozen = true
56
+ @single_match = single_match
57
+ self
58
+ end
59
+
60
+ sig { returns(T::Boolean) }
61
+ def cacheable?
62
+ @cacheable || false
63
+ end
64
+
65
+ # Single-match optimization: when true, once a policy allows for a subject/actor pair,
66
+ # skip evaluation for other subjects (e.g., AllowIfActorIsSelf - only one subject matches)
67
+ sig { returns(T::Boolean) }
68
+ def single_match?
69
+ @single_match || false
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ # Type aliases for use throughout the gem
76
+ module Sequel
77
+ module Privacy
78
+ TPolicy = T.type_alias { Sequel::Privacy::Policy }
79
+ TPolicyArray = T.type_alias { T::Array[T.any(TPolicy, Proc)] }
80
+ TPolicySubject = T.type_alias { T.any(Sequel::Model, T.untyped) }
81
+ end
82
+ end
@@ -0,0 +1,38 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Sequel
5
+ module Privacy
6
+ # DSL for defining custom policies.
7
+ # Extend your policy module with this to get the `policy` method.
8
+ #
9
+ # Example:
10
+ # module P
11
+ # extend Sequel::Privacy::PolicyDSL
12
+ #
13
+ # AlwaysDeny = Sequel::Privacy::BuiltInPolicies::AlwaysDeny
14
+ #
15
+ # policy :AllowAdmins, ->(actor) {
16
+ # allow if actor.is_role?(:admin)
17
+ # }, 'Allow admin users', cacheable: true
18
+ # end
19
+ module PolicyDSL
20
+ # Define a new policy constant on the extending module.
21
+ #
22
+ # @param name [Symbol] The policy name (will become a constant)
23
+ # @param lam [Proc] The policy lambda (0-3 args: actor, subject, direct_object)
24
+ # @param comment [String, nil] Human-readable description
25
+ # @param cacheable [Boolean] Whether results can be cached (default: true)
26
+ # @param single_match [Boolean] Whether only one subject/actor can match (default: false)
27
+ def policy(name, lam, comment = nil, cacheable: true, single_match: false)
28
+ p = Policy.new(&lam).setup(
29
+ policy_name: name,
30
+ comment: comment,
31
+ cacheable: cacheable,
32
+ single_match: single_match
33
+ )
34
+ const_set(name, p)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,8 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Sequel
5
+ module Privacy
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
@@ -0,0 +1,127 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Sequel
5
+ module Privacy
6
+ # ViewerContext represents who is viewing/accessing data.
7
+ # All privacy checks require a viewer context to determine what the viewer can see.
8
+ class ViewerContext
9
+ extend T::Sig
10
+ extend T::Helpers
11
+ abstract!
12
+
13
+ # Create a standard viewer context for an actor
14
+ sig { params(actor: IActor).returns(ActorVC) }
15
+ def self.for_actor(actor)
16
+ ActorVC.new(actor)
17
+ end
18
+
19
+ # Create an API-specific viewer context
20
+ sig { params(actor: IActor).returns(APIVC) }
21
+ def self.for_api_actor(actor)
22
+ APIVC.new(actor)
23
+ end
24
+
25
+ # Create an all-powerful viewer context that bypasses all privacy checks.
26
+ # Use sparingly and always provide a reason for audit logging.
27
+ sig { params(reason: Symbol).returns(AllPowerfulVC) }
28
+ def self.all_powerful(reason)
29
+ Sequel::Privacy.logger&.info("Creating all-powerful viewer context: #{reason}")
30
+ AllPowerfulVC.new(reason)
31
+ end
32
+
33
+ # Create an omniscient viewer context that can see everything but cannot mutate.
34
+ # Used for system operations like authentication lookups.
35
+ sig { params(reason: Symbol).returns(OmniscientVC) }
36
+ def self.omniscient(reason)
37
+ Sequel::Privacy.logger&.debug("Creating omniscient viewer context: #{reason}")
38
+ OmniscientVC.new(reason)
39
+ end
40
+
41
+ # Create an anonymous viewer context for logged-out users.
42
+ # Subject to normal policy evaluation with no actor.
43
+ sig { returns(AnonymousVC) }
44
+ def self.anonymous
45
+ AnonymousVC.new
46
+ end
47
+ end
48
+
49
+ # Standard viewer context with an actor (user/member)
50
+ class ActorVC < ViewerContext
51
+ extend T::Sig
52
+
53
+ sig { params(actor: IActor).void }
54
+ def initialize(actor)
55
+ @actor = T.let(actor, IActor)
56
+ super()
57
+ end
58
+
59
+ sig { returns(IActor) }
60
+ attr_reader :actor
61
+ end
62
+
63
+ # API-specific viewer context (same as ActorVC but can be distinguished)
64
+ class APIVC < ActorVC; end
65
+
66
+ # All-powerful viewer context that bypasses all privacy checks.
67
+ # Used for admin operations, background jobs, etc.
68
+ # Requires a reason for audit logging.
69
+ class AllPowerfulVC < ViewerContext
70
+ extend T::Sig
71
+
72
+ sig { params(reason: Symbol).void }
73
+ def initialize(reason)
74
+ @reason = T.let(reason, Symbol)
75
+ super()
76
+ end
77
+
78
+ sig { returns(Symbol) }
79
+ attr_reader :reason
80
+ end
81
+
82
+ # Omniscient viewer context that can see everything but cannot mutate.
83
+ # Used for system operations like authentication lookups.
84
+ class OmniscientVC < ViewerContext
85
+ extend T::Sig
86
+
87
+ sig { params(reason: Symbol).void }
88
+ def initialize(reason)
89
+ @reason = T.let(reason, Symbol)
90
+ super()
91
+ end
92
+
93
+ sig { returns(Symbol) }
94
+ attr_reader :reason
95
+ end
96
+
97
+ # Anonymous viewer context for logged-out users.
98
+ # Has no actor - policies with arity >= 1 will auto-deny.
99
+ class AnonymousVC < ViewerContext
100
+ extend T::Sig
101
+
102
+ sig { void }
103
+ def initialize
104
+ super()
105
+ end
106
+ end
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
+ # Type alias for viewer contexts
125
+ TViewerContext = T.type_alias { ViewerContext }
126
+ end
127
+ end
@@ -0,0 +1,33 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'sequel'
5
+ require 'sorbet-runtime'
6
+
7
+ module Sequel
8
+ module Privacy
9
+ class << self
10
+ extend T::Sig
11
+
12
+ # Configurable logger for privacy enforcement.
13
+ # Set this to your application's logger (e.g., SemanticLogger).
14
+ sig { returns(T.untyped) }
15
+ attr_accessor :logger
16
+ end
17
+ end
18
+ end
19
+
20
+ # Core privacy infrastructure
21
+ require_relative 'sequel/privacy/version'
22
+ require_relative 'sequel/privacy/errors'
23
+ require_relative 'sequel/privacy/i_actor'
24
+ require_relative 'sequel/privacy/policy'
25
+ require_relative 'sequel/privacy/cache'
26
+ require_relative 'sequel/privacy/actions'
27
+ require_relative 'sequel/privacy/viewer_context'
28
+ require_relative 'sequel/privacy/enforcer'
29
+ require_relative 'sequel/privacy/built_in_policies'
30
+ require_relative 'sequel/privacy/policy_dsl'
31
+
32
+ # The plugin is auto-loaded by Sequel when you call `plugin :privacy`
33
+ # from lib/sequel/plugins/privacy.rb