simple_authorize 0.1.0 โ†’ 1.0.1

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.
data/README.md CHANGED
@@ -1,32 +1,44 @@
1
1
  # SimpleAuthorize
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/simple_authorize.svg)](https://badge.fury.io/rb/simple_authorize)
3
+ [![Gem Version](https://img.shields.io/gem/v/simple_authorize.svg)](https://rubygems.org/gems/simple_authorize)
4
4
  [![Ruby](https://github.com/scottlaplant/simple_authorize/workflows/Ruby/badge.svg)](https://github.com/scottlaplant/simple_authorize/actions)
5
+ [![Downloads](https://img.shields.io/gem/dt/simple_authorize.svg)](https://rubygems.org/gems/simple_authorize)
5
6
 
6
7
  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
 
8
9
  ## Features
9
10
 
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
11
+ - **Policy-Based Authorization** - Define authorization rules in dedicated policy classes
12
+ - **Scope Filtering** - Automatically filter collections based on user permissions
13
+ - **Role-Based Access** - Built-in support for role-based authorization
14
+ - **Zero Dependencies** - No external gems required (only Rails)
15
+ - **Strong Parameters Integration** - Automatically build permitted params from policies
16
+ - **Test Friendly** - Easy to test policies in isolation
17
+ - **Rails Generators** - Quickly scaffold policies for your models
17
18
 
18
19
  ## Installation
19
20
 
20
- Add this line to your application's Gemfile:
21
+ Install the gem directly:
22
+
23
+ ```bash
24
+ gem install simple_authorize
25
+ ```
26
+
27
+ Or add this line to your application's Gemfile:
21
28
 
22
29
  ```ruby
23
30
  gem 'simple_authorize'
24
31
  ```
25
32
 
26
- And then execute:
33
+ Then execute:
27
34
 
28
35
  ```bash
29
36
  bundle install
37
+ ```
38
+
39
+ After installation, run the generator to set up your application:
40
+
41
+ ```bash
30
42
  rails generate simple_authorize:install
31
43
  ```
32
44
 
@@ -47,7 +59,17 @@ end
47
59
 
48
60
  ### 2. Create a Policy
49
61
 
50
- Create a policy class for your model in `app/policies/`:
62
+ Generate a policy for your model using the generator:
63
+
64
+ ```bash
65
+ rails generate simple_authorize:policy Post
66
+ ```
67
+
68
+ This creates:
69
+ - `app/policies/post_policy.rb` - Policy class with CRUD methods
70
+ - `test/policies/post_policy_test.rb` - Test file (or spec file with `--spec`)
71
+
72
+ Or create a policy class manually in `app/policies/`:
51
73
 
52
74
  ```ruby
53
75
  # app/policies/post_policy.rb
@@ -197,6 +219,181 @@ def post_params
197
219
  end
198
220
  ```
199
221
 
222
+ ## Generators
223
+
224
+ SimpleAuthorize provides Rails generators to quickly scaffold policies:
225
+
226
+ ### Install Generator
227
+
228
+ ```bash
229
+ rails generate simple_authorize:install
230
+ ```
231
+
232
+ Creates:
233
+ - `config/initializers/simple_authorize.rb` - Configuration file
234
+ - `app/policies/application_policy.rb` - Base policy class
235
+
236
+ ### Policy Generator
237
+
238
+ ```bash
239
+ rails generate simple_authorize:policy Post
240
+ ```
241
+
242
+ Creates:
243
+ - `app/policies/post_policy.rb` - Policy with CRUD methods and scope
244
+ - `test/policies/post_policy_test.rb` - Minitest tests
245
+
246
+ **Options:**
247
+ - `--spec` - Generate RSpec tests instead of Minitest
248
+ - `--skip-test` - Skip test file generation
249
+
250
+ **Examples:**
251
+
252
+ ```bash
253
+ # Generate policy with RSpec tests
254
+ rails generate simple_authorize:policy Post --spec
255
+
256
+ # Generate policy without tests
257
+ rails generate simple_authorize:policy Post --skip-test
258
+
259
+ # Generate namespaced policy
260
+ rails generate simple_authorize:policy Admin::Post
261
+ ```
262
+
263
+ ## Configuration
264
+
265
+ SimpleAuthorize can be configured in `config/initializers/simple_authorize.rb`:
266
+
267
+ ### Policy Caching
268
+
269
+ Enable policy caching to improve performance by caching policy instances per request:
270
+
271
+ ```ruby
272
+ SimpleAuthorize.configure do |config|
273
+ config.enable_policy_cache = true
274
+ end
275
+ ```
276
+
277
+ **How it works:**
278
+ - Policy instances are cached for the duration of a single request
279
+ - Cache is automatically scoped by user, record, and policy class
280
+ - Each unique combination gets its own cached instance
281
+ - Cache is automatically cleared between requests
282
+ - Particularly useful in views where the same policy may be checked multiple times
283
+
284
+ **Example performance impact:**
285
+
286
+ ```erb
287
+ <!-- Without caching: Creates 3 separate PostPolicy instances -->
288
+ <% if policy(@post).update? %>
289
+ <%= link_to "Edit", edit_post_path(@post) %>
290
+ <% end %>
291
+ <% if policy(@post).destroy? %>
292
+ <%= link_to "Delete", post_path(@post) %>
293
+ <% end %>
294
+ <% if policy(@post).publish? %>
295
+ <%= link_to "Publish", publish_post_path(@post) %>
296
+ <% end %>
297
+
298
+ <!-- With caching: Reuses the same PostPolicy instance -->
299
+ ```
300
+
301
+ **Testing:**
302
+ Use `clear_policy_cache` or `reset_authorization` to clear the cache in tests:
303
+
304
+ ```ruby
305
+ test "multiple checks use cached policy" do
306
+ SimpleAuthorize.configure { |config| config.enable_policy_cache = true }
307
+
308
+ policy1 = policy(@post)
309
+ policy2 = policy(@post)
310
+ assert_same policy1, policy2 # Same instance
311
+
312
+ clear_policy_cache
313
+ policy3 = policy(@post)
314
+ refute_same policy1, policy3 # New instance after clearing
315
+ end
316
+ ```
317
+
318
+ ### Instrumentation & Audit Logging
319
+
320
+ SimpleAuthorize emits `ActiveSupport::Notifications` events for all authorization checks, perfect for security auditing, debugging, and monitoring:
321
+
322
+ ```ruby
323
+ # Subscribe to authorization events
324
+ ActiveSupport::Notifications.subscribe("authorize.simple_authorize") do |name, start, finish, id, payload|
325
+ duration = finish - start
326
+
327
+ Rails.logger.info({
328
+ event: "authorization",
329
+ user_id: payload[:user_id],
330
+ action: payload[:query],
331
+ resource: "#{payload[:record_class]}##{payload[:record_id]}",
332
+ authorized: payload[:authorized],
333
+ duration_ms: (duration * 1000).round(2)
334
+ }.to_json)
335
+ end
336
+
337
+ # Subscribe to policy scope events
338
+ ActiveSupport::Notifications.subscribe("policy_scope.simple_authorize") do |name, start, finish, id, payload|
339
+ Rails.logger.info("Policy scope applied for #{payload[:scope]} by user #{payload[:user_id]}")
340
+ end
341
+ ```
342
+
343
+ **Event Payloads:**
344
+
345
+ Authorization events (`authorize.simple_authorize`):
346
+ - `user`: Current user object
347
+ - `user_id`: User ID
348
+ - `record`: The record being authorized
349
+ - `record_id`: Record ID
350
+ - `record_class`: Record class name
351
+ - `query`: Authorization method called (e.g., "update?")
352
+ - `policy_class`: Policy class used
353
+ - `authorized`: Boolean result
354
+ - `error`: Exception if authorization failed
355
+ - `controller`: Controller name (if available)
356
+ - `action`: Action name (if available)
357
+
358
+ Policy scope events (`policy_scope.simple_authorize`):
359
+ - `user`: Current user object
360
+ - `user_id`: User ID
361
+ - `scope`: The scope being filtered
362
+ - `policy_scope_class`: Scope class used
363
+ - `error`: Exception if scope failed
364
+ - `controller`: Controller name (if available)
365
+ - `action`: Action name (if available)
366
+
367
+ **Use Cases:**
368
+ - Security auditing and compliance
369
+ - Debugging authorization issues
370
+ - Monitoring authorization performance
371
+ - Sending failed authorization attempts to security services
372
+ - Tracking which users access sensitive resources
373
+
374
+ **Disable instrumentation** (if needed for performance in specific scenarios):
375
+
376
+ ```ruby
377
+ SimpleAuthorize.configure do |config|
378
+ config.enable_instrumentation = false
379
+ end
380
+ ```
381
+
382
+ ### Other Configuration Options
383
+
384
+ ```ruby
385
+ SimpleAuthorize.configure do |config|
386
+ # Custom error message for unauthorized access
387
+ config.default_error_message = "Access denied!"
388
+
389
+ # Custom redirect path for unauthorized users
390
+ config.unauthorized_redirect_path = "/access-denied"
391
+
392
+ # Custom method to get current user (default: current_user)
393
+ config.current_user_method = :authenticated_user
394
+ end
395
+ ```
396
+
200
397
  ## Advanced Features
201
398
 
202
399
  ### Headless Policies
@@ -284,7 +481,7 @@ end
284
481
 
285
482
  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
483
 
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).
484
+ To install this gem onto your local machine, run `bundle exec rake install`.
288
485
 
289
486
  ## Comparison with Pundit
290
487
 
@@ -316,6 +513,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
316
513
 
317
514
  ## Credits
318
515
 
319
- Created by Scott LaPlant
320
-
321
- Inspired by [Pundit](https://github.com/varvet/pundit) by Elabs
516
+ SimpleAuthorize is heavily inspired by [Pundit](https://github.com/varvet/pundit) by Elabs. We're grateful to the Pundit team for pioneering this authorization pattern.
data/SECURITY.md ADDED
@@ -0,0 +1,77 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ We release patches for security vulnerabilities. Currently supported versions:
6
+
7
+ | Version | Supported |
8
+ | ------- | ------------------ |
9
+ | 1.0.x | :white_check_mark: |
10
+ | < 1.0 | :x: |
11
+
12
+ ## Reporting a Vulnerability
13
+
14
+ We take the security of SimpleAuthorize seriously. If you discover a security vulnerability, please report it privately.
15
+
16
+ ### How to Report
17
+
18
+ **Please DO NOT open a public GitHub issue for security vulnerabilities.**
19
+
20
+ Please report security vulnerabilities to: **simpleauthorize@gmail.com**
21
+
22
+ Alternatively, you can use GitHub's private vulnerability reporting feature (see "Security" tab in the repository).
23
+
24
+ ### What to Include
25
+
26
+ Please include the following information in your report:
27
+
28
+ - Type of vulnerability (e.g., authorization bypass, XSS, injection)
29
+ - Full paths of source files related to the vulnerability
30
+ - The location of the affected source code (tag/branch/commit or direct URL)
31
+ - Step-by-step instructions to reproduce the issue
32
+ - Proof-of-concept or exploit code (if possible)
33
+ - Impact of the issue, including how an attacker might exploit it
34
+
35
+ ### Response Timeline
36
+
37
+ - **Initial Response**: Within 48 hours of receiving your report
38
+ - **Status Update**: Within 5 business days with an initial assessment
39
+ - **Fix Timeline**: Critical vulnerabilities will be addressed within 30 days
40
+ - **Disclosure**: We will coordinate with you on the disclosure timeline
41
+
42
+ ### Security Update Process
43
+
44
+ 1. We will confirm the vulnerability and determine its severity
45
+ 2. We will develop and test a fix
46
+ 3. We will release a new version with the security patch
47
+ 4. We will publish a security advisory with details after the fix is released
48
+ 5. We will credit you for the discovery (unless you prefer to remain anonymous)
49
+
50
+ ## Security Best Practices
51
+
52
+ When using SimpleAuthorize in your application:
53
+
54
+ 1. **Always verify authorization**: Use `verify_authorized` in controllers to ensure authorization is checked
55
+ 2. **Secure policy defaults**: The default Policy class denies all actions - only permit what's necessary
56
+ 3. **Test your policies**: Write comprehensive tests for all authorization logic
57
+ 4. **Keep updated**: Regularly update to the latest version to get security patches
58
+ 5. **Review policies**: Periodically audit your policy classes for authorization holes
59
+
60
+ ## Known Security Considerations
61
+
62
+ ### Authorization Bypass Prevention
63
+
64
+ - Always call `authorize` before performing sensitive actions
65
+ - Use `skip_authorization` explicitly and only when intentional
66
+ - Be careful with `headless_policy` - ensure proper authorization for non-resource actions
67
+
68
+ ### Scope Security
69
+
70
+ - Always use `policy_scope` to filter collections
71
+ - Don't rely solely on view-level hiding - enforce at the data layer
72
+ - Test scope filtering with different user roles
73
+
74
+ ## Dependencies
75
+
76
+ SimpleAuthorize has minimal dependencies (only Rails/ActiveSupport). We monitor our dependencies for security vulnerabilities and update promptly.
77
+
@@ -4,6 +4,7 @@ require "rails/generators"
4
4
 
5
5
  module SimpleAuthorize
6
6
  module Generators
7
+ # Rails generator to install SimpleAuthorize configuration and base policy
7
8
  class InstallGenerator < Rails::Generators::Base
8
9
  source_root File.expand_path("templates", __dir__)
9
10
 
@@ -13,4 +13,12 @@ SimpleAuthorize.configure do |config|
13
13
 
14
14
  # Custom redirect path for unauthorized access (default: uses referrer or root_path)
15
15
  # config.unauthorized_redirect_path = "/unauthorized"
16
+
17
+ # Enable policy caching for performance optimization (default: false)
18
+ # When enabled, policy instances are cached per request, scoped by user, record, and policy class
19
+ # config.enable_policy_cache = true
20
+
21
+ # Enable instrumentation for authorization events (default: true)
22
+ # When enabled, emits ActiveSupport::Notifications events for all authorization checks
23
+ # config.enable_instrumentation = true
16
24
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module SimpleAuthorize
6
+ module Generators
7
+ # Rails generator to create a policy class for a model
8
+ class PolicyGenerator < Rails::Generators::NamedBase
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Creates a SimpleAuthorize policy class for a model"
12
+
13
+ argument :name, type: :string, required: true, banner: "ModelName"
14
+
15
+ class_option :spec, type: :boolean, default: false, desc: "Generate RSpec test file instead of Minitest"
16
+ class_option :skip_test, type: :boolean, default: false, desc: "Skip generating test file"
17
+
18
+ def create_policy_file
19
+ template "policy.rb.tt", File.join("app/policies", class_path, "#{file_name}_policy.rb")
20
+ end
21
+
22
+ def create_test_file
23
+ return if options[:skip_test]
24
+
25
+ if options[:spec]
26
+ template "policy_spec.rb.tt", File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb")
27
+ else
28
+ template "policy_test.rb.tt", File.join("test/policies", class_path, "#{file_name}_policy_test.rb")
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def policy_class_name
35
+ "#{class_name}Policy"
36
+ end
37
+
38
+ def model_class_name
39
+ class_name
40
+ end
41
+
42
+ def model_instance_name
43
+ file_name
44
+ end
45
+
46
+ def namespaced_policy_class
47
+ if class_path.empty?
48
+ policy_class_name
49
+ else
50
+ "#{class_path.map(&:camelize).join("::")}::#{policy_class_name}"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% if class_path.any? -%>
4
+ module <%= class_path.map(&:camelize).join('::') %>
5
+ <% end -%>
6
+ class <%= policy_class_name %> < ApplicationPolicy
7
+ def index?
8
+ false
9
+ end
10
+
11
+ def show?
12
+ false
13
+ end
14
+
15
+ def create?
16
+ false
17
+ end
18
+
19
+ def update?
20
+ false
21
+ end
22
+
23
+ def destroy?
24
+ false
25
+ end
26
+
27
+ def permitted_attributes
28
+ []
29
+ end
30
+
31
+ class Scope < ApplicationPolicy::Scope
32
+ def resolve
33
+ scope.all
34
+ end
35
+ end
36
+ end
37
+ <% if class_path.any? -%>
38
+ end
39
+ <% end -%>
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+
5
+ <% if class_path.any? -%>
6
+ module <%= class_path.map(&:camelize).join('::') %>
7
+ <% end -%>
8
+ RSpec.describe <%= policy_class_name %>, type: :policy do
9
+ subject(:policy) { described_class.new(user, <%= model_instance_name %>) }
10
+
11
+ let(:user) { User.new }
12
+ let(:<%= model_instance_name %>) { <%= model_class_name %>.new }
13
+
14
+ describe "#index?" do
15
+ it "denies access by default" do
16
+ expect(policy).not_to permit_action(:index)
17
+ end
18
+
19
+ # TODO: Add your authorization logic tests
20
+ end
21
+
22
+ describe "#show?" do
23
+ it "denies access by default" do
24
+ expect(policy).not_to permit_action(:show)
25
+ end
26
+
27
+ # TODO: Add your authorization logic tests
28
+ end
29
+
30
+ describe "#create?" do
31
+ it "denies access by default" do
32
+ expect(policy).not_to permit_action(:create)
33
+ end
34
+
35
+ # TODO: Add your authorization logic tests
36
+ end
37
+
38
+ describe "#update?" do
39
+ it "denies access by default" do
40
+ expect(policy).not_to permit_action(:update)
41
+ end
42
+
43
+ # TODO: Add your authorization logic tests
44
+ end
45
+
46
+ describe "#destroy?" do
47
+ it "denies access by default" do
48
+ expect(policy).not_to permit_action(:destroy)
49
+ end
50
+
51
+ # TODO: Add your authorization logic tests
52
+ end
53
+
54
+ describe "#permitted_attributes" do
55
+ it "returns empty array by default" do
56
+ expect(policy.permitted_attributes).to eq([])
57
+ end
58
+
59
+ # TODO: Add your permitted attributes tests
60
+ end
61
+
62
+ describe "Scope" do
63
+ subject(:scope) { <%= namespaced_policy_class %>::Scope.new(user, <%= model_class_name %>.all) }
64
+
65
+ it "returns all records by default" do
66
+ # TODO: Add your scope tests
67
+ expect(scope.resolve).to eq(<%= model_class_name %>.all)
68
+ end
69
+ end
70
+ end
71
+ <% if class_path.any? -%>
72
+ end
73
+ <% end -%>
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ <% if class_path.any? -%>
6
+ module <%= class_path.map(&:camelize).join('::') %>
7
+ <% end -%>
8
+ class <%= policy_class_name %>Test < ActiveSupport::TestCase
9
+ def setup
10
+ @user = User.new
11
+ @<%= model_instance_name %> = <%= model_class_name %>.new
12
+ end
13
+
14
+ test "index?" do
15
+ policy = <%= namespaced_policy_class %>.new(@user, @<%= model_instance_name %>)
16
+
17
+ # TODO: Add your authorization logic tests
18
+ refute policy.index?
19
+ end
20
+
21
+ test "show?" do
22
+ policy = <%= namespaced_policy_class %>.new(@user, @<%= model_instance_name %>)
23
+
24
+ # TODO: Add your authorization logic tests
25
+ refute policy.show?
26
+ end
27
+
28
+ test "create?" do
29
+ policy = <%= namespaced_policy_class %>.new(@user, @<%= model_instance_name %>)
30
+
31
+ # TODO: Add your authorization logic tests
32
+ refute policy.create?
33
+ end
34
+
35
+ test "update?" do
36
+ policy = <%= namespaced_policy_class %>.new(@user, @<%= model_instance_name %>)
37
+
38
+ # TODO: Add your authorization logic tests
39
+ refute policy.update?
40
+ end
41
+
42
+ test "destroy?" do
43
+ policy = <%= namespaced_policy_class %>.new(@user, @<%= model_instance_name %>)
44
+
45
+ # TODO: Add your authorization logic tests
46
+ refute policy.destroy?
47
+ end
48
+
49
+ test "scope" do
50
+ # TODO: Add your scope tests
51
+ # For example:
52
+ # scope = <%= namespaced_policy_class %>::Scope.new(@user, <%= model_class_name %>.all)
53
+ # assert_equal expected_records, scope.resolve
54
+ end
55
+
56
+ test "permitted_attributes" do
57
+ policy = <%= namespaced_policy_class %>.new(@user, @<%= model_instance_name %>)
58
+
59
+ # TODO: Add your permitted attributes tests
60
+ assert_equal [], policy.permitted_attributes
61
+ end
62
+ end
63
+ <% if class_path.any? -%>
64
+ end
65
+ <% end -%>
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleAuthorize
4
+ # Configuration options for SimpleAuthorize
4
5
  class Configuration
5
6
  # Default error message shown to users when not authorized
6
7
  attr_accessor :default_error_message
@@ -14,11 +15,31 @@ module SimpleAuthorize
14
15
  # Custom redirect path for unauthorized access
15
16
  attr_accessor :unauthorized_redirect_path
16
17
 
18
+ # Enable policy caching for performance optimization (opt-in)
19
+ attr_accessor :enable_policy_cache
20
+
21
+ # Enable instrumentation for authorization events (default: true)
22
+ attr_accessor :enable_instrumentation
23
+
24
+ # Include detailed error information in API responses (default: false)
25
+ attr_accessor :api_error_details
26
+
27
+ # Enable I18n support for error messages (default: false)
28
+ attr_accessor :i18n_enabled
29
+
30
+ # I18n scope for translations (default: 'simple_authorize')
31
+ attr_accessor :i18n_scope
32
+
17
33
  def initialize
18
34
  @default_error_message = "You are not authorized to perform this action."
19
35
  @auto_verify = false
20
36
  @current_user_method = :current_user
21
37
  @unauthorized_redirect_path = nil
38
+ @enable_policy_cache = false
39
+ @enable_instrumentation = true
40
+ @api_error_details = false
41
+ @i18n_enabled = false
42
+ @i18n_scope = "simple_authorize"
22
43
  end
23
44
  end
24
45