rabarber 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +65 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +254 -0
- data/Rakefile +12 -0
- data/lib/generators/rabarber/roles_generator.rb +19 -0
- data/lib/generators/rabarber/templates/create_rabarber_roles.rb.erb +17 -0
- data/lib/rabarber/access.rb +26 -0
- data/lib/rabarber/configuration.rb +43 -0
- data/lib/rabarber/controllers/concerns/authorization.rb +27 -0
- data/lib/rabarber/helpers/helpers.rb +17 -0
- data/lib/rabarber/models/concerns/has_roles.rb +47 -0
- data/lib/rabarber/models/role.rb +13 -0
- data/lib/rabarber/permissions.rb +36 -0
- data/lib/rabarber/rule.rb +41 -0
- data/lib/rabarber/version.rb +5 -0
- data/lib/rabarber.rb +26 -0
- metadata +76 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1af249a1b8bffcd7db310f4a5a187caebb692127d628a852c61004d63b43165d
|
4
|
+
data.tar.gz: ffa8eae28eaf5267d4b93dfe4a2d253b83ec113118b5ca6ff5ec66188e589537
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 55a56413aac6b5bc2a2f9fe7f07374bf05c96c9ed231ef2c04958405d7bdac40160731524f50311127593821054bdb46cb6abfbbe25a5a329d4937dc1164ede1
|
7
|
+
data.tar.gz: ab5e0f475366072ba485db587eea73d080e98e702ed45e1866a688c0f28fa3fb0246aaeddad9a911c09a70cac5aacd57eb039ed084cef6d38a67297143d81f5c
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-rails
|
3
|
+
- rubocop-rspec
|
4
|
+
|
5
|
+
AllCops:
|
6
|
+
TargetRubyVersion: 2.7
|
7
|
+
|
8
|
+
Layout/LineLength:
|
9
|
+
Max: 120
|
10
|
+
|
11
|
+
Metrics/BlockLength:
|
12
|
+
Enabled: false
|
13
|
+
|
14
|
+
Rails/ApplicationController:
|
15
|
+
Exclude:
|
16
|
+
- spec/support/controllers.rb
|
17
|
+
|
18
|
+
Rails/ApplicationRecord:
|
19
|
+
Exclude:
|
20
|
+
- lib/rabarber/models/role.rb
|
21
|
+
- spec/support/models.rb
|
22
|
+
|
23
|
+
RSpec/ContextWording:
|
24
|
+
Enabled: false
|
25
|
+
|
26
|
+
RSpec/ExampleLength:
|
27
|
+
Enabled: false
|
28
|
+
|
29
|
+
RSpec/FilePath:
|
30
|
+
Enabled: false
|
31
|
+
|
32
|
+
RSpec/MultipleExpectations:
|
33
|
+
Enabled: false
|
34
|
+
|
35
|
+
RSpec/NamedSubject:
|
36
|
+
Enabled: false
|
37
|
+
|
38
|
+
RSpec/NestedGroups:
|
39
|
+
Enabled: false
|
40
|
+
|
41
|
+
Style/ClassVars:
|
42
|
+
Enabled: false
|
43
|
+
|
44
|
+
Style/Documentation:
|
45
|
+
Enabled: false
|
46
|
+
|
47
|
+
Style/Lambda:
|
48
|
+
Enabled: true
|
49
|
+
EnforcedStyle: literal
|
50
|
+
|
51
|
+
Style/StringLiterals:
|
52
|
+
Enabled: true
|
53
|
+
EnforcedStyle: double_quotes
|
54
|
+
|
55
|
+
Style/StringLiteralsInInterpolation:
|
56
|
+
Enabled: true
|
57
|
+
EnforcedStyle: double_quotes
|
58
|
+
|
59
|
+
Style/SymbolArray:
|
60
|
+
Enabled: true
|
61
|
+
EnforcedStyle: brackets
|
62
|
+
|
63
|
+
Style/WordArray:
|
64
|
+
Enabled: true
|
65
|
+
EnforcedStyle: brackets
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gemspec
|
6
|
+
|
7
|
+
ENV["RAILS_VERSION"] ? gem("rails", ENV["RAILS_VERSION"]) : gem("rails", ">= 6.1")
|
8
|
+
|
9
|
+
gem "database_cleaner-active_record", "~> 2.1"
|
10
|
+
gem "rake", "~> 13.1"
|
11
|
+
gem "rspec", "~> 3.12"
|
12
|
+
gem "rspec-rails", "~> 6.1"
|
13
|
+
gem "rubocop", "~> 1.57"
|
14
|
+
gem "rubocop-rails", "~> 2.22"
|
15
|
+
gem "rubocop-rspec", "~> 2.25"
|
16
|
+
gem "sqlite3", "~> 1.6"
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 enjaku4
|
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,254 @@
|
|
1
|
+
# Rabarber: Simplified Authorization for Rails
|
2
|
+
|
3
|
+
Rabarber is an authorization library primarily designed for use in the web layer of your application, specifically in controllers and views.
|
4
|
+
|
5
|
+
Rabarber takes a slightly different approach compared to some popular libraries. Instead of answering, "Who can perform actions on this record?" it focuses on a question: "Who can access this endpoint?" In Rabarber, authorization is expressed not as "A user with role 'editor' can edit a post," but rather as "A user with the role 'editor' can access a post editing endpoint."
|
6
|
+
|
7
|
+
#### Example Usage:
|
8
|
+
|
9
|
+
Consider a CRM application where users in different roles have distinct access levels. For instance, an accountant role can interact with invoices and orders but cannot access marketing information, while the marketer role has access to marketing-related data.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add the Rabarber gem to your Gemfile:
|
14
|
+
|
15
|
+
```
|
16
|
+
gem "rabarber"
|
17
|
+
```
|
18
|
+
|
19
|
+
Install the gem:
|
20
|
+
|
21
|
+
```
|
22
|
+
bundle install
|
23
|
+
```
|
24
|
+
|
25
|
+
Next, generate a migration to create tables for storing roles in the database:
|
26
|
+
|
27
|
+
```
|
28
|
+
rails g rabarber:roles
|
29
|
+
```
|
30
|
+
|
31
|
+
Finally, run the migration to apply the changes to the database:
|
32
|
+
|
33
|
+
```
|
34
|
+
rails db:migrate
|
35
|
+
```
|
36
|
+
|
37
|
+
## Configuration
|
38
|
+
|
39
|
+
Seamlessly include Rabarber in your application by adding the following initializer:
|
40
|
+
|
41
|
+
```rb
|
42
|
+
Rabarber.configure do |config|
|
43
|
+
config.current_user_method = :authenticated_user
|
44
|
+
config.must_have_roles = true
|
45
|
+
config.when_unauthorized = ->(controller) {
|
46
|
+
controller.head 418
|
47
|
+
}
|
48
|
+
end
|
49
|
+
```
|
50
|
+
- `current_user_method` must be a symbol representing the method that returns the currently authenticated user. The default value is `:current_user`.
|
51
|
+
- `must_have_roles` must be a boolean, determining whether a user with no roles can access endpoints permitted to everyone. The default value is `false` (allowing users without roles to access endpoints permitted for everyone).
|
52
|
+
- `when_unauthorized` must be a lambda where you can define your actions when access is not authorized (`controller` is the instance of the controller where the code is executed). By default, the user is redirected back for HTML requests; otherwise, a 401 Unauthorized response is sent.
|
53
|
+
|
54
|
+
## Usage
|
55
|
+
|
56
|
+
Include the `Rabarber::HasRoles` module in your model representing application users:
|
57
|
+
|
58
|
+
```rb
|
59
|
+
class User < ApplicationRecord
|
60
|
+
include Rabarber::HasRoles
|
61
|
+
...
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
### This adds the following methods:
|
66
|
+
|
67
|
+
#### `#assign_roles`
|
68
|
+
|
69
|
+
To assign roles to the user, use:
|
70
|
+
|
71
|
+
```rb
|
72
|
+
user.assign_roles(:accountant, :marketer)
|
73
|
+
```
|
74
|
+
By default, the `#assign_roles` method will automatically create any roles that don't exist. If you want to assign only existing roles and prevent the creation of new ones, use the method with the `create_new: false` argument:
|
75
|
+
```rb
|
76
|
+
user.assign_roles(:accountant, :marketer, create_new: false)
|
77
|
+
```
|
78
|
+
|
79
|
+
#### `#revoke_roles`
|
80
|
+
|
81
|
+
To revoke roles from the user, use:
|
82
|
+
|
83
|
+
```rb
|
84
|
+
user.revoke_roles(:accountant, :marketer)
|
85
|
+
```
|
86
|
+
If any of the specified roles doesn't exist or the user doesn't have such a role, it will be ignored.
|
87
|
+
|
88
|
+
#### `#has_role?`
|
89
|
+
|
90
|
+
To check whether the user has a role, use:
|
91
|
+
|
92
|
+
```rb
|
93
|
+
user.has_role?(:accountant, :marketer)
|
94
|
+
```
|
95
|
+
|
96
|
+
It returns `true` if the user has at least one role and `false` otherwise.
|
97
|
+
|
98
|
+
#### `#roles`
|
99
|
+
|
100
|
+
View all roles assigned to the user:
|
101
|
+
|
102
|
+
```rb
|
103
|
+
user.roles
|
104
|
+
```
|
105
|
+
|
106
|
+
Utilize these methods to manipulate user roles. For example, create a custom UI for managing roles or assign necessary roles during migration or runtime (e.g., when the user is created). Adapt them to fit the requirements of your app.
|
107
|
+
|
108
|
+
If you need to list all the role names, use:
|
109
|
+
|
110
|
+
```rb
|
111
|
+
Rabarber::Role.names
|
112
|
+
```
|
113
|
+
|
114
|
+
---
|
115
|
+
|
116
|
+
### Authorization Rules
|
117
|
+
|
118
|
+
Include the `Rabarber::Authorization` module in the controller to which (specifically to it and its children) you want authorization rules to be applied. Typically, it is `ApplicationController`, but it can be any controller.
|
119
|
+
|
120
|
+
```rb
|
121
|
+
class ApplicationController < ActionController::Base
|
122
|
+
include Rabarber::Authorization
|
123
|
+
...
|
124
|
+
end
|
125
|
+
```
|
126
|
+
This adds the `.grant_access` method to the controller and its children. This method allows you to define the access rules.
|
127
|
+
|
128
|
+
The most basic usage of the method is as follows:
|
129
|
+
|
130
|
+
```rb
|
131
|
+
class InvoicesController < ApplicationController
|
132
|
+
grant_access action: :index, roles: [:accountant, :admin]
|
133
|
+
def index
|
134
|
+
...
|
135
|
+
end
|
136
|
+
|
137
|
+
grant_access action: :delete, roles: :admin
|
138
|
+
def delete
|
139
|
+
...
|
140
|
+
end
|
141
|
+
end
|
142
|
+
```
|
143
|
+
This grants access to the `index` action for users with the `accountant` or `admin` role, and access to the `delete` action for only `admin` users.
|
144
|
+
|
145
|
+
You can also define controller-wide rules (without the `action` argument):
|
146
|
+
|
147
|
+
```rb
|
148
|
+
class Crm::BaseController < ApplicationController
|
149
|
+
grant_access roles: :admin, :manager
|
150
|
+
|
151
|
+
grant_access action: :dashboard, roles: :marketer
|
152
|
+
def dashboard
|
153
|
+
...
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class InvoicesController < Crm::BaseControlle
|
158
|
+
grant_access roles: :accountant
|
159
|
+
def index
|
160
|
+
...
|
161
|
+
end
|
162
|
+
|
163
|
+
def delete
|
164
|
+
...
|
165
|
+
end
|
166
|
+
end
|
167
|
+
```
|
168
|
+
This means that `admin` and `manager` have access to all actions inside `Crm::BaseController` and its children, while the `accountant` role has access only to actions in `InvoicesController` and its possible children. Users with the `marketer` role can see only the dashboard in this example.
|
169
|
+
|
170
|
+
Roles (as well as actions) can be omitted:
|
171
|
+
|
172
|
+
```rb
|
173
|
+
class OrdersController < ApplicationController
|
174
|
+
grant_access
|
175
|
+
...
|
176
|
+
end
|
177
|
+
|
178
|
+
class InvoicesController < ApplicationController
|
179
|
+
grant_access action: :index
|
180
|
+
def index
|
181
|
+
...
|
182
|
+
end
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
This allows everyone to access `OrdersController` and its children and the `index` action in `InvoicesController`.
|
187
|
+
|
188
|
+
If you've set the `must_have_roles` setting to `true`, then only the users with at least one role can have access. This setting can be useful if your requirements are so that users without roles are not allowed to see anything.
|
189
|
+
|
190
|
+
For more complex rules, Rabarber provides the following:
|
191
|
+
|
192
|
+
```rb
|
193
|
+
class OrdersController < ApplicationController
|
194
|
+
grant_access if: :user_has_access?
|
195
|
+
...
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
def user_has_access?
|
200
|
+
...
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
class InvoicesController < ApplicationController
|
205
|
+
grant_access action: :index, roles: :accountant, if: -> { current_user.passed_probationary_period? }
|
206
|
+
def index
|
207
|
+
...
|
208
|
+
end
|
209
|
+
end
|
210
|
+
```
|
211
|
+
You can pass a custom rule as an `if` argument. It can be a symbol (the method with the same name will be called) or a lambda.
|
212
|
+
|
213
|
+
Rules defined in children don't override parent rules but rather add to them:
|
214
|
+
```rb
|
215
|
+
class Crm::BaseController < ApplicationController
|
216
|
+
grant_access roles: :admin
|
217
|
+
...
|
218
|
+
end
|
219
|
+
|
220
|
+
class InvoicesController < Crm::BaseControlle
|
221
|
+
grant_access roles: :accountant
|
222
|
+
...
|
223
|
+
end
|
224
|
+
```
|
225
|
+
This means that `InvoicesController` is still accessible to `admin` but is also accessible to `accountant`.
|
226
|
+
|
227
|
+
---
|
228
|
+
|
229
|
+
### View Helpers
|
230
|
+
|
231
|
+
Rabarber also provides a couple of helpers that can be used in views: `visible_to` and `hidden_from`. The usage is straightforward:
|
232
|
+
|
233
|
+
```erb
|
234
|
+
<%= visible_to(:admin, :manager) do %>
|
235
|
+
<p>Visible only to admins and managers</p>
|
236
|
+
<% end %>
|
237
|
+
```
|
238
|
+
|
239
|
+
```erb
|
240
|
+
<%= hidden_from(:accountant) do %>
|
241
|
+
<p>Accountant cannot see this</p>
|
242
|
+
<% end %>
|
243
|
+
```
|
244
|
+
|
245
|
+
## Problems?
|
246
|
+
|
247
|
+
Encountered a bug or facing a problem?
|
248
|
+
|
249
|
+
- **Create an Issue**: If you've identified a problem or have a feature request, please create an issue on the gem's GitHub repository. Be sure to provide detailed information about the problem, including steps to reproduce it.
|
250
|
+
- **Contribute a Solution**: Found a fix for the issue or want to contribute to the project? Feel free to create a pull request with your changes.
|
251
|
+
|
252
|
+
## License
|
253
|
+
|
254
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/migration"
|
4
|
+
|
5
|
+
module Rabarber
|
6
|
+
class RolesGenerator < Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
10
|
+
|
11
|
+
def create_migrations
|
12
|
+
migration_template "create_rabarber_roles.rb.erb", "db/migrate/create_rabarber_roles.rb"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.next_migration_number(_path)
|
16
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateRabarberRoles < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version.to_s %>]
|
4
|
+
def change
|
5
|
+
create_table :rabarber_roles do |t|
|
6
|
+
t.string :name, null: false, index: { unique: true }
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
create_table :rabarber_roles_roleables, id: false do |t|
|
11
|
+
t.belongs_to :role, index: true
|
12
|
+
t.belongs_to :roleable, index: true
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index :rabarber_roles_roleables, %i[role_id roleable_id], unique: true
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Access
|
5
|
+
def access_granted?(roles, controller, action, custom_rule_receiver)
|
6
|
+
controller_accessible?(roles, controller, custom_rule_receiver) ||
|
7
|
+
action_accessible?(roles, controller, action, custom_rule_receiver)
|
8
|
+
end
|
9
|
+
|
10
|
+
def controller_accessible?(roles, controller, custom_rule_receiver)
|
11
|
+
accessible_controllers(roles, custom_rule_receiver).any? do |accessible_controller|
|
12
|
+
controller <= accessible_controller
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def action_accessible?(roles, controller, action, custom_rule_receiver)
|
17
|
+
action_rules[controller].any? { |rule| rule.verify_access(roles, custom_rule_receiver, action) }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def accessible_controllers(roles, custom_rule_receiver)
|
23
|
+
controller_rules.select { |_, rule| rule.verify_access(roles, custom_rule_receiver) }.keys
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
|
5
|
+
module Rabarber
|
6
|
+
class Configuration
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
attr_reader :current_user_method, :must_have_roles, :when_unauthorized
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@current_user_method = :current_user
|
13
|
+
@must_have_roles = false
|
14
|
+
@when_unauthorized = ->(controller) do
|
15
|
+
if controller.request.format.html?
|
16
|
+
controller.redirect_back fallback_location: controller.main_app.root_path
|
17
|
+
else
|
18
|
+
controller.head(:unauthorized)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def current_user_method=(method_name)
|
24
|
+
unless method_name.is_a?(Symbol) || method_name.is_a?(String)
|
25
|
+
raise ArgumentError, "Method name must be a symbol or a string"
|
26
|
+
end
|
27
|
+
|
28
|
+
@current_user_method = method_name.to_sym
|
29
|
+
end
|
30
|
+
|
31
|
+
def must_have_roles=(value)
|
32
|
+
raise ArgumentError, "Value must be a boolean" unless [true, false].include?(value)
|
33
|
+
|
34
|
+
@must_have_roles = value
|
35
|
+
end
|
36
|
+
|
37
|
+
def when_unauthorized=(callable)
|
38
|
+
raise ArgumentError, "Proc is expected" unless callable.is_a?(Proc)
|
39
|
+
|
40
|
+
@when_unauthorized = callable
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Authorization
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_action :check_permissions
|
9
|
+
end
|
10
|
+
|
11
|
+
class_methods do
|
12
|
+
def grant_access(action: nil, roles: nil, if: nil)
|
13
|
+
Permissions.write(self, action, roles, binding.local_variable_get(:if))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def check_permissions
|
20
|
+
return if Permissions.access_granted?(
|
21
|
+
send(::Rabarber::Configuration.instance.current_user_method).roles.names, self.class, action_name.to_sym, self
|
22
|
+
)
|
23
|
+
|
24
|
+
::Rabarber::Configuration.instance.when_unauthorized.call(self)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Helpers
|
5
|
+
def visible_to(*roles, &block)
|
6
|
+
return unless send(::Rabarber::Configuration.instance.current_user_method).has_role?(*roles)
|
7
|
+
|
8
|
+
capture(&block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def hidden_from(*roles, &block)
|
12
|
+
return if send(::Rabarber::Configuration.instance.current_user_method).has_role?(*roles)
|
13
|
+
|
14
|
+
capture(&block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module HasRoles
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
raise Error, "Rabarber::HasRoles can only be included once" if defined?(@@included) && @@included
|
9
|
+
|
10
|
+
@@included = true
|
11
|
+
|
12
|
+
has_and_belongs_to_many :roles, class_name: "Rabarber::Role",
|
13
|
+
foreign_key: "roleable_id",
|
14
|
+
join_table: "rabarber_roles_roleables"
|
15
|
+
|
16
|
+
alias_method :has_role?, :role?
|
17
|
+
end
|
18
|
+
|
19
|
+
def role?(*role_names)
|
20
|
+
unless role_names.all? { |arg| arg.is_a?(Symbol) || arg.is_a?(String) }
|
21
|
+
raise(ArgumentError, "Role names must be symbols or strings")
|
22
|
+
end
|
23
|
+
|
24
|
+
roles.exists?(name: role_names)
|
25
|
+
end
|
26
|
+
|
27
|
+
def assign_roles(*role_names, create_new: true)
|
28
|
+
unless role_names.all? { |arg| arg.is_a?(Symbol) || arg.is_a?(String) }
|
29
|
+
raise(ArgumentError, "Role names must be symbols or strings")
|
30
|
+
end
|
31
|
+
|
32
|
+
if create_new && (new_roles = role_names - Role.names).any?
|
33
|
+
new_roles.each { |role| Role.create!(name: role) }
|
34
|
+
end
|
35
|
+
|
36
|
+
roles << Role.where(name: role_names) - roles
|
37
|
+
end
|
38
|
+
|
39
|
+
def revoke_roles(*role_names)
|
40
|
+
unless role_names.all? { |arg| arg.is_a?(Symbol) || arg.is_a?(String) }
|
41
|
+
raise(ArgumentError, "Role names must be symbols or strings")
|
42
|
+
end
|
43
|
+
|
44
|
+
self.roles = roles - Role.where(name: role_names)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "access"
|
4
|
+
require_relative "rule"
|
5
|
+
|
6
|
+
module Rabarber
|
7
|
+
class Permissions
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
extend Access
|
11
|
+
|
12
|
+
attr_reader :storage
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@storage = { controller_rules: Hash.new({}), action_rules: Hash.new([]) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.write(controller, action, roles, custom_rule)
|
19
|
+
rule = Rule.new(action, roles, custom_rule)
|
20
|
+
|
21
|
+
if action
|
22
|
+
instance.storage[:action_rules][controller] += [rule]
|
23
|
+
else
|
24
|
+
instance.storage[:controller_rules][controller] = rule
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.controller_rules
|
29
|
+
instance.storage[:controller_rules]
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.action_rules
|
33
|
+
instance.storage[:action_rules]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
class Rule
|
5
|
+
attr_reader :action, :roles, :custom
|
6
|
+
|
7
|
+
def initialize(action, roles, custom)
|
8
|
+
@action = action
|
9
|
+
@roles = Array(roles)
|
10
|
+
@custom = custom
|
11
|
+
end
|
12
|
+
|
13
|
+
def verify_access(user_roles, custom_rule_receiver, action_name = nil)
|
14
|
+
action_accessible?(action_name) && roles_permitted?(user_roles) && custom_rule_followed?(custom_rule_receiver)
|
15
|
+
end
|
16
|
+
|
17
|
+
def action_accessible?(action_name)
|
18
|
+
action_name.nil? || action_name == action
|
19
|
+
end
|
20
|
+
|
21
|
+
def roles_permitted?(user_roles)
|
22
|
+
return false if ::Rabarber::Configuration.instance.must_have_roles && user_roles.empty?
|
23
|
+
|
24
|
+
roles.empty? || (roles & user_roles).any?
|
25
|
+
end
|
26
|
+
|
27
|
+
def custom_rule_followed?(custom_rule_receiver)
|
28
|
+
custom.nil? || execute_custom_rule(custom_rule_receiver)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def execute_custom_rule(custom_rule_receiver)
|
34
|
+
if custom.is_a?(Proc)
|
35
|
+
custom_rule_receiver.instance_exec(&custom)
|
36
|
+
else
|
37
|
+
custom_rule_receiver.send(custom)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/rabarber.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rabarber/version"
|
4
|
+
require_relative "rabarber/configuration"
|
5
|
+
|
6
|
+
require "active_record"
|
7
|
+
require "active_support"
|
8
|
+
|
9
|
+
require_relative "rabarber/controllers/concerns/authorization"
|
10
|
+
|
11
|
+
require_relative "rabarber/helpers/helpers"
|
12
|
+
|
13
|
+
require_relative "rabarber/models/role"
|
14
|
+
require_relative "rabarber/models/concerns/has_roles"
|
15
|
+
|
16
|
+
require_relative "rabarber/permissions"
|
17
|
+
|
18
|
+
module Rabarber
|
19
|
+
module_function
|
20
|
+
|
21
|
+
def configure
|
22
|
+
yield(Configuration.instance)
|
23
|
+
end
|
24
|
+
|
25
|
+
class Error < StandardError; end
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rabarber
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- enjaku4
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-12-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.1'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- enjaku4@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".rspec"
|
35
|
+
- ".rubocop.yml"
|
36
|
+
- CHANGELOG.md
|
37
|
+
- Gemfile
|
38
|
+
- LICENSE.txt
|
39
|
+
- README.md
|
40
|
+
- Rakefile
|
41
|
+
- lib/generators/rabarber/roles_generator.rb
|
42
|
+
- lib/generators/rabarber/templates/create_rabarber_roles.rb.erb
|
43
|
+
- lib/rabarber.rb
|
44
|
+
- lib/rabarber/access.rb
|
45
|
+
- lib/rabarber/configuration.rb
|
46
|
+
- lib/rabarber/controllers/concerns/authorization.rb
|
47
|
+
- lib/rabarber/helpers/helpers.rb
|
48
|
+
- lib/rabarber/models/concerns/has_roles.rb
|
49
|
+
- lib/rabarber/models/role.rb
|
50
|
+
- lib/rabarber/permissions.rb
|
51
|
+
- lib/rabarber/rule.rb
|
52
|
+
- lib/rabarber/version.rb
|
53
|
+
homepage: https://github.com/enjaku4/rabarber
|
54
|
+
licenses:
|
55
|
+
- MIT
|
56
|
+
metadata: {}
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '2.7'
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubygems_version: 3.1.6
|
73
|
+
signing_key:
|
74
|
+
specification_version: 4
|
75
|
+
summary: Simple authorization library for Ruby on Rails.
|
76
|
+
test_files: []
|