hubbado-policy 1.0.1
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 +27 -0
- data/LICENSE +21 -0
- data/README.md +404 -0
- data/config/locales/en.yml +4 -0
- data/hubbado-policy.gemspec +35 -0
- data/lib/hubbado/policy/base.rb +78 -0
- data/lib/hubbado/policy/railtie.rb +7 -0
- data/lib/hubbado/policy/result.rb +40 -0
- data/lib/hubbado/policy/scope.rb +50 -0
- data/lib/hubbado-policy.rb +13 -0
- metadata +150 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8338406be7ea8412d5c7d81fee557858ac5327d9f84ef048a662b04f381dcb41
|
4
|
+
data.tar.gz: 4f58a42ba93883f48735ab220f9ff8047f360b41ea9d0a6e35c8c21c9a78318e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4e084a506c028722b36741f9d93f0f0cf6385d04c8aedbab34b3053eaaeb804e5734bc93466351f2c4ecfe486ea5763ba6f5ed2700877fbeb445a828eb61e1b2
|
7
|
+
data.tar.gz: 48fc0f6b56cd3dc53da3bc79e73fb9ba41bd35795e3e9da0b3e8b9d82164aafa07cb656047debfb8bcd067ca916b5b84e0e3012cb0bc1497d051ff23d870f480
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,27 @@
|
|
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
|
+
## [1.0.1] - 2025-05-20
|
9
|
+
|
10
|
+
Bump because rubygems does not allow repushing the same version even if it is yanked.
|
11
|
+
|
12
|
+
## [1.0.0] - 2025-05-20
|
13
|
+
|
14
|
+
### Added
|
15
|
+
- Initial release of the hubbado-policy gem
|
16
|
+
- `Policy` class with DSL for defining authorization rules
|
17
|
+
- `Result` class to represent policy outcomes
|
18
|
+
- `Scope` class for filtering collections based on permissions
|
19
|
+
- Testing support with `Substitute` module for scopes
|
20
|
+
- Rails integration via Railtie
|
21
|
+
- Full compatibility with the eventide-project/dependency gem
|
22
|
+
|
23
|
+
### Documentation
|
24
|
+
- Comprehensive README with usage examples
|
25
|
+
- Detailed explanations of all major components
|
26
|
+
- Rails integration guide
|
27
|
+
- Testing instructions and examples
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Hubbado
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,404 @@
|
|
1
|
+
# Hubbado Policy
|
2
|
+
|
3
|
+
A lightweight, flexible policy framework for Ruby applications that helps you implement authorization logic in a consistent way.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'hubbado-policy'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
$ bundle install
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
$ gem install hubbado-policy
|
23
|
+
```
|
24
|
+
|
25
|
+
## Overview
|
26
|
+
|
27
|
+
Hubbado Policy provides three main components:
|
28
|
+
|
29
|
+
1. **Policy** - Defines authorization rules
|
30
|
+
2. **Result** - Represents the outcome of policy checks
|
31
|
+
3. **Scope** - Filters collections based on authorization rules
|
32
|
+
|
33
|
+
## Policy Objects
|
34
|
+
|
35
|
+
Policy objects encapsulate authorization logic and determine whether certain actions are permitted for a given user and record combination.
|
36
|
+
|
37
|
+
### Basic Usage
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
class ArticlePolicy < Hubbado::Policy::Base
|
41
|
+
define_policy :view do
|
42
|
+
return permitted if user.admin?
|
43
|
+
|
44
|
+
if record.published?
|
45
|
+
permitted
|
46
|
+
else
|
47
|
+
denied(:not_published)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
define_policy :edit do
|
52
|
+
return permitted if user.admin?
|
53
|
+
|
54
|
+
if record.author == user
|
55
|
+
permitted
|
56
|
+
else
|
57
|
+
denied(:not_author)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Usage
|
63
|
+
policy = ArticlePolicy.build(current_user, article)
|
64
|
+
|
65
|
+
# Check permissions
|
66
|
+
if policy.view?
|
67
|
+
# User can view the article
|
68
|
+
end
|
69
|
+
|
70
|
+
# Get detailed result object
|
71
|
+
result = policy.edit
|
72
|
+
if result.permitted?
|
73
|
+
# User can edit
|
74
|
+
else
|
75
|
+
# Show error message
|
76
|
+
flash[:error] = result.message
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
### Policy DSL
|
81
|
+
|
82
|
+
The `define_policy` method creates three methods for each policy rule:
|
83
|
+
|
84
|
+
1. The base method (e.g., `edit`) that returns a `Result` object
|
85
|
+
2. A predicate method (e.g., `edit?`) that returns a boolean
|
86
|
+
3. An underlying implementation method
|
87
|
+
|
88
|
+
The policy methods support using `return` statements within the block. If a policy method returns `nil` or doesn't explicitly return a `permitted` or `denied` result, it will automatically default to a generic `denied` result. This simplifies policy implementations by not requiring explicit denials for all paths.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
define_policy :publish do
|
92
|
+
# Early returns work fine
|
93
|
+
return permitted if user.admin?
|
94
|
+
return denied(:not_verified) unless user.verified?
|
95
|
+
|
96
|
+
# If this evaluates to nil, it automatically becomes a generic denial
|
97
|
+
permitted if record.draft? && record.author == user
|
98
|
+
|
99
|
+
# Reaching the end of the method without returning a result
|
100
|
+
# will also produce a generic denial
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
### Policy Results
|
105
|
+
|
106
|
+
Every policy check returns a `Result` object, which can be either permitted or denied with a specific reason.
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
# Allow access
|
110
|
+
permitted
|
111
|
+
|
112
|
+
# Deny access with a reason code
|
113
|
+
denied(:not_author)
|
114
|
+
|
115
|
+
# Deny with additional data
|
116
|
+
denied(:quota_exceeded, data: { limit: 10, usage: 12 })
|
117
|
+
```
|
118
|
+
|
119
|
+
### Internationalization
|
120
|
+
|
121
|
+
Hubbado Policy integrates with I18n for translating error messages:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
# config/locales/en.yml
|
125
|
+
en:
|
126
|
+
article_policy:
|
127
|
+
not_published: "This article hasn't been published yet"
|
128
|
+
not_author: "Only the author can edit this article"
|
129
|
+
hubbado_policy:
|
130
|
+
errors:
|
131
|
+
denied: "Access denied"
|
132
|
+
```
|
133
|
+
|
134
|
+
## Result Objects
|
135
|
+
|
136
|
+
Result objects represent the outcome of a policy check, containing:
|
137
|
+
|
138
|
+
- Permission status (permitted or denied)
|
139
|
+
- Reason code for denial
|
140
|
+
- Optional additional data
|
141
|
+
- I18n integration for error messages
|
142
|
+
|
143
|
+
### Usage
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
result = ArticlePolicy.build(current_user, article).view
|
147
|
+
|
148
|
+
if result.permitted?
|
149
|
+
# Proceed with action
|
150
|
+
elsif result.denied?
|
151
|
+
# Handle denial
|
152
|
+
error_message = result.message # Automatically translated
|
153
|
+
additional_data = result.data # Any extra context provided
|
154
|
+
end
|
155
|
+
```
|
156
|
+
|
157
|
+
### Built-in Methods
|
158
|
+
|
159
|
+
- `permitted?` - Returns true if access is allowed
|
160
|
+
- `denied?` - Returns true if access is denied
|
161
|
+
- `generic_deny?` - Returns true if using the default denial reason
|
162
|
+
- `message` - Returns the localized error message
|
163
|
+
- `data` - Returns any additional context data
|
164
|
+
|
165
|
+
## Scope Objects
|
166
|
+
|
167
|
+
Scope objects filter collections based on what a user is authorized to access.
|
168
|
+
|
169
|
+
### Basic Usage
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
class ArticleScope < Hubbado::Policy::Scope
|
173
|
+
def self.default_scope
|
174
|
+
Article.all
|
175
|
+
end
|
176
|
+
|
177
|
+
def resolve(record, scope, **options)
|
178
|
+
return scope if record.admin?
|
179
|
+
|
180
|
+
scope.where(published: true).or(scope.where(author_id: record.id))
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Usage
|
185
|
+
visible_articles = ArticleScope.call(current_user)
|
186
|
+
```
|
187
|
+
|
188
|
+
### Custom Scopes
|
189
|
+
|
190
|
+
You can pass custom base scopes:
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
# Scope only to a specific category
|
194
|
+
category_articles = ArticleScope.call(
|
195
|
+
current_user,
|
196
|
+
Article.where(category_id: params[:category_id])
|
197
|
+
)
|
198
|
+
|
199
|
+
# Pass additional options
|
200
|
+
recent_articles = ArticleScope.call(
|
201
|
+
current_user,
|
202
|
+
Article.all,
|
203
|
+
only_recent: true
|
204
|
+
)
|
205
|
+
```
|
206
|
+
|
207
|
+
### Testing with Substitutes
|
208
|
+
|
209
|
+
Scope objects include a Substitute module for testing:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
# In your tests
|
213
|
+
scope = ArticleScope.new
|
214
|
+
scope.extend(Hubbado::Policy::Scope::Substitute)
|
215
|
+
scope.result = [article1, article2]
|
216
|
+
|
217
|
+
# Now you can assert the scope was called with expected arguments
|
218
|
+
scope.call(user)
|
219
|
+
assert scope.scoped?(user)
|
220
|
+
```
|
221
|
+
|
222
|
+
### Using with Eventide Dependency
|
223
|
+
|
224
|
+
Hubbado Policy is designed to work seamlessly with the [eventide-project/dependency](https://github.com/eventide-project/dependency) gem. Creating substitutes for testing is as simple as:
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
# Create a substitute instance of ArticleScope
|
228
|
+
article_scope = Dependency::Substitute.build(ArticleScope)
|
229
|
+
|
230
|
+
# Configure the result
|
231
|
+
article_scope.result = [article1, article2]
|
232
|
+
|
233
|
+
# Use in tests
|
234
|
+
service = SomeService.new
|
235
|
+
service.article_scope = article_scope
|
236
|
+
|
237
|
+
# Run the service
|
238
|
+
result = service.list_articles(user)
|
239
|
+
|
240
|
+
# Verify the scope was called with expected arguments
|
241
|
+
assert article_scope.scoped?(user)
|
242
|
+
```
|
243
|
+
|
244
|
+
This approach makes testing with substitutes straightforward while maintaining all the benefits of dependency injection.
|
245
|
+
|
246
|
+
## Dependency Configuration
|
247
|
+
|
248
|
+
Policy and Scope objects support the `configure` instance method that can be defined in a subclass. This method is called when the object is initialized and is intended to be used for configuring dependencies.
|
249
|
+
|
250
|
+
### Basic Configuration
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
class ComplexPolicy < HubbadoPolicy::Policy
|
254
|
+
attr_reader :permission_service
|
255
|
+
|
256
|
+
def configure
|
257
|
+
@permission_service = PermissionService.new
|
258
|
+
end
|
259
|
+
|
260
|
+
define_policy :complex_rule do
|
261
|
+
if permission_service.check_permission(user, record)
|
262
|
+
permitted
|
263
|
+
else
|
264
|
+
denied(:no_permission)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
```
|
269
|
+
|
270
|
+
### Integration with Eventide Dependency
|
271
|
+
|
272
|
+
Hubbado Policy is designed to work seamlessly with the [eventide-project/dependency](https://github.com/eventide-project/dependency) gem for dependency management. This provides a powerful way to handle service dependencies in your policies and scopes.
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
# First, set up your dependencies
|
276
|
+
require 'dependency'; Dependency.activate
|
277
|
+
require 'hubbado-policy'
|
278
|
+
|
279
|
+
class Services
|
280
|
+
dependency :permission_service, PermissionService
|
281
|
+
dependency :audit_logger, AuditLogger
|
282
|
+
end
|
283
|
+
|
284
|
+
# Then use them in your policy
|
285
|
+
class ArticlePolicy < HubbadoPolicy::Policy
|
286
|
+
dependency :permission_service, PermissionService
|
287
|
+
dependency :audit_logger, AuditLogger
|
288
|
+
|
289
|
+
def configure
|
290
|
+
Services.configure(self)
|
291
|
+
end
|
292
|
+
|
293
|
+
define_policy :publish do
|
294
|
+
# Log the attempt
|
295
|
+
audit_logger.log_action("publish_attempt", user: user, record: record)
|
296
|
+
|
297
|
+
# Check permissions
|
298
|
+
if permission_service.can_publish?(user, record)
|
299
|
+
permitted
|
300
|
+
else
|
301
|
+
denied(:cannot_publish)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
```
|
306
|
+
|
307
|
+
### Benefits of Using Dependency
|
308
|
+
|
309
|
+
Using the `dependency` gem with Hubbado Policy offers several advantages:
|
310
|
+
|
311
|
+
1. **Clear dependency declaration** - Dependencies are explicitly declared at the class level
|
312
|
+
2. **Consistent initialization** - The `configure` method provides a standard place for setting up dependencies
|
313
|
+
3. **Testability** - Dependencies can be easily substituted in tests
|
314
|
+
4. **Service reuse** - Common services can be configured once and reused across policies and scopes
|
315
|
+
|
316
|
+
### Using Dependency with Scopes
|
317
|
+
|
318
|
+
The same pattern works for Scope objects:
|
319
|
+
|
320
|
+
```ruby
|
321
|
+
class ArticleScope < HubbadoPolicy::Scope
|
322
|
+
include Dependency
|
323
|
+
|
324
|
+
dependency :visibility_service, VisibilityService
|
325
|
+
|
326
|
+
def configure
|
327
|
+
Services.configure(self)
|
328
|
+
end
|
329
|
+
|
330
|
+
def self.default_scope
|
331
|
+
Article.all
|
332
|
+
end
|
333
|
+
|
334
|
+
def resolve(record, scope, **options)
|
335
|
+
visibility_service.filter_visible_for(record, scope, **options)
|
336
|
+
end
|
337
|
+
end
|
338
|
+
```
|
339
|
+
|
340
|
+
## Rails Integration
|
341
|
+
|
342
|
+
Hubbado Policy includes built-in Rails integration through a Railtie that automatically loads the necessary components and configurations.
|
343
|
+
|
344
|
+
### Automatic Loading
|
345
|
+
|
346
|
+
When used with Rails, the gem automatically loads its default locale file:
|
347
|
+
|
348
|
+
```ruby
|
349
|
+
# lib/hubbado/policy/railtie.rb
|
350
|
+
module Hubbado
|
351
|
+
module Policy
|
352
|
+
class Railtie < ::Rails::Railtie
|
353
|
+
I18n.load_path << File.expand_path("../../../../config/locales/en.yml", __FILE__)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
```
|
358
|
+
|
359
|
+
The Railtie is loaded automatically when Rails is detected:
|
360
|
+
|
361
|
+
```ruby
|
362
|
+
# lib/hubbado-policy.rb
|
363
|
+
require "hubbado/policy/railtie" if defined?(Rails::Railtie)
|
364
|
+
```
|
365
|
+
|
366
|
+
### Recommended Rails Setup
|
367
|
+
|
368
|
+
For Rails applications, we recommend organizing your policies in the `app/policies` directory:
|
369
|
+
|
370
|
+
```
|
371
|
+
app/
|
372
|
+
├── policies/
|
373
|
+
│ ├── application_policy.rb
|
374
|
+
│ ├── article_policy.rb
|
375
|
+
│ └── user_policy.rb
|
376
|
+
├── scopes/
|
377
|
+
│ ├── article_scope.rb
|
378
|
+
│ └── user_scope.rb
|
379
|
+
├── controllers/
|
380
|
+
├── models/
|
381
|
+
└── ...
|
382
|
+
```
|
383
|
+
|
384
|
+
You may want to create a base `ApplicationPolicy` that all your policies inherit from:
|
385
|
+
|
386
|
+
```ruby
|
387
|
+
# app/policies/application_policy.rb
|
388
|
+
class ApplicationPolicy < Hubbado::Policy::Base
|
389
|
+
# Common methods for all policies
|
390
|
+
end
|
391
|
+
```
|
392
|
+
|
393
|
+
|
394
|
+
## Contributing
|
395
|
+
|
396
|
+
1. Fork it
|
397
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
398
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
399
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
400
|
+
5. Create new Pull Request
|
401
|
+
|
402
|
+
## License
|
403
|
+
|
404
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,35 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "hubbado-policy"
|
3
|
+
s.version = "1.0.1"
|
4
|
+
s.summary = "A lightweight, flexible policy framework for Ruby applications"
|
5
|
+
|
6
|
+
s.authors = ["Hubbado Devs"]
|
7
|
+
s.email = ["devs@hubbado.com"]
|
8
|
+
s.homepage = 'https://github.com/hubbado/hubbado-policy'
|
9
|
+
s.license = "MIT"
|
10
|
+
|
11
|
+
s.metadata["homepage_uri"] = s.homepage
|
12
|
+
s.metadata["source_code_uri"] = s.homepage
|
13
|
+
s.metadata["changelog_uri"] = "#{s.homepage}/blob/master/CHANGELOG.md"
|
14
|
+
|
15
|
+
s.require_paths = ["lib"]
|
16
|
+
s.files = Dir.glob(%w[
|
17
|
+
lib/**/*.rb
|
18
|
+
config/**/*.yml
|
19
|
+
*.gemspec
|
20
|
+
LICENSE*
|
21
|
+
README*
|
22
|
+
CHANGELOG*
|
23
|
+
])
|
24
|
+
s.platform = Gem::Platform::RUBY
|
25
|
+
s.required_ruby_version = ">= 3.2"
|
26
|
+
|
27
|
+
s.add_runtime_dependency "i18n"
|
28
|
+
s.add_runtime_dependency "evt-casing"
|
29
|
+
s.add_runtime_dependency "evt-record_invocation"
|
30
|
+
s.add_runtime_dependency "evt-template_method"
|
31
|
+
|
32
|
+
s.add_development_dependency "debug"
|
33
|
+
s.add_development_dependency "hubbado-style"
|
34
|
+
s.add_development_dependency "test_bench"
|
35
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Hubbado
|
2
|
+
module Policy
|
3
|
+
class Base
|
4
|
+
module PolicyDSL
|
5
|
+
def self.included(klass)
|
6
|
+
klass.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def define_policy(policy, *args, **kwargs, &block)
|
11
|
+
@policies ||= []
|
12
|
+
@policies << policy
|
13
|
+
|
14
|
+
# NOTE: This uses the technique described here so that the block given to
|
15
|
+
# define_policy can have return statements without causing LocalJumpError
|
16
|
+
# http://blog.jayfields.com/2007/03/ruby-localjumperror-workaround.html
|
17
|
+
define_method policy, &block
|
18
|
+
new_method = instance_method(policy)
|
19
|
+
define_method policy do |*args, **kwargs|
|
20
|
+
new_method.bind(self).call(*args, **kwargs) || denied
|
21
|
+
end
|
22
|
+
|
23
|
+
define_method "#{policy}?" do |*args, **kwargs|
|
24
|
+
send(policy, *args, **kwargs).permitted?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :policies
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
include PolicyDSL
|
33
|
+
|
34
|
+
attr_reader :user, :record
|
35
|
+
|
36
|
+
def self.build(user, record)
|
37
|
+
instance = new(user, record)
|
38
|
+
instance.configure
|
39
|
+
instance
|
40
|
+
end
|
41
|
+
|
42
|
+
# Define this in a subclass if there are dependencies to be configure
|
43
|
+
template_method :configure
|
44
|
+
|
45
|
+
def self.denied(reason = nil, data: nil)
|
46
|
+
reason ||= :denied
|
47
|
+
Result.new(false, reason, i18n_scope: i18n_scope, data: data)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.permitted
|
51
|
+
Result.new(true, :permitted, i18n_scope: i18n_scope)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.i18n_scope
|
55
|
+
@i18n_scope ||= Casing::Underscore::String.(name).gsub('/', '.')
|
56
|
+
end
|
57
|
+
|
58
|
+
def initialize(user, record)
|
59
|
+
raise "User not provided" unless user
|
60
|
+
|
61
|
+
@user = user
|
62
|
+
@record = record
|
63
|
+
end
|
64
|
+
|
65
|
+
def ==(other)
|
66
|
+
self.class == other.class && user == other.user && record == other.record
|
67
|
+
end
|
68
|
+
|
69
|
+
def denied(reason = nil, data: nil)
|
70
|
+
self.class.denied(reason, data: data)
|
71
|
+
end
|
72
|
+
|
73
|
+
def permitted
|
74
|
+
self.class.permitted
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Hubbado
|
2
|
+
module Policy
|
3
|
+
class Result
|
4
|
+
attr_reader :reason
|
5
|
+
attr_reader :data
|
6
|
+
|
7
|
+
def initialize(permitted, reason, i18n_scope: nil, data: nil)
|
8
|
+
data ||= {}
|
9
|
+
i18n_scope ||= "hubbado_policy"
|
10
|
+
|
11
|
+
@permitted = permitted
|
12
|
+
@reason = reason
|
13
|
+
@i18n_scope = i18n_scope
|
14
|
+
@data = data
|
15
|
+
end
|
16
|
+
|
17
|
+
def permitted?
|
18
|
+
!!@permitted
|
19
|
+
end
|
20
|
+
|
21
|
+
def denied?
|
22
|
+
!@permitted
|
23
|
+
end
|
24
|
+
|
25
|
+
def generic_deny?
|
26
|
+
@reason == :denied
|
27
|
+
end
|
28
|
+
|
29
|
+
def message
|
30
|
+
return if permitted?
|
31
|
+
return ::I18n.t('hubbado_policy.errors.denied') if generic_deny?
|
32
|
+
::I18n.t(@reason, scope: @i18n_scope)
|
33
|
+
end
|
34
|
+
|
35
|
+
def ==(other)
|
36
|
+
self.class == other.class && permitted? == other.permitted? && @reason == other.reason
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Hubbado
|
2
|
+
module Policy
|
3
|
+
class Scope
|
4
|
+
class << self
|
5
|
+
# Define this in a subclass
|
6
|
+
template_method :default_scope
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.call(record, scope = nil, **options)
|
10
|
+
build.(record, scope, **options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.build
|
14
|
+
instance = new
|
15
|
+
instance.configure
|
16
|
+
instance
|
17
|
+
end
|
18
|
+
|
19
|
+
# Define this in a subclass if there are dependencies to be configure
|
20
|
+
template_method :configure
|
21
|
+
|
22
|
+
def call(record, scope = nil, **options)
|
23
|
+
scope ||= self.class.default_scope
|
24
|
+
|
25
|
+
resolve(record, scope, **options)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Implement this in a subclass
|
29
|
+
template_method :resolve do |_record, _scope, **_options|
|
30
|
+
raise MethodMissing
|
31
|
+
end
|
32
|
+
|
33
|
+
module Substitute
|
34
|
+
include RecordInvocation
|
35
|
+
|
36
|
+
attr_writer :result
|
37
|
+
|
38
|
+
record def call(record, scope = nil, **options) = result
|
39
|
+
|
40
|
+
def result
|
41
|
+
@result ||= []
|
42
|
+
end
|
43
|
+
|
44
|
+
def scoped?(record, scope = nil, **options)
|
45
|
+
invoked?(:call, record: record, scope: scope, options: options)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "i18n"
|
2
|
+
require "casing"
|
3
|
+
require "record_invocation"
|
4
|
+
require "template_method"; TemplateMethod.activate
|
5
|
+
|
6
|
+
I18n.load_path += Dir[File.expand_path("config/locales") + "/*.yml"]
|
7
|
+
I18n.default_locale = :en
|
8
|
+
|
9
|
+
require "hubbado/policy/railtie" if defined?(Rails::Railtie)
|
10
|
+
|
11
|
+
require "hubbado/policy/scope"
|
12
|
+
require "hubbado/policy/result"
|
13
|
+
require "hubbado/policy/base"
|
metadata
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hubbado-policy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Hubbado Devs
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: i18n
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: evt-casing
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: evt-record_invocation
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: evt-template_method
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: debug
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: hubbado-style
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: test_bench
|
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'
|
110
|
+
email:
|
111
|
+
- devs@hubbado.com
|
112
|
+
executables: []
|
113
|
+
extensions: []
|
114
|
+
extra_rdoc_files: []
|
115
|
+
files:
|
116
|
+
- CHANGELOG.md
|
117
|
+
- LICENSE
|
118
|
+
- README.md
|
119
|
+
- config/locales/en.yml
|
120
|
+
- hubbado-policy.gemspec
|
121
|
+
- lib/hubbado-policy.rb
|
122
|
+
- lib/hubbado/policy/base.rb
|
123
|
+
- lib/hubbado/policy/railtie.rb
|
124
|
+
- lib/hubbado/policy/result.rb
|
125
|
+
- lib/hubbado/policy/scope.rb
|
126
|
+
homepage: https://github.com/hubbado/hubbado-policy
|
127
|
+
licenses:
|
128
|
+
- MIT
|
129
|
+
metadata:
|
130
|
+
homepage_uri: https://github.com/hubbado/hubbado-policy
|
131
|
+
source_code_uri: https://github.com/hubbado/hubbado-policy
|
132
|
+
changelog_uri: https://github.com/hubbado/hubbado-policy/blob/master/CHANGELOG.md
|
133
|
+
rdoc_options: []
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '3.2'
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubygems_version: 3.6.7
|
148
|
+
specification_version: 4
|
149
|
+
summary: A lightweight, flexible policy framework for Ruby applications
|
150
|
+
test_files: []
|