authorizy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE +21 -0
  4. data/README.md +190 -0
  5. data/lib/authorizy.rb +19 -0
  6. data/lib/authorizy/base_cop.rb +21 -0
  7. data/lib/authorizy/config.rb +15 -0
  8. data/lib/authorizy/core.rb +43 -0
  9. data/lib/authorizy/expander.rb +61 -0
  10. data/lib/authorizy/extension.rb +31 -0
  11. data/lib/authorizy/version.rb +5 -0
  12. data/lib/generators/authorizy/install_generator.rb +23 -0
  13. data/lib/generators/authorizy/templates/config/initializers/authorizy.rb +23 -0
  14. data/lib/generators/authorizy/templates/db/migrate/add_authorizy_on_users.rb +7 -0
  15. data/spec/authorizy/base_cop/access_question_spec.rb +9 -0
  16. data/spec/authorizy/config/aliases_spec.rb +13 -0
  17. data/spec/authorizy/config/cop_spec.rb +13 -0
  18. data/spec/authorizy/config/current_user_spec.rb +31 -0
  19. data/spec/authorizy/config/dependencies_spec.rb +13 -0
  20. data/spec/authorizy/config/initialize_spec.rb +7 -0
  21. data/spec/authorizy/config/redirect_url_spec.rb +31 -0
  22. data/spec/authorizy/cop/controller_spec.rb +42 -0
  23. data/spec/authorizy/cop/model_spec.rb +15 -0
  24. data/spec/authorizy/cop/namespaced_controller_spec.rb +42 -0
  25. data/spec/authorizy/core/access_spec.rb +137 -0
  26. data/spec/authorizy/expander/expand_spec.rb +144 -0
  27. data/spec/authorizy/extension/authorizy_question_spec.rb +46 -0
  28. data/spec/authorizy/extension/authorizy_spec.rb +56 -0
  29. data/spec/common_helper.rb +11 -0
  30. data/spec/spec_helper.rb +29 -0
  31. data/spec/support/application.rb +8 -0
  32. data/spec/support/common.rb +13 -0
  33. data/spec/support/controllers/admin/dummy_controller.rb +13 -0
  34. data/spec/support/controllers/dummy_controller.rb +11 -0
  35. data/spec/support/coverage.rb +14 -0
  36. data/spec/support/i18n.rb +3 -0
  37. data/spec/support/locales/en.yml +3 -0
  38. data/spec/support/models/authorizy_cop.rb +31 -0
  39. data/spec/support/models/empty_cop.rb +4 -0
  40. data/spec/support/models/user.rb +4 -0
  41. data/spec/support/routes.rb +6 -0
  42. data/spec/support/schema.rb +22 -0
  43. metadata +198 -0
@@ -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
@@ -0,0 +1,5 @@
1
+ # v0.1.0
2
+
3
+ ## Features
4
+
5
+ - Enables permission control via JSON data;
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.
@@ -0,0 +1,190 @@
1
+ # Authorizy
2
+
3
+ [![CI](https://github.com/wbotelhos/authorizy/workflows/CI/badge.svg)](https://github.com/wbotelhos/authorizy/actions)
4
+ [![Gem Version](https://badge.fury.io/rb/authorizy.svg)](https://badge.fury.io/rb/authorizy)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/f312587b4f126bb13e85/maintainability)](https://codeclimate.com/github/wbotelhos/authorizy/maintainability)
6
+ [![Coverage](https://codecov.io/gh/wbotelhos/blogy/branch/master/graph/badge.svg?token=PENDING)](https://codecov.io/gh/wbotelhos/authorizy)
7
+ [![Sponsor](https://img.shields.io/badge/donate-%3C3-brightgreen.svg)](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
+ ```
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authorizy
4
+ VERSION = '0.1.0'
5
+ 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