authorizy 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 +5 -0
- data/LICENSE +21 -0
- data/README.md +190 -0
- data/lib/authorizy.rb +19 -0
- data/lib/authorizy/base_cop.rb +21 -0
- data/lib/authorizy/config.rb +15 -0
- data/lib/authorizy/core.rb +43 -0
- data/lib/authorizy/expander.rb +61 -0
- data/lib/authorizy/extension.rb +31 -0
- data/lib/authorizy/version.rb +5 -0
- data/lib/generators/authorizy/install_generator.rb +23 -0
- data/lib/generators/authorizy/templates/config/initializers/authorizy.rb +23 -0
- data/lib/generators/authorizy/templates/db/migrate/add_authorizy_on_users.rb +7 -0
- data/spec/authorizy/base_cop/access_question_spec.rb +9 -0
- data/spec/authorizy/config/aliases_spec.rb +13 -0
- data/spec/authorizy/config/cop_spec.rb +13 -0
- data/spec/authorizy/config/current_user_spec.rb +31 -0
- data/spec/authorizy/config/dependencies_spec.rb +13 -0
- data/spec/authorizy/config/initialize_spec.rb +7 -0
- data/spec/authorizy/config/redirect_url_spec.rb +31 -0
- data/spec/authorizy/cop/controller_spec.rb +42 -0
- data/spec/authorizy/cop/model_spec.rb +15 -0
- data/spec/authorizy/cop/namespaced_controller_spec.rb +42 -0
- data/spec/authorizy/core/access_spec.rb +137 -0
- data/spec/authorizy/expander/expand_spec.rb +144 -0
- data/spec/authorizy/extension/authorizy_question_spec.rb +46 -0
- data/spec/authorizy/extension/authorizy_spec.rb +56 -0
- data/spec/common_helper.rb +11 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/application.rb +8 -0
- data/spec/support/common.rb +13 -0
- data/spec/support/controllers/admin/dummy_controller.rb +13 -0
- data/spec/support/controllers/dummy_controller.rb +11 -0
- data/spec/support/coverage.rb +14 -0
- data/spec/support/i18n.rb +3 -0
- data/spec/support/locales/en.yml +3 -0
- data/spec/support/models/authorizy_cop.rb +31 -0
- data/spec/support/models/empty_cop.rb +4 -0
- data/spec/support/models/user.rb +4 -0
- data/spec/support/routes.rb +6 -0
- data/spec/support/schema.rb +22 -0
- metadata +198 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d9be49c0763122d6a892671c240a9211720a664a62d66ee457c31203a436c0f8
|
4
|
+
data.tar.gz: 8c6806a8c63b06c3f750cba5ed61ce512e8520b79d20273711cfdac1b1345188
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4aecc0fc9dfb238e9a0b4948bf49e424ae3bdd1ce9d8b5942c8ae66605c259b0f2f80b0800dcf6c383248a76a6a47ead49f42ff4f746523875eb8fb8e3c2a628
|
7
|
+
data.tar.gz: 62386459e46f79d0d17a21e4229fd69c4915eb762f115adf2a76323590f0c471ef325bfa9f8c44ab6283a20e32dd196692da4958fd9a1f436255baafa83d1248
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Washington Botelho
|
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,190 @@
|
|
1
|
+
# Authorizy
|
2
|
+
|
3
|
+
[](https://github.com/wbotelhos/authorizy/actions)
|
4
|
+
[](https://badge.fury.io/rb/authorizy)
|
5
|
+
[](https://codeclimate.com/github/wbotelhos/authorizy/maintainability)
|
6
|
+
[](https://codecov.io/gh/wbotelhos/authorizy)
|
7
|
+
[](https://www.patreon.com/wbotelhos)
|
8
|
+
|
9
|
+
A JSON based Authorization.
|
10
|
+
|
11
|
+
##### Why not [cancancan](https://github.com/CanCanCommunity/cancancan)?
|
12
|
+
|
13
|
+
I have been working with cancan/cancancan for years. Since the beginning with [database access](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Abilities-in-Database.md). After a while, I realised I built a couple of abstractions around `ability` class and suddenly migrated to JSON for better performance. As I need a full role admin I decided to start to extract this logic to a gem.
|
14
|
+
|
15
|
+
## Install
|
16
|
+
|
17
|
+
Add the following code on your `Gemfile` and run `bundle install`:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'authorizy'
|
21
|
+
```
|
22
|
+
|
23
|
+
Run the following task to create Authorizy migration and initialize.
|
24
|
+
|
25
|
+
```sh
|
26
|
+
rails g rating:install
|
27
|
+
```
|
28
|
+
|
29
|
+
Then execute the migration to adds the column `authorizy` to your `users` table.
|
30
|
+
|
31
|
+
```sh
|
32
|
+
rake db:migrate
|
33
|
+
```
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
class ApplicationController < ActionController::Base
|
39
|
+
include Authorizy::Extension
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
Add the `authorizy` filter on the controller you want enables authorization.
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
class UserController < ApplicationController
|
47
|
+
before_action :authorizy
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
## JSON
|
52
|
+
|
53
|
+
The column `authorizy` is a JSON column that has a key called `permission` with a list of permissions identified by the controller and action name which the user can access.
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
{
|
57
|
+
permissions: [
|
58
|
+
{ controller: :user, action: :create },
|
59
|
+
{ controller: :user, action: :update },
|
60
|
+
}
|
61
|
+
}
|
62
|
+
```
|
63
|
+
|
64
|
+
## Configuration
|
65
|
+
|
66
|
+
You can change the default configuration.
|
67
|
+
|
68
|
+
### Aliases
|
69
|
+
|
70
|
+
Alias is an action that maps another action. We have some defaults.
|
71
|
+
|
72
|
+
|Action|alias |
|
73
|
+
|------|------|
|
74
|
+
|create|new |
|
75
|
+
|edit |update|
|
76
|
+
|new |create|
|
77
|
+
|update|edit |
|
78
|
+
|
79
|
+
You can add more alias, for example, all permissions for action `index` will allow access to action `gridy` of the same controller. So `users#index` will allow `users#gridy` too.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
Authorizy.configure do |config|
|
83
|
+
config.aliases = { index: :gridy }
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
### Dependencies
|
88
|
+
|
89
|
+
You can allow access to one or more controllers and actions based on your permissions. It'll consider not only the `action`, like [aliases](#aliases) but the controller either.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
Authorizy.configure do |config|
|
93
|
+
config.dependencies = {
|
94
|
+
payments: {
|
95
|
+
index: [
|
96
|
+
{ controller: :users, action: :index },
|
97
|
+
{ controller: :enrollments, action: :index },
|
98
|
+
]
|
99
|
+
}
|
100
|
+
}
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
So now if a have the permission `payments#index` I'll receive more two permissions: `users#index` and `enrollments#index`.
|
105
|
+
|
106
|
+
### Cop
|
107
|
+
|
108
|
+
Sometimes we need to allow access in runtime because the permission will depend on the request data and/or some dynamic logic. For this you can create a *Cop* class, the inherit from `Authorizy::BaseCop`, to allow it based on logic. It works like a [Interceptor](https://en.wikipedia.org/wiki/Interceptor_pattern).
|
109
|
+
|
110
|
+
First, you need to configure your cop:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
Authorizy.configure do |config|
|
114
|
+
config.cop = AuthorizyCop
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
Now creates the cop class. The following example will intercept all access to the controller `users_controller`:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
class AuthorizyCop < Authorizy::BaseCop
|
122
|
+
def users
|
123
|
+
return false if action == 'create'
|
124
|
+
return false if controller == 'users'
|
125
|
+
return true if current_user == User.find_by(admin: true)
|
126
|
+
return true if params[:allow] == 'true'
|
127
|
+
return true if session[:logged] == 'true'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
As you can see, you have access to a couple of variables: `action`, `controller`, `current_user`, `params`, and `session`.
|
133
|
+
|
134
|
+
If your controller has a namespace, just use `__` to separate the modules name:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
class AuthorizyCop < Authorizy::BaseCop
|
138
|
+
def admin__users
|
139
|
+
end
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
### Current User
|
144
|
+
|
145
|
+
By default Authorizy fetch the current user from the variable `current_user`. You have a config, that receives the controller context, where you can change it:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
Authorizy.configure do |config|
|
149
|
+
config.current_user -> (context) { context.current_person }
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
### Redirect URL
|
154
|
+
|
155
|
+
When authorization fails and the request is not a XHR request a redirect happens to `/` path. You can change it:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
Authorizy.configure do |config|
|
159
|
+
config.redirect_url -> (context) { context.new_session_url }
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
# Helper
|
164
|
+
|
165
|
+
You can use `authorizy?` method to check if `current_user` has access to some `controller` and `action`.
|
166
|
+
|
167
|
+
Using on controller:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
class UserController < ApplicationController
|
171
|
+
before_action :assign_events, if: -> { authorizy?('system/events', 'index') }
|
172
|
+
|
173
|
+
def assign_events
|
174
|
+
end
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
Using on view:
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
<% if authorizy?(:users, :create) %>
|
182
|
+
<a href="/users/new">New User</a>
|
183
|
+
<% end %>
|
184
|
+
```
|
185
|
+
|
186
|
+
Using on jBuilder view:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
json.create_link new_users_url if authorizy?(:users, :create)
|
190
|
+
```
|
data/lib/authorizy.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authorizy
|
4
|
+
require 'authorizy/base_cop'
|
5
|
+
require 'authorizy/config'
|
6
|
+
require 'authorizy/core'
|
7
|
+
require 'authorizy/expander'
|
8
|
+
require 'authorizy/extension'
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def config
|
12
|
+
@config ||= Authorizy::Config.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def configure
|
16
|
+
yield(config)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authorizy
|
4
|
+
class BaseCop
|
5
|
+
def initialize(current_user, params, session, controller, action)
|
6
|
+
@action = action
|
7
|
+
@controller = controller
|
8
|
+
@current_user = current_user
|
9
|
+
@params = params
|
10
|
+
@session = session
|
11
|
+
end
|
12
|
+
|
13
|
+
def access?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
attr_reader :action, :controller, :current_user, :params, :session
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authorizy
|
4
|
+
class Config
|
5
|
+
attr_accessor :aliases, :dependencies, :cop, :current_user, :redirect_url
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@aliases = {}
|
9
|
+
@cop = Authorizy::BaseCop
|
10
|
+
@current_user = -> (context) { context.respond_to?(:current_user) ? context.current_user : nil }
|
11
|
+
@dependencies = {}
|
12
|
+
@redirect_url = -> (context) { context.respond_to?(:root_url) ? context.root_url : '/' }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authorizy
|
4
|
+
class Core
|
5
|
+
def initialize(current_user, params, session, controller: params['controller'], action: params['action'])
|
6
|
+
@action = action.to_s
|
7
|
+
@controller = controller.to_s
|
8
|
+
@current_user = current_user
|
9
|
+
@params = params
|
10
|
+
@session = session
|
11
|
+
end
|
12
|
+
|
13
|
+
def access?
|
14
|
+
return false if @current_user.blank?
|
15
|
+
|
16
|
+
granted = permissions.any? do |item|
|
17
|
+
data = item.stringify_keys
|
18
|
+
|
19
|
+
data['controller'].to_s == @controller && data['action'].to_s == @action
|
20
|
+
end
|
21
|
+
|
22
|
+
return true if granted
|
23
|
+
|
24
|
+
cop.respond_to?(cop_controller) && cop.public_send(cop_controller)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def cop
|
30
|
+
Authorizy.config.cop.new(@current_user, @params, @session, @controller, @action)
|
31
|
+
end
|
32
|
+
|
33
|
+
def cop_controller
|
34
|
+
@controller.sub('/', '__')
|
35
|
+
end
|
36
|
+
|
37
|
+
def permissions
|
38
|
+
Authorizy::Expander.new.expand(
|
39
|
+
[@session['permissions']].flatten.compact.presence || @current_user.authorizy.try(:[], 'permissions')
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authorizy
|
4
|
+
class Expander
|
5
|
+
def expand(permissions)
|
6
|
+
return [] if permissions.blank?
|
7
|
+
|
8
|
+
result = {}
|
9
|
+
|
10
|
+
permissions.each do |permission|
|
11
|
+
item = permission.stringify_keys.transform_values(&:to_s)
|
12
|
+
|
13
|
+
result[key_for(item)] = item
|
14
|
+
|
15
|
+
if (items = controller_dependency(item))
|
16
|
+
items.each { |data| result[key_for(data)] = data }
|
17
|
+
end
|
18
|
+
|
19
|
+
actions = [default_aliases[item['action']]].flatten.compact
|
20
|
+
|
21
|
+
next if actions.blank?
|
22
|
+
|
23
|
+
actions.each do |action|
|
24
|
+
result[key_for(item, action: action)] = { 'action' => action.to_s, 'controller' => item['controller'].to_s }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
result.values
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def aliases
|
34
|
+
Authorizy.config.aliases.stringify_keys
|
35
|
+
end
|
36
|
+
|
37
|
+
def controller_dependency(item)
|
38
|
+
return if (actions = dependencies[item['controller']]).blank?
|
39
|
+
return if (permissions = actions[item['action']]).blank?
|
40
|
+
|
41
|
+
permissions.map { |permission| permission.transform_values(&:to_s) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def default_aliases
|
45
|
+
{
|
46
|
+
'create' => 'new',
|
47
|
+
'edit' => 'update',
|
48
|
+
'new' => 'create',
|
49
|
+
'update' => 'edit',
|
50
|
+
}.merge(aliases)
|
51
|
+
end
|
52
|
+
|
53
|
+
def dependencies
|
54
|
+
Authorizy.config.dependencies.deep_stringify_keys
|
55
|
+
end
|
56
|
+
|
57
|
+
def key_for(item, action: nil)
|
58
|
+
"#{item['controller']}##{action || item['action']}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authorizy
|
4
|
+
module Extension
|
5
|
+
extend ::ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
helper_method(:authorizy?)
|
9
|
+
|
10
|
+
def authorizy
|
11
|
+
return if Authorizy::Core.new(authorizy_user, params, session).access?
|
12
|
+
|
13
|
+
info = I18n.t('authorizy.denied', action: params[:action], controller: params[:controller])
|
14
|
+
|
15
|
+
return render(json: { message: info }, status: 422) if request.xhr?
|
16
|
+
|
17
|
+
redirect_to Authorizy.config.redirect_url.call(self), info: info
|
18
|
+
end
|
19
|
+
|
20
|
+
def authorizy?(controller, action)
|
21
|
+
Authorizy::Core.new(authorizy_user, params, session, action: action, controller: controller).access?
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def authorizy_user
|
27
|
+
Authorizy.config.current_user.call(self)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authorizy
|
4
|
+
class InstallGenerator < Rails::Generators::Base
|
5
|
+
source_root File.expand_path('templates', __dir__)
|
6
|
+
|
7
|
+
desc 'Creates Initializer and Migration for Authorizy'
|
8
|
+
|
9
|
+
def create_initializer
|
10
|
+
copy_file 'config/initializers/authorizy.rb', 'config/initializers/authorizy.rb'
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_migration
|
14
|
+
copy_file 'db/migrate/add_authorizy_on_users.rb', "db/migrate/#{timestamp(0)}_add_authorizy_on_users.rb"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def timestamp(seconds)
|
20
|
+
(Time.current + seconds.seconds).strftime('%Y%m%d%H%M%S')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|