simple_authorize 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 79902cf5162234cb48b57c7a0584265292141771e59594199c7adee9dea7b1c7
4
+ data.tar.gz: 3022a11895f81c9a02918d88bb1929c4c75afb13eec0b0315e36cbdbaecc0745
5
+ SHA512:
6
+ metadata.gz: 24db25e4450b4f5cb060b902f6bb99b9544aaf39397757ad0403b2feeb4ee99a678632219eec710cce36c0923fad9cb0da9465e85fa9edf60de7028e99a524a9
7
+ data.tar.gz: 2cffd9ff55d8a33d6093297d8c2beea976403cdf5c2d0c5b77a1f40990fff673ee43c763264964d721be1d97c68d74190bbf35c4674419ee6da999ea19a25262
data/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
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
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - Initial release of SimpleAuthorize
12
+ - Policy-based authorization system
13
+ - Controller concern with authorization methods
14
+ - Base policy class with default deny-all policies
15
+ - Policy scope support for filtering collections
16
+ - Strong parameters integration via `permitted_attributes` and `policy_params`
17
+ - Automatic verification module (opt-in)
18
+ - Headless policy support for policies without models
19
+ - Namespace support for policies
20
+ - Role-based helper methods (`admin_user?`, `contributor_user?`, `viewer_user?`)
21
+ - Custom error handling with `NotAuthorizedError`
22
+ - Install generator (`rails generate simple_authorize:install`)
23
+ - Configuration system via initializer
24
+ - Comprehensive documentation and examples
25
+ - Test helper methods for easy testing
26
+ - Backwards compatibility aliases for Pundit-style usage
27
+
28
+ ## [0.1.0] - 2025-11-01
29
+
30
+ ### Added
31
+ - Initial gem structure
32
+ - Core authorization framework extracted from production Rails application
33
+ - MIT license
34
+ - README with comprehensive documentation
35
+ - Generator templates for installation
36
+
37
+ [Unreleased]: https://github.com/scottlaplant/simple_authorize/compare/v0.1.0...HEAD
38
+ [0.1.0]: https://github.com/scottlaplant/simple_authorize/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Scott
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,321 @@
1
+ # SimpleAuthorize
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/simple_authorize.svg)](https://badge.fury.io/rb/simple_authorize)
4
+ [![Ruby](https://github.com/scottlaplant/simple_authorize/workflows/Ruby/badge.svg)](https://github.com/scottlaplant/simple_authorize/actions)
5
+
6
+ SimpleAuthorize is a lightweight, powerful authorization framework for Rails that provides policy-based access control without external dependencies. Inspired by Pundit, it offers a clean API for managing permissions in your Rails applications.
7
+
8
+ ## Features
9
+
10
+ - ๐Ÿ”’ **Policy-Based Authorization** - Define authorization rules in dedicated policy classes
11
+ - ๐ŸŽฏ **Scope Filtering** - Automatically filter collections based on user permissions
12
+ - ๐Ÿ”‘ **Role-Based Access** - Built-in support for role-based authorization
13
+ - ๐Ÿš€ **Zero Dependencies** - No external gems required (only Rails)
14
+ - โœ… **Strong Parameters Integration** - Automatically build permitted params from policies
15
+ - ๐Ÿงช **Test Friendly** - Easy to test policies in isolation
16
+ - ๐Ÿ“ **Rails Generators** - Quickly scaffold policies for your models
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'simple_authorize'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ ```bash
29
+ bundle install
30
+ rails generate simple_authorize:install
31
+ ```
32
+
33
+ This will create:
34
+ - `config/initializers/simple_authorize.rb` - Configuration file
35
+ - `app/policies/application_policy.rb` - Base policy class
36
+
37
+ ## Quick Start
38
+
39
+ ### 1. Include SimpleAuthorize in your ApplicationController
40
+
41
+ ```ruby
42
+ class ApplicationController < ActionController::Base
43
+ include SimpleAuthorize::Controller
44
+ rescue_from_authorization_errors
45
+ end
46
+ ```
47
+
48
+ ### 2. Create a Policy
49
+
50
+ Create a policy class for your model in `app/policies/`:
51
+
52
+ ```ruby
53
+ # app/policies/post_policy.rb
54
+ class PostPolicy < ApplicationPolicy
55
+ def index?
56
+ true
57
+ end
58
+
59
+ def show?
60
+ true
61
+ end
62
+
63
+ def create?
64
+ user.present?
65
+ end
66
+
67
+ def update?
68
+ user.present? && (record.user_id == user.id || user.admin?)
69
+ end
70
+
71
+ def destroy?
72
+ update?
73
+ end
74
+
75
+ class Scope < ApplicationPolicy::Scope
76
+ def resolve
77
+ if user&.admin?
78
+ scope.all
79
+ else
80
+ scope.where(published: true)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ ```
86
+
87
+ ### 3. Use Authorization in Your Controllers
88
+
89
+ ```ruby
90
+ class PostsController < ApplicationController
91
+ def index
92
+ @posts = policy_scope(Post)
93
+ end
94
+
95
+ def show
96
+ @post = Post.find(params[:id])
97
+ authorize @post
98
+ end
99
+
100
+ def create
101
+ @post = Post.new(post_params)
102
+ authorize @post
103
+
104
+ if @post.save
105
+ redirect_to @post
106
+ else
107
+ render :new
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ def post_params
114
+ params.require(:post).permit(:title, :body, :published)
115
+ end
116
+ end
117
+ ```
118
+
119
+ ### 4. Use in Views
120
+
121
+ Check permissions in your views:
122
+
123
+ ```erb
124
+ <% if policy(@post).update? %>
125
+ <%= link_to "Edit", edit_post_path(@post) %>
126
+ <% end %>
127
+
128
+ <% if policy(@post).destroy? %>
129
+ <%= link_to "Delete", post_path(@post), method: :delete %>
130
+ <% end %>
131
+ ```
132
+
133
+ ## Core Concepts
134
+
135
+ ### Policies
136
+
137
+ Policies are plain Ruby objects that encapsulate authorization logic. Each policy corresponds to a model and defines what actions users can perform.
138
+
139
+ ```ruby
140
+ class PostPolicy < ApplicationPolicy
141
+ def update?
142
+ # Only the owner or an admin can update
143
+ user.present? && (record.user_id == user.id || user.admin?)
144
+ end
145
+ end
146
+ ```
147
+
148
+ ### Scopes
149
+
150
+ Scopes filter collections based on user permissions:
151
+
152
+ ```ruby
153
+ class PostPolicy < ApplicationPolicy
154
+ class Scope < ApplicationPolicy::Scope
155
+ def resolve
156
+ if user&.admin?
157
+ scope.all
158
+ else
159
+ scope.where(published: true)
160
+ end
161
+ end
162
+ end
163
+ end
164
+ ```
165
+
166
+ Use in controllers:
167
+
168
+ ```ruby
169
+ def index
170
+ @posts = policy_scope(Post)
171
+ end
172
+ ```
173
+
174
+ ### Strong Parameters
175
+
176
+ SimpleAuthorize can automatically build permitted parameters from policies:
177
+
178
+ ```ruby
179
+ class PostPolicy < ApplicationPolicy
180
+ def permitted_attributes
181
+ if user&.admin?
182
+ [:title, :body, :published, :featured]
183
+ else
184
+ [:title, :body]
185
+ end
186
+ end
187
+ end
188
+ ```
189
+
190
+ Use in controllers:
191
+
192
+ ```ruby
193
+ def post_params
194
+ policy_params(Post, :post)
195
+ # Or manually:
196
+ # params.require(:post).permit(*permitted_attributes(Post.new))
197
+ end
198
+ ```
199
+
200
+ ## Advanced Features
201
+
202
+ ### Headless Policies
203
+
204
+ For policies that don't correspond to a model:
205
+
206
+ ```ruby
207
+ class DashboardPolicy < ApplicationPolicy
208
+ def show?
209
+ user&.admin?
210
+ end
211
+ end
212
+
213
+ # In controller:
214
+ def show
215
+ authorize_headless(DashboardPolicy)
216
+ end
217
+ ```
218
+
219
+ ### Custom Query Methods
220
+
221
+ Define custom authorization queries:
222
+
223
+ ```ruby
224
+ class PostPolicy < ApplicationPolicy
225
+ def publish?
226
+ user&.admin? || (user&.contributor? && owner?)
227
+ end
228
+ end
229
+
230
+ # In controller:
231
+ authorize @post, :publish?
232
+ ```
233
+
234
+ ### Automatic Verification
235
+
236
+ Ensure every action is authorized:
237
+
238
+ ```ruby
239
+ class ApplicationController < ActionController::Base
240
+ include SimpleAuthorize::Controller
241
+ include SimpleAuthorize::Controller::AutoVerify # Enable auto-verification
242
+ rescue_from_authorization_errors
243
+ end
244
+ ```
245
+
246
+ This will require `authorize` or `policy_scope` in all actions.
247
+
248
+ Skip verification when needed:
249
+
250
+ ```ruby
251
+ class PublicController < ApplicationController
252
+ skip_authorization_check :index, :show
253
+ end
254
+ ```
255
+
256
+ ## Testing
257
+
258
+ Test policies in isolation:
259
+
260
+ ```ruby
261
+ require 'test_helper'
262
+
263
+ class PostPolicyTest < ActiveSupport::TestCase
264
+ test "admin can update any post" do
265
+ admin = users(:admin)
266
+ post = posts(:one)
267
+ policy = PostPolicy.new(admin, post)
268
+
269
+ assert policy.update?
270
+ end
271
+
272
+ test "user can only update their own posts" do
273
+ user = users(:regular)
274
+ own_post = posts(:user_post)
275
+ other_post = posts(:other_post)
276
+
277
+ assert PostPolicy.new(user, own_post).update?
278
+ refute PostPolicy.new(user, other_post).update?
279
+ end
280
+ end
281
+ ```
282
+
283
+ ## Development
284
+
285
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
286
+
287
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
288
+
289
+ ## Comparison with Pundit
290
+
291
+ SimpleAuthorize is heavily inspired by Pundit and offers a similar API. Key differences:
292
+
293
+ | Feature | SimpleAuthorize | Pundit |
294
+ |---------|----------------|--------|
295
+ | Dependencies | None (Rails only) | Standalone gem |
296
+ | Base class | `SimpleAuthorize::Policy` | `ApplicationPolicy` (user-defined) |
297
+ | Installation | Generator creates base policy | Manual setup required |
298
+ | Module name | `SimpleAuthorize::Controller` | `Pundit` |
299
+ | Compatibility | Rails 6.0+ | Rails 4.0+ |
300
+
301
+ Migration from Pundit is straightforward - most code will work with minimal changes.
302
+
303
+ ## Contributing
304
+
305
+ Bug reports and pull requests are welcome on GitHub at https://github.com/scottlaplant/simple_authorize.
306
+
307
+ 1. Fork it
308
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
309
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
310
+ 4. Push to the branch (`git push origin my-new-feature`)
311
+ 5. Create new Pull Request
312
+
313
+ ## License
314
+
315
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
316
+
317
+ ## Credits
318
+
319
+ Created by Scott LaPlant
320
+
321
+ Inspired by [Pundit](https://github.com/varvet/pundit) by Elabs
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module SimpleAuthorize
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Creates a SimpleAuthorize initializer and ApplicationPolicy base class"
11
+
12
+ def copy_initializer
13
+ template "simple_authorize.rb", "config/initializers/simple_authorize.rb"
14
+ end
15
+
16
+ def copy_application_policy
17
+ template "application_policy.rb", "app/policies/application_policy.rb"
18
+ end
19
+
20
+ def show_readme
21
+ readme "README" if behavior == :invoke
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,80 @@
1
+ ===============================================================================
2
+
3
+ SimpleAuthorize has been installed!
4
+
5
+ ===============================================================================
6
+
7
+ Next steps:
8
+
9
+ 1. Include SimpleAuthorize::Controller in your ApplicationController:
10
+
11
+ class ApplicationController < ActionController::Base
12
+ include SimpleAuthorize::Controller
13
+ rescue_from_authorization_errors
14
+ end
15
+
16
+ 2. Create policies for your models in app/policies/:
17
+
18
+ # app/policies/post_policy.rb
19
+ class PostPolicy < ApplicationPolicy
20
+ def index?
21
+ true
22
+ end
23
+
24
+ def show?
25
+ true
26
+ end
27
+
28
+ def create?
29
+ user.present?
30
+ end
31
+
32
+ def update?
33
+ user.present? && (record.user_id == user.id || user.admin?)
34
+ end
35
+
36
+ def destroy?
37
+ update?
38
+ end
39
+
40
+ class Scope < ApplicationPolicy::Scope
41
+ def resolve
42
+ if user&.admin?
43
+ scope.all
44
+ else
45
+ scope.where(published: true)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ 3. Use authorization in your controllers:
52
+
53
+ class PostsController < ApplicationController
54
+ def index
55
+ @posts = policy_scope(Post)
56
+ end
57
+
58
+ def show
59
+ @post = Post.find(params[:id])
60
+ authorize @post
61
+ end
62
+
63
+ def create
64
+ @post = Post.new(post_params)
65
+ authorize @post
66
+ # ...
67
+ end
68
+ end
69
+
70
+ 4. (Optional) Enable automatic verification:
71
+
72
+ class ApplicationController < ActionController::Base
73
+ include SimpleAuthorize::Controller
74
+ include SimpleAuthorize::Controller::AutoVerify
75
+ rescue_from_authorization_errors
76
+ end
77
+
78
+ For more information, see: https://github.com/yourusername/simple_authorize
79
+
80
+ ===============================================================================
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base policy class that all other policies inherit from
4
+ # Inherits from SimpleAuthorize::Policy which provides default deny-all policies
5
+ class ApplicationPolicy < SimpleAuthorize::Policy
6
+ # Override default policies here if needed
7
+ # For example, allow all logged-in users to view index:
8
+ # def index?
9
+ # logged_in?
10
+ # end
11
+
12
+ # Add custom helper methods here
13
+ # protected
14
+ #
15
+ # def owned_by_user?
16
+ # record.user_id == user&.id
17
+ # end
18
+
19
+ # Scope class for filtering collections
20
+ class Scope < SimpleAuthorize::Policy::Scope
21
+ # Override the resolve method to customize collection filtering
22
+ # def resolve
23
+ # if admin?
24
+ # scope.all
25
+ # else
26
+ # scope.where(published: true)
27
+ # end
28
+ # end
29
+ end
30
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configure SimpleAuthorize
4
+ SimpleAuthorize.configure do |config|
5
+ # Default error message shown to users when not authorized
6
+ # config.default_error_message = "You are not authorized to perform this action."
7
+
8
+ # Enable automatic verification (requires including SimpleAuthorize::Controller::AutoVerify)
9
+ # config.auto_verify = false
10
+
11
+ # The method to call to get the current user (default: current_user)
12
+ # config.current_user_method = :current_user
13
+
14
+ # Custom redirect path for unauthorized access (default: uses referrer or root_path)
15
+ # config.unauthorized_redirect_path = "/unauthorized"
16
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuthorize
4
+ class Configuration
5
+ # Default error message shown to users when not authorized
6
+ attr_accessor :default_error_message
7
+
8
+ # Whether to enable automatic verification (opt-in)
9
+ attr_accessor :auto_verify
10
+
11
+ # The method to call to get the current user (default: current_user)
12
+ attr_accessor :current_user_method
13
+
14
+ # Custom redirect path for unauthorized access
15
+ attr_accessor :unauthorized_redirect_path
16
+
17
+ def initialize
18
+ @default_error_message = "You are not authorized to perform this action."
19
+ @auto_verify = false
20
+ @current_user_method = :current_user
21
+ @unauthorized_redirect_path = nil
22
+ end
23
+ end
24
+
25
+ class << self
26
+ attr_writer :configuration
27
+
28
+ def configuration
29
+ @configuration ||= Configuration.new
30
+ end
31
+
32
+ def configure
33
+ yield(configuration)
34
+ end
35
+
36
+ def reset_configuration!
37
+ @configuration = Configuration.new
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuthorize
4
+ # Enhanced authorization system with full feature parity
5
+ # Provides comprehensive authorization without external dependencies
6
+ module Controller
7
+ extend ActiveSupport::Concern
8
+
9
+ # Custom error classes for authorization
10
+ class NotAuthorizedError < StandardError
11
+ attr_reader :query, :record, :policy
12
+
13
+ def initialize(options = {})
14
+ if options.is_a?(String)
15
+ # Handle plain string message
16
+ super(options)
17
+ else
18
+ # Handle hash options
19
+ @query = options[:query]
20
+ @record = options[:record]
21
+ @policy = options[:policy]
22
+
23
+ message = options[:message] || "not allowed to #{@query} this #{@record.class}"
24
+ super(message)
25
+ end
26
+ end
27
+ end
28
+
29
+ class PolicyNotDefinedError < StandardError; end
30
+ class AuthorizationNotPerformedError < StandardError; end
31
+ class PolicyScopingNotPerformedError < StandardError; end
32
+ # Alias for backwards compatibility
33
+ ScopingNotPerformedError = PolicyScopingNotPerformedError
34
+
35
+ included do
36
+ # Make these available as helper methods in views
37
+ helper_method :policy, :policy_scope, :authorized_user if respond_to?(:helper_method)
38
+ end
39
+
40
+ # Module to enable automatic verification - opt-in for safety
41
+ module AutoVerify
42
+ extend ActiveSupport::Concern
43
+
44
+ included do
45
+ # Track whether authorization was performed
46
+ after_action :verify_authorized, except: :index
47
+ after_action :verify_policy_scoped, only: :index
48
+ end
49
+ end
50
+
51
+ # Core authorization methods
52
+
53
+ def authorize(record, query = nil, policy_class: nil)
54
+ query ||= "#{action_name}?"
55
+ @_policy = policy(record, policy_class: policy_class)
56
+
57
+ unless @_policy.public_send(query)
58
+ raise NotAuthorizedError.new(query: query, record: record, policy: @_policy)
59
+ end
60
+
61
+ @authorization_performed = true
62
+ record
63
+ end
64
+
65
+ # Authorize and raise exception if not authorized
66
+ def authorize!(record, query = nil, policy_class: nil)
67
+ authorize(record, query, policy_class: policy_class)
68
+ end
69
+
70
+ # Get or instantiate policy for a record
71
+ def policy(record, policy_class: nil, namespace: nil)
72
+ policy_class ||= if namespace
73
+ policy_class_for(record, namespace: namespace)
74
+ else
75
+ policy_class_for(record)
76
+ end
77
+ policy_class.new(authorized_user, record)
78
+ rescue NameError
79
+ raise PolicyNotDefinedError, "unable to find policy `#{policy_class}` for `#{record}`"
80
+ end
81
+
82
+ # Ensure policy exists, raising if not found
83
+ def policy!(record, policy_class: nil)
84
+ policy(record, policy_class: policy_class)
85
+ end
86
+
87
+ # Scope a relation using the policy scope
88
+ def policy_scope(scope, policy_scope_class: nil)
89
+ @policy_scoping_performed = true
90
+
91
+ policy_scope_class ||= policy_scope_class_for(scope)
92
+ policy_scope_class.new(authorized_user, scope).resolve
93
+ rescue NameError
94
+ raise PolicyNotDefinedError, "unable to find scope `#{policy_scope_class}` for `#{scope}`"
95
+ end
96
+
97
+ # Ensure scope exists, raising if not found
98
+ def policy_scope!(scope, policy_scope_class: nil)
99
+ policy_scope(scope, policy_scope_class: policy_scope_class)
100
+ end
101
+
102
+ # Get permitted attributes for strong parameters
103
+ def permitted_attributes(record, action = nil)
104
+ action ||= action_name
105
+ policy = policy(record)
106
+ method_name = "permitted_attributes_for_#{action}"
107
+
108
+ if policy.respond_to?(method_name)
109
+ policy.public_send(method_name)
110
+ elsif policy.respond_to?(:permitted_attributes)
111
+ policy.permitted_attributes
112
+ else
113
+ raise PolicyNotDefinedError, "unable to find permitted attributes for #{record}"
114
+ end
115
+ end
116
+
117
+ # Automatically build permitted params from policy
118
+ def policy_params(record, param_key = nil)
119
+ param_key ||= record.model_name.param_key
120
+ params.require(param_key).permit(*permitted_attributes(record))
121
+ end
122
+
123
+ # Verify that authorization was performed
124
+ def verify_authorized
125
+ return if authorization_performed?
126
+ raise AuthorizationNotPerformedError, "#{self.class}##{action_name} is missing authorization"
127
+ end
128
+
129
+ # Verify that scoping was performed for index actions
130
+ def verify_policy_scoped
131
+ return if policy_scoped?
132
+ raise PolicyScopingNotPerformedError, "#{self.class}##{action_name} is missing policy scope"
133
+ end
134
+
135
+ # Skip authorization verification for specific actions
136
+ def skip_authorization
137
+ @authorization_performed = true
138
+ end
139
+
140
+ # Skip policy scope verification for specific actions
141
+ def skip_policy_scope
142
+ @policy_scoping_performed = true
143
+ end
144
+
145
+ # Check if authorization was performed
146
+ def authorization_performed?
147
+ @authorization_performed == true
148
+ end
149
+
150
+ # Check if policy scoping was performed
151
+ def policy_scoped?
152
+ @policy_scoping_performed == true
153
+ end
154
+
155
+ # Get the user for authorization (can be overridden)
156
+ def authorized_user
157
+ current_user
158
+ end
159
+
160
+ # Reset authorization tracking (useful in tests)
161
+ def reset_authorization
162
+ @authorization_performed = nil
163
+ @policy_scoping_performed = nil
164
+ @_policy = nil
165
+ end
166
+
167
+ # Support for headless policies (policies without a model)
168
+ def authorize_headless(policy_class, query = nil)
169
+ query ||= "#{action_name}?"
170
+ policy = policy_class.new(authorized_user, nil)
171
+
172
+ unless policy.public_send(query)
173
+ raise NotAuthorizedError.new(query: query, record: policy_class, policy: policy)
174
+ end
175
+
176
+ @authorization_performed = true
177
+ true
178
+ end
179
+
180
+ # Check if user can perform action without raising
181
+ def allowed_to?(action, record, policy_class: nil)
182
+ policy = policy(record, policy_class: policy_class)
183
+ policy.public_send("#{action}?")
184
+ rescue PolicyNotDefinedError
185
+ false
186
+ end
187
+
188
+ # Get all allowed actions for a record
189
+ def allowed_actions(record)
190
+ policy = policy(record)
191
+ actions = []
192
+
193
+ %i[index? show? create? update? destroy?].each do |method|
194
+ actions << method.to_s.delete("?").to_sym if policy.respond_to?(method) && policy.public_send(method)
195
+ end
196
+
197
+ actions
198
+ end
199
+
200
+ # Role helper methods
201
+ def admin_user?
202
+ current_user&.admin?
203
+ end
204
+
205
+ def contributor_user?
206
+ current_user&.contributor?
207
+ end
208
+
209
+ def viewer_user?
210
+ current_user&.viewer?
211
+ end
212
+
213
+ # Alias for handle_unauthorized that matches common convention
214
+ def user_not_authorized(exception = nil)
215
+ handle_unauthorized(exception)
216
+ end
217
+
218
+ protected
219
+
220
+ def policy_class_for(record, namespace: nil)
221
+ klass = record.class
222
+ record_class = if record.is_a?(Class)
223
+ record.name
224
+ elsif record.respond_to?(:model_name)
225
+ record.model_name.to_s
226
+ elsif klass.respond_to?(:model_name)
227
+ klass.model_name.to_s
228
+ else
229
+ klass.name
230
+ end
231
+
232
+ policy_class_name = if namespace
233
+ "#{namespace.to_s.camelize}::#{record_class}Policy"
234
+ else
235
+ "#{record_class}Policy"
236
+ end
237
+
238
+ begin
239
+ policy_class_name.constantize
240
+ rescue NameError
241
+ # Fall back to non-namespaced policy if namespaced one doesn't exist
242
+ if namespace
243
+ "#{record_class}Policy".constantize
244
+ else
245
+ raise
246
+ end
247
+ end
248
+ end
249
+
250
+ def policy_scope_class_for(scope)
251
+ if scope.respond_to?(:model_name)
252
+ "#{scope.model_name}Policy::Scope".constantize
253
+ elsif scope.is_a?(Class)
254
+ "#{scope}Policy::Scope".constantize
255
+ else
256
+ "#{scope.class}Policy::Scope".constantize
257
+ end
258
+ end
259
+
260
+ # Handle authorization errors
261
+ def handle_unauthorized(exception = nil)
262
+ flash[:alert] = "You are not authorized to perform this action."
263
+ safe_redirect_path = safe_referrer_path || root_path
264
+
265
+ if exception
266
+ redirect_to(safe_redirect_path, status: :see_other)
267
+ else
268
+ redirect_to(safe_redirect_path)
269
+ end
270
+ end
271
+
272
+ # Safely get referrer path, only if it's from our own domain
273
+ def safe_referrer_path
274
+ referrer = request.referrer
275
+ return nil unless referrer.present?
276
+
277
+ referrer_uri = URI.parse(referrer)
278
+ request_uri = URI.parse(request.url)
279
+
280
+ # Only allow referrers from the same host
281
+ if referrer_uri.host == request_uri.host
282
+ referrer_uri.path
283
+ else
284
+ nil
285
+ end
286
+ rescue URI::InvalidURIError
287
+ nil
288
+ end
289
+
290
+ # Class methods for controller configuration
291
+ class_methods do
292
+ # Rescue from authorization errors
293
+ def rescue_from_authorization_errors
294
+ rescue_from SimpleAuthorize::Controller::NotAuthorizedError, with: :handle_unauthorized
295
+ end
296
+
297
+ # Skip authorization for specific actions
298
+ def skip_authorization_check(*actions)
299
+ skip_after_action :verify_authorized, only: actions
300
+ skip_after_action :verify_policy_scoped, only: actions
301
+ end
302
+
303
+ # Skip all authorization checks for controller
304
+ def skip_all_authorization_checks
305
+ skip_after_action :verify_authorized
306
+ skip_after_action :verify_policy_scoped
307
+ end
308
+
309
+ # Configure which actions need authorization
310
+ def authorize_actions(*actions)
311
+ after_action :verify_authorized, only: actions
312
+ end
313
+
314
+ # Configure which actions need policy scoping
315
+ def scope_actions(*actions)
316
+ after_action :verify_policy_scoped, only: actions
317
+ end
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuthorize
4
+ # Base policy class that all other policies inherit from
5
+ class Policy
6
+ attr_reader :user, :record
7
+
8
+ def initialize(user, record)
9
+ @user = user
10
+ @record = record
11
+ end
12
+
13
+ # Default policies - deny everything by default
14
+ def index?
15
+ false
16
+ end
17
+
18
+ def show?
19
+ false
20
+ end
21
+
22
+ def create?
23
+ false
24
+ end
25
+
26
+ def new?
27
+ create?
28
+ end
29
+
30
+ def update?
31
+ false
32
+ end
33
+
34
+ def edit?
35
+ update?
36
+ end
37
+
38
+ def destroy?
39
+ false
40
+ end
41
+
42
+ # Helper methods
43
+ protected
44
+
45
+ def admin?
46
+ user&.admin?
47
+ end
48
+
49
+ def contributor?
50
+ user&.contributor?
51
+ end
52
+
53
+ def viewer?
54
+ user&.viewer?
55
+ end
56
+
57
+ def owner?
58
+ record.respond_to?(:user_id) && record.user_id == user&.id
59
+ end
60
+
61
+ def logged_in?
62
+ user.present?
63
+ end
64
+
65
+ def can_create_content?
66
+ user&.can_create_content?
67
+ end
68
+
69
+ def can_manage_content?
70
+ user&.can_manage_content?
71
+ end
72
+
73
+ # Scope class for filtering collections
74
+ class Scope
75
+ attr_reader :user, :scope
76
+
77
+ def initialize(user, scope)
78
+ @user = user
79
+ @scope = scope
80
+ end
81
+
82
+ def resolve
83
+ scope.all
84
+ end
85
+
86
+ protected
87
+
88
+ def admin?
89
+ user&.admin?
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuthorize
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/all"
4
+ require_relative "simple_authorize/version"
5
+ require_relative "simple_authorize/configuration"
6
+ require_relative "simple_authorize/controller"
7
+ require_relative "simple_authorize/policy"
8
+
9
+ module SimpleAuthorize
10
+ class Error < StandardError; end
11
+
12
+ # Railtie for automatic integration with Rails
13
+ class Railtie < Rails::Railtie
14
+ initializer "simple_authorize.configure" do
15
+ ActiveSupport.on_load(:action_controller) do
16
+ # Make SimpleAuthorize::Controller available as Authorization for backwards compatibility
17
+ ::Authorization = SimpleAuthorize::Controller unless defined?(::Authorization)
18
+ # Make SimpleAuthorize::Policy available as ApplicationPolicy for backwards compatibility
19
+ ::ApplicationPolicy = SimpleAuthorize::Policy unless defined?(::ApplicationPolicy)
20
+ end
21
+ end
22
+
23
+ # Generate initializer template
24
+ generators do
25
+ require_relative "generators/simple_authorize/install/install_generator"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,4 @@
1
+ module SimpleAuthorize
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_authorize
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Scott
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '6.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '6.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: railties
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '6.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '6.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rails
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '6.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '6.0'
54
+ description: SimpleAuthorize is a lightweight authorization framework for Rails that
55
+ provides policy-based access control, role management, and scope filtering without
56
+ requiring external gems. Inspired by Pundit but completely standalone.
57
+ email:
58
+ - scottlaplant@users.noreply.github.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - CHANGELOG.md
64
+ - LICENSE.txt
65
+ - README.md
66
+ - Rakefile
67
+ - lib/generators/simple_authorize/install/install_generator.rb
68
+ - lib/generators/simple_authorize/install/templates/README
69
+ - lib/generators/simple_authorize/install/templates/application_policy.rb
70
+ - lib/generators/simple_authorize/install/templates/simple_authorize.rb
71
+ - lib/simple_authorize.rb
72
+ - lib/simple_authorize/configuration.rb
73
+ - lib/simple_authorize/controller.rb
74
+ - lib/simple_authorize/policy.rb
75
+ - lib/simple_authorize/version.rb
76
+ - sig/simple_authorize.rbs
77
+ homepage: https://github.com/scottlaplant/simple_authorize
78
+ licenses:
79
+ - MIT
80
+ metadata:
81
+ homepage_uri: https://github.com/scottlaplant/simple_authorize
82
+ source_code_uri: https://github.com/scottlaplant/simple_authorize
83
+ changelog_uri: https://github.com/scottlaplant/simple_authorize/blob/main/CHANGELOG.md
84
+ bug_tracker_uri: https://github.com/scottlaplant/simple_authorize/issues
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 3.0.0
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.6.9
100
+ specification_version: 4
101
+ summary: Simple, powerful authorization for Rails without external dependencies
102
+ test_files: []