action_sentinel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fa3b8401bf44885aec2145a79689ff7f38b02dac73c69922fbc22c0fdbd7b307
4
+ data.tar.gz: 6e4b635a86277548e76d18b6f4f73e5aa73619f5941923e2cfd5433acf7e526f
5
+ SHA512:
6
+ metadata.gz: b19c9599910bce3915839ee7342090bfc66b5dbe46b8cbb11f6e96da2c55d0efed6a64e2bdae8b20d99b7e256f2bae3a2bf27aeb4b55b4315294a7a98f95d974
7
+ data.tar.gz: 22dd485c1ba4af40cdd4cd6076a98e267515510c6eb63f3c13e68c4567fce329d59dc940679170d644ef71bc4d3574a54925390c396aee1e258efbf1fc6051c6
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,32 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+ Exclude:
4
+ - "lib/generators/**/templates/*"
5
+ NewCops: disable
6
+
7
+ Style/Documentation:
8
+ Enabled: true
9
+ Exclude:
10
+ - "lib/generators/action_sentinel/**"
11
+ - "spec/support/*"
12
+
13
+ Style/StringLiterals:
14
+ Enabled: true
15
+ EnforcedStyle: double_quotes
16
+
17
+ Style/StringLiteralsInInterpolation:
18
+ Enabled: true
19
+ EnforcedStyle: double_quotes
20
+
21
+ Layout/LineLength:
22
+ Max: 120
23
+
24
+ Metrics/BlockLength:
25
+ Enabled: true
26
+ Exclude:
27
+ - "spec/**/*"
28
+
29
+ Naming/HeredocDelimiterNaming:
30
+ Enabled: true
31
+ Exclude:
32
+ - "action_sentinel.gemspec"
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --exclude lib/generators/action_sentinel/templates/
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-12-04
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in action_sentinel.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Denis Stael
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,170 @@
1
+ # ActionSentinel
2
+
3
+ Simple authorization of controller actions based on model-level permissions.
4
+
5
+ This gem enables access authorization control, based on the access permission settings for each controller and its actions, at the model level.
6
+
7
+ ## Installation
8
+
9
+ ### 1. Add the gem into your project
10
+
11
+ Add this to your Gemfile and run `bundle install`.
12
+
13
+ ```ruby
14
+ gem 'action_sentinel'
15
+ ```
16
+
17
+ Or install it yourself as:
18
+ ```sh
19
+ gem install action_sentinel
20
+ ```
21
+
22
+ ### 2. Generate AccessPermission Model
23
+
24
+ Use the ActionSentinel generator to create the AccessPermission model and its relationship, which AccessPermission will belong to. You must specify the name of the model that will have access permissions. For example, if you want to set permissions to a User model, you can use:
25
+
26
+ ```
27
+ rails g action_sentinel:access_permission User
28
+ ```
29
+
30
+ If your database uses UUID for the primary and foreign keys, you can pass the `--uuid` option:
31
+
32
+ ```
33
+ rails g action_sentinel:access_permission User --uuid
34
+ ```
35
+
36
+ The generator will create the AccessPermission model and a migration, and insert into your User class the method `action_permissible`, which includes the new methods and associates the model with the access permissions:
37
+
38
+ ```ruby
39
+ # AccessPermission model
40
+ class AccessPermission < ApplicationRecord
41
+ belongs_to :user
42
+
43
+ validates :controller_name, uniqueness: { scope: :group_id }
44
+ end
45
+
46
+ # User model with permissions added
47
+ class User < ApplicationRecord
48
+ action_permissible
49
+ end
50
+
51
+ ```
52
+
53
+ ### 3. Run the migration
54
+
55
+ ```
56
+ rails db:migrate
57
+ ```
58
+
59
+ ### 4. Include authorization in ApplicationController
60
+
61
+ Include `ActionSentinel::Authorization` into your application controller:
62
+
63
+ ```ruby
64
+ class ApplicationController < ActionController::Base
65
+ include ActionSentinel::Authorization
66
+ end
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ ### Adding permissions
72
+
73
+ It is possible to add one or more permissions to access a controller, calling:
74
+
75
+ ```ruby
76
+ # Adding permission to access show action in UsersController
77
+ user.add_permissions_to 'show', 'users'
78
+
79
+ # Adding permissions to access create and update actions in UsersController
80
+ user.add_permissions_to 'create', 'update', 'users'
81
+ ```
82
+ _The arguments must be related to the actions of a controller, and the last argument is the name of the controller. The actions arguments must be in downcase format and must be equal to the actions methods of the controller. The controller argument, must be in downcase and plural format, ignoring the "Controller" prefix._
83
+
84
+ _For example: a controller called `UsersController` must be passed just as `users`._
85
+
86
+ Also is possible to pass the arguments as symbols:
87
+ ```ruby
88
+ user.add_permissions_to :create, :update, :users
89
+ ```
90
+
91
+ ### Removing permissions
92
+
93
+ It is possible to remove one or more permissions to access a controller, calling:
94
+
95
+ ```ruby
96
+ # Removing permission to access create action in UsersController
97
+ user.remove_permissions_to 'create', 'users'
98
+
99
+ # Removing permissions to access create and update actions in UsersController
100
+ user.remove_permissions_to 'create', 'update', 'users'
101
+ ```
102
+
103
+ ### Checking if has permission to access an action
104
+
105
+ To check if the user has permission to access an action from a controller, you just need to call the method `has_permission_to?` passing the action and the controller as argument:
106
+
107
+ ```ruby
108
+ user.has_permission_to? 'create', 'users'
109
+ ```
110
+
111
+ ## Authorization
112
+
113
+ To authorize the actions in a controller, you must call `authorize_action!`. Action Sentinel will authorize the access if the current user has permission to access the action.
114
+
115
+ ```ruby
116
+ def create
117
+ authorize_action!
118
+
119
+ # implementation code
120
+ end
121
+ ```
122
+
123
+ Also can be called in `before_action` method:
124
+
125
+ ```ruby
126
+ before_action :authorize_action!
127
+ ```
128
+
129
+ ### Action User
130
+
131
+ The authorization module expects that a `current_user` method exists into your controller (if you are using devise for example), but you can override `action_user` method to reflect your current user:
132
+
133
+ ```ruby
134
+ class ApplicationController < ActionController::Base
135
+ include ActionSentinel::Authorization
136
+
137
+ protected
138
+
139
+ def action_user
140
+ your_current_user
141
+ end
142
+ end
143
+ ```
144
+
145
+ ### Rescuing an UnauthorizedAction in ApplicationController
146
+ ---
147
+
148
+ Action Sentinel raises an `ActionSentinel::UnauthorizedAction` if the user does not have the permission to access an action. You can rescue this error and respond in your customized format using `rescue_from` in your `ApplicationController`:
149
+
150
+ ```ruby
151
+ class ApplicationController < ActionController::Base
152
+ include ActionSentinel::Authorization
153
+
154
+ rescue_from ActionSentinel::UnauthorizedAction, with: :unauthorized_action
155
+
156
+ protected
157
+
158
+ def unauthorized_action(error)
159
+ render json: { error_message: error.message }, status: :forbidden
160
+ end
161
+ end
162
+ ```
163
+
164
+ ## Contributing
165
+
166
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Null-Bug-Company/action_sentinel.
167
+
168
+ ## License
169
+
170
+ 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionSentinel
4
+ # Authorization methods for controlling access to controller actions.
5
+ module Authorization
6
+ protected
7
+
8
+ # Authorize the current user to access the controller action.
9
+ #
10
+ # @raise [UnauthorizedAction] if the user is not authorized.
11
+ # @return [void]
12
+ def authorize_action!
13
+ return if action_user.has_permission_to?(action_name, controller_name)
14
+
15
+ raise UnauthorizedAction, "Not allowed to access '#{action_name}' action in #{controller_name}_controller"
16
+ end
17
+
18
+ # Retrieve the user associated with the current action.
19
+ #
20
+ # This method expects a current_user method with the current authenticated user
21
+ # to exist or should be overridden to reflect the current user
22
+ #
23
+ # @return [Object] The user associated with the current action.
24
+ def action_user
25
+ current_user
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module ActionSentinel
6
+ # Provides methods for managing access permissions associated with a model.
7
+ #
8
+ # This module is designed to be included in models that need to manage access permissions.
9
+ # It introduces methods for adding, removing, and checking permissions associated with a specific
10
+ # controller and actions.
11
+ #
12
+ # @example Including Permissible in a Model
13
+ # class User < ApplicationRecord
14
+ # include ActionSentinel::Permissible
15
+ # end
16
+ #
17
+ # user = User.new
18
+ # user.add_permissions_to(:create, :update, :users)
19
+ # user.has_permission_to?(:create, :users) # => true
20
+ #
21
+ module Permissible
22
+ extend ActiveSupport::Concern
23
+
24
+ included do
25
+ has_many :access_permissions
26
+ end
27
+
28
+ # Add permissions to the access_permissions association for a specific controller.
29
+ #
30
+ # @param actions [Array<Symbol, String>] The actions to add permissions for.
31
+ # @param controller_name [String] The name of the controller.
32
+ # @return [Boolean] true if the permission was saved, false otherwise.
33
+ def add_permissions_to(*actions, controller_name)
34
+ permission = access_permissions.find_or_initialize_by(controller_name: controller_name)
35
+ permission.assign_attributes(actions: (permission.actions + sanitize_actions_array(actions)).uniq)
36
+ permission.save
37
+ end
38
+
39
+ # Remove permissions from the access_permissions association for a specific controller.
40
+ #
41
+ # @param actions [Array<Symbol, String>] The actions to remove permissions for.
42
+ # @param controller_name [String] The name of the controller.
43
+ # @return [Boolean, nil] true if the permission was saved, false if it was not or nil
44
+ # if the permission was not found.
45
+ def remove_permissions_to(*actions, controller_name)
46
+ permission = access_permissions.find_by(controller_name: controller_name)
47
+ permission&.update(actions: (permission.actions - sanitize_actions_array(actions)))
48
+ end
49
+
50
+ # rubocop:disable Naming/PredicateName
51
+
52
+ # Check if the model has permission to perform a specific action in a controller.
53
+ #
54
+ # @param action [Symbol, String] The action to check permission for.
55
+ # @param controller_name [String] The name of the controller.
56
+ # @return [Boolean] true if the model has permission, false otherwise.
57
+ def has_permission_to?(action, controller_name)
58
+ query = access_permissions.where(controller_name: controller_name)
59
+
60
+ query = if %w[sqlite sqlite3].include? self.class.connection.adapter_name.downcase
61
+ query.where("actions LIKE ?", "%#{action}%")
62
+ else
63
+ query.where(':action = ANY("access_permissions"."actions")', action: action)
64
+ end
65
+
66
+ query.exists?
67
+ end
68
+ # rubocop:enable Naming/PredicateName
69
+
70
+ private
71
+
72
+ # Sanitize an array of actions by converting them to strings and removing blanks.
73
+ #
74
+ # @param actions [Array<Symbol, String>] The actions to sanitize.
75
+ # @return [Array<String>] The sanitized actions.
76
+ def sanitize_actions_array(actions)
77
+ actions.map { |action| action.to_s unless action.blank? }.compact
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module ActionSentinel
6
+ class Railtie < Rails::Railtie # :nodoc:
7
+ initializer "action_sentinel.initialize" do
8
+ ActiveSupport.on_load(:active_record) do
9
+ ActiveRecord::Base.extend ActionSentinel
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionSentinel
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_sentinel/version"
4
+ require "action_sentinel/railtie"
5
+ require "action_sentinel/authorization"
6
+ require "action_sentinel/permissible"
7
+
8
+ # The ActionSentinel module provides a simple mechanism for user authorization
9
+ # within Rails applications, based on permissions in model-level.
10
+ module ActionSentinel
11
+ #
12
+ # Exception class raised when an unauthorized action is attempted.
13
+ class UnauthorizedAction < StandardError; end
14
+
15
+ # Includes the Permissible module in the calling model class
16
+ # to manage permissions for controller actions.
17
+ #
18
+ # @example:
19
+ #
20
+ # class User < ApplicationRecord
21
+ # action_permissible
22
+ # end
23
+ #
24
+ # @see ActionSentinel::Permissible
25
+ def action_permissible
26
+ include ActionSentinel::Permissible
27
+ end
28
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module ActionSentinel
7
+ class AccessPermissionGenerator < Rails::Generators::Base
8
+ include Rails::Generators::Migration
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ argument :model_name, type: :string, desc: "Name of the model to associate with AccessPermission"
13
+
14
+ class_option :uuid, type: :boolean, default: false, desc: "Use UUID type as primary/foreign key"
15
+
16
+ def self.next_migration_number(path)
17
+ ActiveRecord::Generators::Base.next_migration_number(path)
18
+ end
19
+
20
+ def create_access_pemission_and_migration
21
+ inject_action_permissible_into_model
22
+ generate_access_permission
23
+ generate_migration
24
+ end
25
+
26
+ private
27
+
28
+ def generate_access_permission
29
+ template "access_permission.rb", File.join("app", "models", "access_permission.rb")
30
+ end
31
+
32
+ def table_id_type
33
+ options.uuid? ? ", id: :uuid" : ""
34
+ end
35
+
36
+ def generate_migration
37
+ migration_template "migration.rb", "db/migrate/create_access_permissions.rb"
38
+ end
39
+
40
+ def model_class
41
+ model_name.camelize
42
+ end
43
+
44
+ def singular_model_name
45
+ model_name.underscore
46
+ end
47
+
48
+ def migration_version
49
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
50
+ end
51
+
52
+ def inject_action_permissible_into_model
53
+ model_file = File.join("app", "models", "#{singular_model_name}.rb")
54
+ inject_into_class(model_file, model_class) do
55
+ "\taction_permissible\n\n"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AccessPermission < ApplicationRecord
4
+ belongs_to :<%= singular_model_name %>
5
+
6
+ validates :controller_name, uniqueness: { scope: :<%= singular_model_name %>_id }
7
+ end
@@ -0,0 +1,13 @@
1
+ class CreateAccessPermissions < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :access_permissions<%= table_id_type %> do |t|
4
+ t.string :controller_name, null: false
5
+ t.string :actions, null: false, array: true, default: []
6
+ t.references :<%= singular_model_name %>, null: false<%= table_id_type %>
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :access_permissions, [:controller_name, :<%= singular_model_name %>_id], unique: true
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: action_sentinel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Denis Stael
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-12-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.21'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.21'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.4.2
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.4.2
55
+ description: " This gem enables access authorization control, based on the access
56
+ permission settings for \n each controller and its actions, at the model level.\n"
57
+ email:
58
+ - denis@nullbug.dev
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".rspec"
64
+ - ".rubocop.yml"
65
+ - ".yardopts"
66
+ - CHANGELOG.md
67
+ - Gemfile
68
+ - LICENSE.txt
69
+ - README.md
70
+ - Rakefile
71
+ - lib/action_sentinel.rb
72
+ - lib/action_sentinel/authorization.rb
73
+ - lib/action_sentinel/permissible.rb
74
+ - lib/action_sentinel/railtie.rb
75
+ - lib/action_sentinel/version.rb
76
+ - lib/generators/action_sentinel/access_permission_generator.rb
77
+ - lib/generators/action_sentinel/templates/access_permission.rb
78
+ - lib/generators/action_sentinel/templates/migration.rb
79
+ homepage: https://github.com/Null-Bug-Company/action_sentinel
80
+ licenses:
81
+ - MIT
82
+ metadata:
83
+ homepage_uri: https://github.com/Null-Bug-Company/action_sentinel
84
+ source_code_uri: https://github.com/Null-Bug-Company/action_sentinel
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: 2.6.0
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubygems_version: 3.3.7
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: Simple authorization of controller actions based on model-level permissions
104
+ test_files: []