sequel-privacy 0.5 → 0.5.2
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/README.md +17 -39
- data/lib/sequel/plugins/privacy.rb +25 -0
- data/lib/sequel/privacy/built_in_policies.rb +2 -4
- data/lib/sequel/privacy/cache.rb +0 -1
- data/lib/sequel/privacy/enforcer.rb +9 -19
- 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: 2e415c08d25c658e76c101a7c7ccbdbb791ef1f00deb6a81481ada2044a2b779
|
|
4
|
+
data.tar.gz: 819dfce705006ad300e092650cd2014944989c6bec4baefb6bdd5a0fe1bd6bc1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b875efc8f711c1403eb90e8ebc6ca5071b325c3e6e7856cc7c43f4dc43f5edb4add75a6081db527d30a184c856719a1461da50a2e6a754567a8a5886567daea8
|
|
7
|
+
data.tar.gz: b1c2f4a0525b3d4232f4f369d483affbf3bcc84235602d116f2d8a40d80c686e5bd8a677e1367bbc53012b8341a63e787b7d502377b2322f63afdbf5f10518ec
|
data/README.md
CHANGED
|
@@ -221,56 +221,34 @@ system, so these VCs give you an escape hatch for things like scripts while also
|
|
|
221
221
|
You could also create lint rules that prevent the casual creation of these viewer contexts.
|
|
222
222
|
|
|
223
223
|
```ruby
|
|
224
|
+
# You can use an omniscient viewer context to load the user from a session
|
|
225
|
+
# or however you store them. Discard this viewer context when you're done with it.
|
|
226
|
+
def current_user
|
|
227
|
+
return @current_user if @current_user
|
|
228
|
+
login_vc = Sequel::Privacy::ViewerContext.omniscient(:login)
|
|
229
|
+
user = User.for_vc(login_vc)[session_user_id]
|
|
230
|
+
return nil unless user
|
|
231
|
+
|
|
232
|
+
# Attach an ActorVC to the loaded user so that future calls to its fields and
|
|
233
|
+
# associations respect privacy .
|
|
234
|
+
@current_user ||= user.for_vc(Sequel::Privacy::ViewerContext.for_actor(user))
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def current_vc
|
|
238
|
+
current_user&.vc || Sequel::Privacy::ViewerContext.anonymous()
|
|
239
|
+
end
|
|
240
|
+
|
|
224
241
|
# Standard viewer (most common)
|
|
225
|
-
current_vc = Sequel::Privacy::ViewerContext.for_actor(current_user)
|
|
226
242
|
users_groups = Group.for_vc(current_vc).where(creator: current_user).all
|
|
227
243
|
|
|
228
|
-
# API-specific (can be distinguished in policies)
|
|
229
|
-
vc = Sequel::Privacy::ViewerContext.for_api_actor(current_user)
|
|
230
|
-
|
|
231
244
|
# Anonymous viewer (logged-out users)
|
|
232
245
|
logged_out_vc = Sequel::Privacy::ViewerContext.anonymous
|
|
233
246
|
posts = Post.for_vc(logged_out_vc).where(published: true).all
|
|
234
247
|
|
|
235
|
-
# Omniscient VCs can read any object in the system, but are incapable of writes.
|
|
236
|
-
# Dispose of these ViewerContexts quickly.
|
|
237
|
-
current_user = Sequel::Privacy::ViewerContext.omniscient(:login).then {|vc| User.for_vc(vc)[authenticated_user_id] }
|
|
238
|
-
current_vc = Sequel::Privacy::ViewerContext.for_actor(current_user)
|
|
239
|
-
|
|
240
248
|
# All-powerful ViewerContexts dangerously bypass all read and write checks.
|
|
241
249
|
admin_vc = Sequel::Privacy::ViewerContext.all_powerful(:admin_migration)
|
|
242
250
|
```
|
|
243
251
|
|
|
244
|
-
### A Note Login & Authenticated Users
|
|
245
|
-
|
|
246
|
-
If your User or equivalent model is privacy-aware *and* is protected by
|
|
247
|
-
policies that would complicating fetching (or login), then you will have
|
|
248
|
-
trouble creating a `current_user` for an `ActorVC`.
|
|
249
|
-
|
|
250
|
-
In both cases you can use an `OmniscientVC` to make your initial User query.
|
|
251
|
-
|
|
252
|
-
```ruby
|
|
253
|
-
before do
|
|
254
|
-
if session[:user_id]
|
|
255
|
-
current_user = Sequel::Privacy::ViewerContext.omniscient(:session).then {|vc| User.for_vc(vc)[session[:user_id]] }
|
|
256
|
-
current_vc = Sequel::Privacy::ViewerContext.for_actor(current_user)
|
|
257
|
-
else
|
|
258
|
-
current_user = nil
|
|
259
|
-
current_vc = Sequel::Privacy::ViewerContext.anonymous
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
post '/auth/password' do
|
|
264
|
-
user = Sequel::Privacy::ViewerContext.omniscient(:login).then {|vc| User.for_vc(vc).first(email: params[:email]) }
|
|
265
|
-
|
|
266
|
-
pass unless user
|
|
267
|
-
pass unless user.password == params[:password]
|
|
268
|
-
|
|
269
|
-
session[:user_id] = user.id
|
|
270
|
-
redirect '/'
|
|
271
|
-
end
|
|
272
|
-
```
|
|
273
|
-
|
|
274
252
|
## Mutation Enforcement
|
|
275
253
|
|
|
276
254
|
When a viewer context is attached, mutations are automatically checked:
|
|
@@ -405,8 +405,10 @@ module Sequel
|
|
|
405
405
|
case type
|
|
406
406
|
when :many_to_one, :one_to_one
|
|
407
407
|
_override_singular_association(name)
|
|
408
|
+
_override_association_dataset(name)
|
|
408
409
|
when :one_to_many, :many_to_many
|
|
409
410
|
_override_plural_association(name)
|
|
411
|
+
_override_association_dataset(name)
|
|
410
412
|
# Check if there are already privacy policies defined for this association
|
|
411
413
|
setup_association_privacy(name) if privacy_association_policies[name]
|
|
412
414
|
end
|
|
@@ -436,6 +438,29 @@ module Sequel
|
|
|
436
438
|
|
|
437
439
|
private
|
|
438
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
|
+
|
|
439
464
|
sig { params(name: Symbol).void }
|
|
440
465
|
def _override_singular_association(name)
|
|
441
466
|
original = instance_method(name)
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
module Sequel
|
|
5
5
|
module Privacy
|
|
6
|
-
# Built-in policies that ship with the gem.
|
|
7
|
-
# Applications should define their own policies using PolicyDSL.
|
|
8
6
|
module BuiltInPolicies
|
|
9
|
-
# Always deny access.
|
|
7
|
+
# Always deny access. You should specify this policy at the end of every chain, but the framework
|
|
8
|
+
# currently appends it. This could change; I'm not sure about this yet.
|
|
10
9
|
AlwaysDeny = Policy.create(
|
|
11
10
|
:AlwaysDeny,
|
|
12
11
|
-> { :deny },
|
|
@@ -14,7 +13,6 @@ module Sequel
|
|
|
14
13
|
cacheable: true
|
|
15
14
|
)
|
|
16
15
|
|
|
17
|
-
# Always allow access. Use sparingly.
|
|
18
16
|
AlwaysAllow = Policy.create(
|
|
19
17
|
:AlwaysAllow,
|
|
20
18
|
-> { :allow },
|
data/lib/sequel/privacy/cache.rb
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
module Sequel
|
|
5
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
6
|
module Enforcer
|
|
9
7
|
extend T::Sig
|
|
10
8
|
|
|
@@ -138,9 +136,7 @@ module Sequel
|
|
|
138
136
|
).returns(Symbol)
|
|
139
137
|
end
|
|
140
138
|
def self.evaluate_child_policies(child_policies, subject, actor, viewer_context, direct_object)
|
|
141
|
-
unless child_policies.all? { |c| c.is_a?(Proc) }
|
|
142
|
-
Kernel.raise "Policy combinator contains non-policy members"
|
|
143
|
-
end
|
|
139
|
+
Kernel.raise 'Policy combinator contains non-policy members' unless child_policies.all? { |c| c.is_a?(Proc) }
|
|
144
140
|
|
|
145
141
|
results = child_policies.map do |child_policy|
|
|
146
142
|
policy_result(child_policy, subject, actor, viewer_context, direct_object)
|
|
@@ -190,14 +186,10 @@ module Sequel
|
|
|
190
186
|
result ||= :pass
|
|
191
187
|
|
|
192
188
|
# Handle combinator results
|
|
193
|
-
if result.is_a?(Array)
|
|
194
|
-
result = evaluate_child_policies(result, subject, actor, viewer_context, direct_object)
|
|
195
|
-
end
|
|
189
|
+
result = evaluate_child_policies(result, subject, actor, viewer_context, direct_object) if result.is_a?(Array)
|
|
196
190
|
|
|
197
191
|
# Cache result
|
|
198
|
-
if policy.cacheable? && !from_cache
|
|
199
|
-
Sequel::Privacy.cache[cache_key] = result
|
|
200
|
-
end
|
|
192
|
+
Sequel::Privacy.cache[cache_key] = result if policy.cacheable? && !from_cache
|
|
201
193
|
|
|
202
194
|
# Log result
|
|
203
195
|
log_result(policy, result, actor, subject, from_cache, skipped_from_single_match)
|
|
@@ -227,9 +219,7 @@ module Sequel
|
|
|
227
219
|
# Anonymous viewers (no actor) auto-deny unless the policy opts in
|
|
228
220
|
# with allow_anonymous: true (for state-gate policies that examine
|
|
229
221
|
# only the subject).
|
|
230
|
-
if !actor && policy.arity >= 1 && !policy.allow_anonymous?
|
|
231
|
-
return :deny
|
|
232
|
-
end
|
|
222
|
+
return :deny if !actor && policy.arity >= 1 && !policy.allow_anonymous?
|
|
233
223
|
|
|
234
224
|
case policy.arity
|
|
235
225
|
when 0
|
|
@@ -259,14 +249,14 @@ module Sequel
|
|
|
259
249
|
actor_id = actor ? actor.id : 'anonymous'
|
|
260
250
|
logger.debug do
|
|
261
251
|
msg = "#{result.to_s.upcase}: #{policy.policy_name || 'anonymous'} for actor[#{actor_id}] on #{subject.class}[#{subject_id(subject)}]"
|
|
262
|
-
msg +=
|
|
263
|
-
msg +=
|
|
252
|
+
msg += ' (cached)' if from_cache
|
|
253
|
+
msg += ' (skipped: single_match)' if skipped
|
|
264
254
|
msg
|
|
265
255
|
end
|
|
266
256
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
257
|
+
return unless policy.comment && %i[deny allow].include?(result)
|
|
258
|
+
|
|
259
|
+
logger.debug { " ⮑ #{policy.comment}" }
|
|
270
260
|
end
|
|
271
261
|
|
|
272
262
|
sig { params(subject: TPolicySubject).returns(T.untyped) }
|