sequel-privacy 0.6.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 627da75fe2db0e41382bd4b6a6e44cf85bb01a3a52aef9c1c16df85b0b4af5d8
4
- data.tar.gz: e0cfd6b79e35d81a51d6897fa37f9130e5fb567f3422c0014500be6bb0d95183
3
+ metadata.gz: c7ef079bb3498aa1006aa327a138bcb1735b78ae02a65b11d2ee4976d9871978
4
+ data.tar.gz: 95000199db0cbb08908c1a261152c641b71ceb7898875a9f732936662df9334e
5
5
  SHA512:
6
- metadata.gz: c1450ee72abf6ef060a7d2f8d541f3d418724cca9650370746e996427d4da438e93fa96410b47b6606ab4a41542d014b46dbf97a08e3e4623cf120b419736372
7
- data.tar.gz: 5ccadc4dfb1b84fb7ab9d3beefb2b396b2b1302239c0b63fc97a2c34a6fb9b5a025857542814167e007326a17e08e1697d56731cdff690a5c9d72d850cb96529
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.1.0] - 2024-01-24
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. Be sure to properly set to an Actor VC after you've logged-in
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
- # You can use an omniscient viewer context to load the user from a session
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
- # Attach an ActorVC to the loaded user so that future calls to its fields and
278
- # associations respect privacy .
279
- @current_user ||= user.for_vc(Sequel::Privacy::ViewerContext.for_actor(user))
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:
@@ -639,13 +639,12 @@ module Sequel
639
639
  @viewer_context = T.let(@viewer_context, T.nilable(Sequel::Privacy::ViewerContext))
640
640
  end
641
641
 
642
- sig { params(vc: T.nilable(Sequel::Privacy::ViewerContext)).returns(T.nilable(Sequel::Privacy::ViewerContext)) }
643
- def viewer_context=(vc)
644
- @viewer_context = T.let(vc, T.nilable(Sequel::Privacy::ViewerContext))
645
- end
646
642
 
647
- sig { params(vc: Sequel::Privacy::ViewerContext).returns(T.self_type) }
648
- def for_vc(vc)
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
649
648
  @viewer_context = T.let(vc, T.nilable(Sequel::Privacy::ViewerContext))
650
649
  self
651
650
  end
@@ -38,6 +38,8 @@ module Sequel
38
38
  ).returns(T::Boolean)
39
39
  end
40
40
  def self.enforce(policies, subject, viewer_context, direct_object = nil)
41
+ viewer_context.assert_usable!
42
+
41
43
  saved = Thread.current[EVAL_KEY]
42
44
  Thread.current[EVAL_KEY] = true
43
45
 
@@ -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
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Sequel
5
5
  module Privacy
6
- VERSION = '0.6.1'
6
+ VERSION = '0.7.0'
7
7
  end
8
8
  end
@@ -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: 0.6.1
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.15
155
+ rubygems_version: 4.0.14
142
156
  specification_version: 4
143
157
  summary: Privacy enforcement plugin for Sequel models
144
158
  test_files: []