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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +412 -0
- data/lib/sequel/plugins/privacy.rb +792 -0
- data/lib/sequel/privacy/actions.rb +40 -0
- data/lib/sequel/privacy/built_in_policies.rb +37 -0
- data/lib/sequel/privacy/cache.rb +33 -0
- data/lib/sequel/privacy/enforcer.rb +246 -0
- data/lib/sequel/privacy/errors.rb +23 -0
- data/lib/sequel/privacy/i_actor.rb +17 -0
- data/lib/sequel/privacy/policy.rb +82 -0
- data/lib/sequel/privacy/policy_dsl.rb +38 -0
- data/lib/sequel/privacy/version.rb +8 -0
- data/lib/sequel/privacy/viewer_context.rb +127 -0
- data/lib/sequel-privacy.rb +33 -0
- data/rbi/sequel_privacy.rbi +66 -0
- metadata +144 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 644015302ea6057f9b102b24803bafb4c67059e51a6a66c02d825216404ae05f
|
|
4
|
+
data.tar.gz: 8504cb1ca168b0f34b2fa7705a782ca74567aa1839d4f13816f69aaa5e27546e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8a714fa412538a84dbed42f938eb419a85c626a045a5fc6fa8865014b039bc49eee75eb429c5adeb6d946908ab3e09c04bf5b8240c231090e955b6f8dbed5d4c
|
|
7
|
+
data.tar.gz: d82cd3eaf425d213c12d99a813a6de7656b377241a64c0c82bd6ba098e42e69334567f8b1e03d8e1ff209932b4e8b0310725b57be0a91eb720d30b752e63ca06
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
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
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2024-01-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release
|
|
14
|
+
- `plugin :privacy` for Sequel models
|
|
15
|
+
- Policy definition DSL (`policies` method)
|
|
16
|
+
- Field-level privacy protection (`protect_field` method)
|
|
17
|
+
- ViewerContext system (ActorVC, AllPowerfulVC, APIVC)
|
|
18
|
+
- Policy evaluation engine with caching
|
|
19
|
+
- Built-in policies: AlwaysDeny, AlwaysAllow, PassAndLog
|
|
20
|
+
- PolicyDSL module for defining custom policies
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Austin Bales
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# Sequel::Privacy
|
|
2
|
+
|
|
3
|
+
A Sequel plugin that allows you to define policies that are executed when your models are loaded, created or mutated.
|
|
4
|
+
Supports field-level policies to protect data based on actor/viewers' relationships to given models.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Add to your Gemfile:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem 'sequel-privacy'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Then require it after Sequel:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
require 'sequel'
|
|
18
|
+
require 'sequel-privacy'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### 1. Define Your Policy Module
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
# policies/base.rb
|
|
27
|
+
module P
|
|
28
|
+
extend Sequel::Privacy::PolicyDSL
|
|
29
|
+
|
|
30
|
+
AlwaysDeny = Sequel::Privacy::BuiltInPolicies::AlwaysDeny
|
|
31
|
+
AlwaysAllow = Sequel::Privacy::BuiltInPolicies::AlwaysAllow
|
|
32
|
+
PassAndLog = Sequel::Privacy::BuiltInPolicies::PassAndLog
|
|
33
|
+
|
|
34
|
+
policy :AllowIfPublished, ->(subject) {
|
|
35
|
+
allow if subject.published
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
policy :AllowAdmins, ->(_subject, actor) {
|
|
39
|
+
allow if actor.is_role?(:admin)
|
|
40
|
+
}, 'Allow admin users', cacheable: true
|
|
41
|
+
|
|
42
|
+
policy :AllowMembers, ->(_subject, actor) {
|
|
43
|
+
allow if actor.is_role?(:member)
|
|
44
|
+
}, cacheable: true
|
|
45
|
+
|
|
46
|
+
policy :AllowSelf, ->(subject, actor) {
|
|
47
|
+
allow if subject == actor
|
|
48
|
+
}, 'Allow if subject is the actor', single_match: true
|
|
49
|
+
|
|
50
|
+
policy :AllowFriendsOfSubject, ->(subject, actor) {
|
|
51
|
+
allow if subject.includes_friend?(actor)
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. Add Privacy to Your Models
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
class Member < Sequel::Model
|
|
60
|
+
plugin :privacy
|
|
61
|
+
|
|
62
|
+
privacy do
|
|
63
|
+
# Define who can view this model; be strategic about the order of your policies so that You
|
|
64
|
+
# don't evaluate ones you don't need to.
|
|
65
|
+
can :view, P::AllowSelf, P::AllowMembers
|
|
66
|
+
can :edit, P::AllowSelf, P::AllowAdmins
|
|
67
|
+
can :create, P::AllowAdmins
|
|
68
|
+
|
|
69
|
+
field :email, P::AllowMembers
|
|
70
|
+
field :phone, P::AllowSelf, P::AllowFriendsOfSubject, P::AllowAdmins
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The `privacy` block provides:
|
|
76
|
+
- `can :action, *policies` - Define policies for an action (`:view`, `:edit`, `:create`, etc.)
|
|
77
|
+
- `field :name, *policies` - Protect a field (auto-creates `:view_#{field}` policy)
|
|
78
|
+
- `finalize!` - Prevent further modifications to privacy settings
|
|
79
|
+
|
|
80
|
+
`AlwaysDeny` is automatically appended to all policy chains (fail-secure by default).
|
|
81
|
+
|
|
82
|
+
### 3. Query with Privacy Enforcement
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# Create a viewer context
|
|
86
|
+
vc = Sequel::Privacy::ViewerContext.for_actor(current_user)
|
|
87
|
+
|
|
88
|
+
# Query - results are automatically filtered by :view policy
|
|
89
|
+
members = Member.for_vc(vc).where(org_id: 1).all
|
|
90
|
+
|
|
91
|
+
# Check permissions explicitly
|
|
92
|
+
member.allow?(vc, :view) # => true/false
|
|
93
|
+
member.allow?(vc, :edit) # => true/false
|
|
94
|
+
|
|
95
|
+
# Protected fields return nil if denied
|
|
96
|
+
member.email # => nil if :view_email denies
|
|
97
|
+
member.phone # => nil if :view_phone denies
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Policy Definition
|
|
101
|
+
|
|
102
|
+
Policies are lambdas that execute in the context of an `Actions` struct, giving access to `allow`, `deny`, and `pass` outcome methods, as well as the `all` combinator. Policies accept up to three parameters: `actor`, `subject` & `actor` or `subject`, `actor` and `direct_object`.
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
|
|
107
|
+
policy :AlwaysAllow, -> { allow }
|
|
108
|
+
|
|
109
|
+
policy :AllowIfPublished, ->(subject) {
|
|
110
|
+
allow if subject.published
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
policy :AllowAdmins, ->(_subject, actor) {
|
|
114
|
+
allow if actor.is_role?(:admin)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
policy :AllowOwner, ->(subject, actor) {
|
|
118
|
+
allow if subject.owner_id == actor.id
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
policy :AllowSelfJoin, ->(_group, actor, target_user) {
|
|
122
|
+
allow if actor.id == target_user.id
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
policy :AllowSelfRemove, ->(_group, actor, target_user) {
|
|
126
|
+
allow if actor.id == target_user.id
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Policy Return Values
|
|
131
|
+
|
|
132
|
+
- `allow` - Permits the action, stops evaluation
|
|
133
|
+
- `deny` - Rejects the action, stops evaluation
|
|
134
|
+
- `pass` (or no explicit return) - Continues to the next policy in the chain
|
|
135
|
+
|
|
136
|
+
### Policy Options
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
policy :MyPolicy, ->() { ... },
|
|
140
|
+
'Human-readable description', # For logging
|
|
141
|
+
cacheable: true, # Cache results (default: true)
|
|
142
|
+
single_match: false # Only one subject can match
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**`cacheable: true`** (default): Results are cached for the duration of the request, keyed by policy + arguments. Use for policies that don't depend on mutable state.
|
|
146
|
+
|
|
147
|
+
**`single_match: true`**: Optimization for policies for which there is only one matching Actor possible for a given Subject. For example in `AllowAuthors`, since a `Post` can have only one other, it's not worth a potentially expensive check on other combinations once you've found the winner.
|
|
148
|
+
|
|
149
|
+
### Policy Combinators
|
|
150
|
+
|
|
151
|
+
Use `all()` to require multiple conditions:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
policy :AllowMemberToRemoveSelf, ->(subject, actor, direct_object) {
|
|
155
|
+
all(
|
|
156
|
+
P::AllowIfIncludesMember,
|
|
157
|
+
P::AllowIfDirectObjectIsActor
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
All sub-policies must return `:allow` for the combinator to return `:allow`. Any `:deny` results in `:deny`.
|
|
163
|
+
|
|
164
|
+
## Viewer Contexts
|
|
165
|
+
|
|
166
|
+
Viewer Contexts should be created by the router/controller layer of your application, you should generally
|
|
167
|
+
have one VC for the entire request lifecycle. The plugin provides several VC types for different use-cases.
|
|
168
|
+
|
|
169
|
+
Anonymous VCs are useful for logged out users, and can check that their access is properly constrained to things
|
|
170
|
+
that are meant to be fully public.
|
|
171
|
+
|
|
172
|
+
Omniscient VCs are most useful when your application needs to see an object that a user cannot for some reason.
|
|
173
|
+
Handle them with care. Login is the most salient example.
|
|
174
|
+
|
|
175
|
+
All-Powerful VCs bypass all privacy checks and are used in situations where the system needs unfettered access
|
|
176
|
+
to models. In a production setting, your application should prohibit raw Database access outside of the privacy-aware
|
|
177
|
+
system, so these VCs give you an escape hatch for things like scripts while also keeping an audit trail.
|
|
178
|
+
|
|
179
|
+
`omniscient` and `all_powerful` require a reason (symbol) for audit logging.
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
# Standard viewer (most common)
|
|
183
|
+
current_vc = Sequel::Privacy::ViewerContext.for_actor(current_user)
|
|
184
|
+
users_groups = Group.for_vc(current_vc).where(creator: current_user).all
|
|
185
|
+
|
|
186
|
+
# API-specific (can be distinguished in policies)
|
|
187
|
+
vc = Sequel::Privacy::ViewerContext.for_api_actor(current_user)
|
|
188
|
+
|
|
189
|
+
# Anonymous viewer (logged-out users)
|
|
190
|
+
logged_out_vc = Sequel::Privacy::ViewerContext.anonymous
|
|
191
|
+
posts = Post.for_vc(logged_out_vc).where(published: true).all
|
|
192
|
+
|
|
193
|
+
# Omniscient VCs can read any object in the system, but are incapable of writes.
|
|
194
|
+
# Dispose of these ViewerContexts quickly.
|
|
195
|
+
current_user = Sequel::Privacy::ViewerContext.omniscient(:login).then {|vc| User.for_vc(vc)[authenticated_user_id] }
|
|
196
|
+
current_vc = Sequel::Privacy::ViewerContext.for_actor(current_user)
|
|
197
|
+
|
|
198
|
+
# All-powerful ViewerContexts dangerously bypass all read and write checks.
|
|
199
|
+
admin_vc = Sequel::Privacy::ViewerContext.all_powerful(:admin_migration)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Mutation Enforcement
|
|
203
|
+
|
|
204
|
+
When a viewer context is attached, mutations are automatically checked:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
member = Member.for_vc(vc).first
|
|
208
|
+
|
|
209
|
+
# Check :edit policy before saving existing records
|
|
210
|
+
member.name = "New Name"
|
|
211
|
+
member.save # Raises Unauthorized if :edit denies
|
|
212
|
+
|
|
213
|
+
# Create new records with privacy enforcement
|
|
214
|
+
new_member = Member.for_vc(vc).create(name: "Test")
|
|
215
|
+
# or
|
|
216
|
+
new_member = Member.for_vc(vc).new(name: "Test")
|
|
217
|
+
new_member.save # Raises Unauthorized if :create denies
|
|
218
|
+
|
|
219
|
+
# Check field-level policies when modifying protected fields
|
|
220
|
+
member.update(email: "new@example.com") # Raises FieldUnauthorized if :view_email denies
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Association Privacy
|
|
224
|
+
|
|
225
|
+
For operations involving associations (like adding/removing members from a group), use the `association` block in the privacy DSL. This automatically wraps Sequel's association methods (`add_*`, `remove_*`, `remove_all_*`) with privacy checks.
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
class Group < Sequel::Model
|
|
229
|
+
plugin :privacy
|
|
230
|
+
|
|
231
|
+
many_to_many :members, class: :User,
|
|
232
|
+
join_table: :group_memberships,
|
|
233
|
+
left_key: :group_id,
|
|
234
|
+
right_key: :user_id
|
|
235
|
+
|
|
236
|
+
privacy do
|
|
237
|
+
can :view, P::AllowMembers
|
|
238
|
+
can :edit, P::AllowAdmins
|
|
239
|
+
|
|
240
|
+
association :members do
|
|
241
|
+
can :add, P::AllowGroupAdmin, P::AllowSelfJoin
|
|
242
|
+
can :remove, P::AllowGroupAdmin, P::AllowSelfRemove
|
|
243
|
+
can :remove_all, P::AllowGroupAdmin
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The `association` block supports three actions:
|
|
250
|
+
- `:add` - Wraps `add_*` method (e.g., `add_member`)
|
|
251
|
+
- `:remove` - Wraps `remove_*` method (e.g., `remove_member`)
|
|
252
|
+
- `:remove_all` - Wraps `remove_all_*` method (e.g., `remove_all_members`)
|
|
253
|
+
|
|
254
|
+
Association policies use 3-arity, receiving `(subject, actor, direct_object)`:
|
|
255
|
+
- `subject` - The model instance (e.g., the group)
|
|
256
|
+
- `actor` - The current user from the viewer context
|
|
257
|
+
- `direct_object` - The object being added/removed (e.g., the user being added to the group)
|
|
258
|
+
|
|
259
|
+
For `remove_all`, the direct object is `nil` since there's no specific target.
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
# Allow users to add/remove themselves
|
|
263
|
+
policy :AllowSelfJoin, ->(_group, actor, target_user) {
|
|
264
|
+
allow if actor.id == target_user.id
|
|
265
|
+
}, single_match: true
|
|
266
|
+
|
|
267
|
+
policy :AllowSelfRemove, ->(_group, actor, target_user) {
|
|
268
|
+
allow if actor.id == target_user.id
|
|
269
|
+
}, single_match: true
|
|
270
|
+
|
|
271
|
+
# Allow group admins to add/remove anyone
|
|
272
|
+
policy :AllowGroupAdmin, ->(group, actor, _target_user) {
|
|
273
|
+
allow if GroupAdmin.where(group_id: group.id, user_id: actor.id).exists?
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Usage:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
group = Group.for_vc(vc).first
|
|
281
|
+
|
|
282
|
+
# User joins themselves (allowed by AllowSelfJoin)
|
|
283
|
+
group.add_member(current_user)
|
|
284
|
+
|
|
285
|
+
# Admin removes another user (allowed by AllowGroupAdmin)
|
|
286
|
+
group.remove_member(other_user)
|
|
287
|
+
|
|
288
|
+
# Admin removes all members
|
|
289
|
+
group.remove_all_members
|
|
290
|
+
|
|
291
|
+
# Non-admin trying to add someone else raises Unauthorized
|
|
292
|
+
group.add_member(other_user) # Raises Sequel::Privacy::Unauthorized
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Association privacy methods:
|
|
296
|
+
- Require a viewer context (raises `MissingViewerContext` if missing)
|
|
297
|
+
- Deny operations with `OmniscientVC` (read-only context cannot mutate)
|
|
298
|
+
- Work with both `one_to_many` and `many_to_many` associations
|
|
299
|
+
|
|
300
|
+
### Exception Types
|
|
301
|
+
|
|
302
|
+
- `Sequel::Privacy::Unauthorized` - Action denied at the record level
|
|
303
|
+
- `Sequel::Privacy::FieldUnauthorized` - Action denied at the field level
|
|
304
|
+
- `Sequel::Privacy::MissingViewerContext` - Attempted privacy-aware query without a viewer context
|
|
305
|
+
|
|
306
|
+
## Logging
|
|
307
|
+
|
|
308
|
+
Configure a logger to see policy evaluation:
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
Sequel::Privacy.logger = Logger.new(STDOUT)
|
|
312
|
+
# or with SemanticLogger
|
|
313
|
+
Sequel::Privacy.logger = SemanticLogger['Privacy']
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Log output shows:
|
|
317
|
+
- Policy evaluation results (ALLOW/DENY/PASS)
|
|
318
|
+
- Cache hits
|
|
319
|
+
- Single-match optimizations
|
|
320
|
+
- All-powerful/omniscient context bypasses
|
|
321
|
+
|
|
322
|
+
## Cache Management
|
|
323
|
+
|
|
324
|
+
Policy results are cached per-request to avoid redundant evaluation. Clear between requests:
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
# In Rack middleware
|
|
328
|
+
class PrivacyCacheMiddleware
|
|
329
|
+
def initialize(app)
|
|
330
|
+
@app = app
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def call(env)
|
|
334
|
+
Sequel::Privacy.clear_cache!
|
|
335
|
+
@app.call(env)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Or manually:
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
Sequel::Privacy.cache.clear
|
|
344
|
+
Sequel::Privacy.single_matches.clear
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Actor Interface
|
|
348
|
+
|
|
349
|
+
Your user/member model must implement `Sequel::Privacy::IActor`:
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
class Member < Sequel::Model
|
|
353
|
+
include Sequel::Privacy::IActor
|
|
354
|
+
|
|
355
|
+
def id
|
|
356
|
+
self[:id]
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The interface requires:
|
|
362
|
+
- `id` - Returns the actor's unique identifier
|
|
363
|
+
|
|
364
|
+
You can add additional methods like `is_role?` for use in your policies, but they are not required by the interface.
|
|
365
|
+
|
|
366
|
+
## Policy Inheritance
|
|
367
|
+
|
|
368
|
+
Child classes inherit privacy policies from their parents:
|
|
369
|
+
|
|
370
|
+
```ruby
|
|
371
|
+
class User < Sequel::Model
|
|
372
|
+
plugin :privacy
|
|
373
|
+
|
|
374
|
+
privacy do
|
|
375
|
+
can :view, P::AllowAdmins
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
class Admin < User
|
|
380
|
+
# Inherits :view policy
|
|
381
|
+
privacy do
|
|
382
|
+
can :edit, P::AllowSelf
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Built-in Policies
|
|
388
|
+
|
|
389
|
+
- `Sequel::Privacy::BuiltInPolicies::AlwaysDeny` - Always denies (fail-secure default)
|
|
390
|
+
- `Sequel::Privacy::BuiltInPolicies::AlwaysAllow` - Always allows
|
|
391
|
+
- `Sequel::Privacy::BuiltInPolicies::PassAndLog` - Passes with a log message (useful for debugging)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
## Type Safety (Sorbet)
|
|
395
|
+
|
|
396
|
+
The gem is mostly fully typed with Sorbet. Type definitions are provided for all public APIs.
|
|
397
|
+
|
|
398
|
+
## AI Statement
|
|
399
|
+
|
|
400
|
+
The core of this project was written by me (arbales) over the course of 2025 for a platform that
|
|
401
|
+
manages mailing lists and member information for a social group. Claude assisted substantially with
|
|
402
|
+
extracting it into a Gem and wrote the tests in their entirety.
|
|
403
|
+
|
|
404
|
+
## TODO
|
|
405
|
+
|
|
406
|
+
I'd like to support generation of Sorbet signatures on models that use the plugin so that
|
|
407
|
+
Sorbet users don't have to shim their models to use it. In my own work, I've shimmed my
|
|
408
|
+
base model class, but this isn't ideal.
|
|
409
|
+
|
|
410
|
+
## License
|
|
411
|
+
|
|
412
|
+
MIT
|