simple_authorize 1.0.1 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0016cdf20b078e1d8e9d67b037742260a3e07d8d73a8056aea027c182ed422dc
4
- data.tar.gz: 4653aae4a3e588e3ebb94e6afb16518793a2e5b5c167b117f5c8bf6517e2ee7f
3
+ metadata.gz: 1e7ec7dc4e13ab781a1379671cb043b2a03956c29e32554ff00624ee57cd009a
4
+ data.tar.gz: d0fb3ae9757d4ccd93250a09847630e5f4958c32498bff21047cf1e903cc4c19
5
5
  SHA512:
6
- metadata.gz: 8fdc1204e73d66f005d3f19893244712013300f12529b59a28e709f21552134e52e8efbb944197403be7832521cf983c15a7c1b8c46c3b2d3808dbbdc5511f2a
7
- data.tar.gz: 5da4732fc1838f7a495336b4380f3bb90a523a5922fe6130e3e6b6c50cd0d2fd3d37f17f083de74ea8128871aac18f2d3a9ca7ef1e5196b225371c82e6ddd4cd
6
+ metadata.gz: ca8717f9564cdbe1fe7873e7c5c7f10452a8825d83170565f306272e9b5cfcbd18adf181081e3603c58e206eb743e6ee4b4b8d1a4fd477a11d5946a5bd793fea
7
+ data.tar.gz: cbe20818849f24ee61dec25ec75f08ba22c1bf75f5c9c2c1021cdaea3548aa01b084e009d16391c60568897148f13d2ba95e1c2817ae6c9697dea29fec9983a8
data/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.0] - 2025-11-05
9
+
10
+ ### Added
11
+
12
+ #### Policy Composition
13
+ - **Reusable Policy Modules** - Built-in modules for common authorization patterns
14
+ - **Ownable Module** - Ownership-based authorization helpers
15
+ - **Publishable Module** - Publishing workflow authorization
16
+ - **Timestamped Module** - Time-based access control
17
+ - **Approvable Module** - Approval workflow helpers
18
+ - **SoftDeletable Module** - Soft deletion authorization
19
+ - **Custom Module Support** - Easy creation of custom authorization modules
20
+
21
+ #### Context-Aware Policies
22
+ - **Request Context** - Pass additional context to policies (IP, time, location, etc.)
23
+ - **Controller Integration** - `authorization_context` method for building context
24
+ - **Context in Scopes** - Context available in Policy::Scope classes
25
+ - **Common Patterns** - Built-in support for geographic restrictions, time-based access, rate limiting
26
+
8
27
  ## [1.0.0] - 2025-11-03
9
28
 
10
29
  ### Added
data/README.md CHANGED
@@ -11,6 +11,8 @@ SimpleAuthorize is a lightweight, powerful authorization framework for Rails tha
11
11
  - **Policy-Based Authorization** - Define authorization rules in dedicated policy classes
12
12
  - **Scope Filtering** - Automatically filter collections based on user permissions
13
13
  - **Role-Based Access** - Built-in support for role-based authorization
14
+ - **Policy Composition** - Mix and match reusable authorization modules
15
+ - **Context-Aware Policies** - Make authorization decisions based on request context (IP, time, location, etc.)
14
16
  - **Zero Dependencies** - No external gems required (only Rails)
15
17
  - **Strong Parameters Integration** - Automatically build permitted params from policies
16
18
  - **Test Friendly** - Easy to test policies in isolation
@@ -394,6 +396,217 @@ SimpleAuthorize.configure do |config|
394
396
  end
395
397
  ```
396
398
 
399
+ ## Policy Composition
400
+
401
+ Policy Composition allows you to build complex authorization policies by combining reusable modules. This promotes DRY code and consistent authorization patterns across your application.
402
+
403
+ ### Using Built-in Policy Modules
404
+
405
+ SimpleAuthorize provides several ready-to-use policy modules:
406
+
407
+ ```ruby
408
+ class ArticlePolicy < ApplicationPolicy
409
+ include SimpleAuthorize::PolicyModules::Ownable
410
+ include SimpleAuthorize::PolicyModules::Publishable
411
+
412
+ def show?
413
+ published? || owner_or_admin?
414
+ end
415
+
416
+ def update?
417
+ owner_or_admin? && not_published?
418
+ end
419
+ end
420
+ ```
421
+
422
+ ### Available Policy Modules
423
+
424
+ #### Ownable
425
+ Provides ownership-based authorization:
426
+ - `owner?` - Check if user owns the record
427
+ - `owner_or_admin?` - Check if user is owner or admin
428
+ - `can_modify?` - Common pattern for modification rights
429
+
430
+ #### Publishable
431
+ For content with draft/published states:
432
+ - `published?` - Check if record is published
433
+ - `can_publish?` - Check if user can publish
434
+ - `can_preview?` - Check if user can preview drafts
435
+
436
+ #### Timestamped
437
+ Time-based authorization:
438
+ - `expired?` - Check if record has expired
439
+ - `within_time_window?` - Check if record is in valid time range
440
+ - `locked?` - Check if record is time-locked
441
+
442
+ #### Approvable
443
+ For approval workflows:
444
+ - `approved?` - Check if record is approved
445
+ - `can_approve?` - Check if user can approve (not their own content)
446
+ - `can_submit_for_approval?` - Check if user can submit for approval
447
+
448
+ #### SoftDeletable
449
+ For soft deletion support:
450
+ - `soft_deleted?` - Check if record is soft deleted
451
+ - `can_restore?` - Check if user can restore
452
+ - `can_permanently_destroy?` - Check if user can hard delete
453
+
454
+ ### Creating Custom Policy Modules
455
+
456
+ ```ruby
457
+ module MyApp::PolicyModules::Subscribable
458
+ protected
459
+
460
+ def subscriber?
461
+ user&.subscriptions&.active&.any?
462
+ end
463
+
464
+ def premium_subscriber?
465
+ user&.subscription&.premium?
466
+ end
467
+
468
+ def can_access_premium_content?
469
+ premium_subscriber? || admin?
470
+ end
471
+ end
472
+
473
+ class PremiumContentPolicy < ApplicationPolicy
474
+ include MyApp::PolicyModules::Subscribable
475
+
476
+ def show?
477
+ can_access_premium_content?
478
+ end
479
+ end
480
+ ```
481
+
482
+ ## Context-Aware Policies
483
+
484
+ Context-Aware Policies allow you to make authorization decisions based on additional context beyond just the user and record. This is useful for IP-based restrictions, time-based access, rate limiting, and more.
485
+
486
+ ### Basic Usage
487
+
488
+ Override the `authorization_context` method in your controller:
489
+
490
+ ```ruby
491
+ class ApplicationController < ActionController::Base
492
+ include SimpleAuthorize::Controller
493
+
494
+ private
495
+
496
+ def authorization_context
497
+ {
498
+ ip_address: request.remote_ip,
499
+ user_agent: request.user_agent,
500
+ current_time: Time.current,
501
+ country: request.location&.country,
502
+ two_factor_verified: session[:two_factor_verified],
503
+ user_plan: current_user&.subscription&.plan
504
+ }
505
+ end
506
+ end
507
+ ```
508
+
509
+ ### Using Context in Policies
510
+
511
+ Access context in your policies through the `context` method:
512
+
513
+ ```ruby
514
+ class SecureDocumentPolicy < ApplicationPolicy
515
+ def show?
516
+ # Require 2FA for sensitive documents
517
+ return false unless context[:two_factor_verified]
518
+
519
+ # Check IP restrictions
520
+ return false unless trusted_ip?
521
+
522
+ owner_or_admin?
523
+ end
524
+
525
+ private
526
+
527
+ def trusted_ip?
528
+ return true if context[:ip_address].nil?
529
+
530
+ trusted_ips = ["192.168.1.0/24", "10.0.0.0/8"]
531
+ trusted_ips.any? { |range| IPAddr.new(range).include?(context[:ip_address]) }
532
+ end
533
+ end
534
+ ```
535
+
536
+ ### Common Context Patterns
537
+
538
+ #### Geographic Restrictions
539
+ ```ruby
540
+ class RegionalContentPolicy < ApplicationPolicy
541
+ def show?
542
+ allowed_countries = ["US", "CA", "UK"]
543
+ allowed_countries.include?(context[:country]) || admin?
544
+ end
545
+ end
546
+ ```
547
+
548
+ #### Time-Based Access
549
+ ```ruby
550
+ class BusinessHoursPolicy < ApplicationPolicy
551
+ def create?
552
+ return true if admin?
553
+
554
+ hour = context[:current_time].hour
555
+ hour >= 9 && hour < 17 # 9 AM to 5 PM only
556
+ end
557
+ end
558
+ ```
559
+
560
+ #### Rate Limiting
561
+ ```ruby
562
+ class ApiPolicy < ApplicationPolicy
563
+ def create?
564
+ return true if admin?
565
+
566
+ request_count = context[:request_count] || 0
567
+ request_count < 100 # Limit to 100 requests
568
+ end
569
+ end
570
+ ```
571
+
572
+ #### Plan-Based Features
573
+ ```ruby
574
+ class ExportPolicy < ApplicationPolicy
575
+ def export?
576
+ case context[:user_plan]
577
+ when "enterprise"
578
+ true
579
+ when "pro"
580
+ owner_or_admin?
581
+ when "basic"
582
+ admin?
583
+ else
584
+ false
585
+ end
586
+ end
587
+ end
588
+ ```
589
+
590
+ ### Context with Policy Scopes
591
+
592
+ Context is also available in policy scopes:
593
+
594
+ ```ruby
595
+ class DocumentPolicy < ApplicationPolicy
596
+ class Scope < ApplicationPolicy::Scope
597
+ def resolve
598
+ if context[:department]
599
+ scope.where(department: context[:department])
600
+ elsif user.admin?
601
+ scope.all
602
+ else
603
+ scope.where(user: user)
604
+ end
605
+ end
606
+ end
607
+ end
608
+ ```
609
+
397
610
  ## Advanced Features
398
611
 
399
612
  ### Headless Policies
@@ -141,9 +141,9 @@ module SimpleAuthorize
141
141
  if SimpleAuthorize.configuration.enable_policy_cache
142
142
  policy_cache_key = build_policy_cache_key(record, policy_class)
143
143
  @_policy_cache ||= {}
144
- @_policy_cache[policy_cache_key] ||= policy_class.new(authorized_user, record)
144
+ @_policy_cache[policy_cache_key] ||= policy_class.new(authorized_user, record, context: authorization_context)
145
145
  else
146
- policy_class.new(authorized_user, record)
146
+ policy_class.new(authorized_user, record, context: authorization_context)
147
147
  end
148
148
  rescue NameError
149
149
  raise PolicyNotDefinedError, "unable to find policy `#{policy_class}` for `#{record}`"
@@ -163,7 +163,7 @@ module SimpleAuthorize
163
163
  error = nil
164
164
 
165
165
  begin
166
- result = policy_scope_class.new(authorized_user, scope).resolve
166
+ result = policy_scope_class.new(authorized_user, scope, context: authorization_context).resolve
167
167
  rescue NameError
168
168
  error = PolicyNotDefinedError.new("unable to find scope `#{policy_scope_class}` for `#{scope}`")
169
169
  end
@@ -277,6 +277,26 @@ module SimpleAuthorize
277
277
  current_user
278
278
  end
279
279
 
280
+ # Build context for authorization (can be overridden)
281
+ # Override this method in your ApplicationController to provide
282
+ # context data for policies
283
+ #
284
+ # Example:
285
+ # def authorization_context
286
+ # {
287
+ # ip_address: request.remote_ip,
288
+ # user_agent: request.user_agent,
289
+ # subdomain: request.subdomain,
290
+ # current_time: Time.current,
291
+ # request_count: rate_limiter.count_for(current_user),
292
+ # two_factor_verified: session[:two_factor_verified],
293
+ # user_plan: current_user&.subscription&.plan
294
+ # }
295
+ # end
296
+ def authorization_context
297
+ {}
298
+ end
299
+
280
300
  # Clear the policy cache
281
301
  def clear_policy_cache
282
302
  @_policy_cache = nil
@@ -5,9 +5,10 @@ module SimpleAuthorize
5
5
  class Policy
6
6
  attr_reader :user, :record
7
7
 
8
- def initialize(user, record)
8
+ def initialize(user, record, context: nil)
9
9
  @user = user
10
10
  @record = record
11
+ @context = context
11
12
  end
12
13
 
13
14
  # Default policies - deny everything by default
@@ -64,6 +65,10 @@ module SimpleAuthorize
64
65
  # Helper methods
65
66
  protected
66
67
 
68
+ def context
69
+ @context || {}
70
+ end
71
+
67
72
  def admin?
68
73
  user&.admin?
69
74
  end
@@ -96,9 +101,10 @@ module SimpleAuthorize
96
101
  class Scope
97
102
  attr_reader :user, :scope
98
103
 
99
- def initialize(user, scope)
104
+ def initialize(user, scope, context: nil)
100
105
  @user = user
101
106
  @scope = scope
107
+ @context = context
102
108
  end
103
109
 
104
110
  def resolve
@@ -107,6 +113,10 @@ module SimpleAuthorize
107
113
 
108
114
  protected
109
115
 
116
+ def context
117
+ @context || {}
118
+ end
119
+
110
120
  def admin?
111
121
  user&.admin?
112
122
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleAuthorize
4
- VERSION = "1.0.1"
4
+ VERSION = "1.1.0"
5
5
  end
@@ -5,6 +5,7 @@ require_relative "simple_authorize/version"
5
5
  require_relative "simple_authorize/configuration"
6
6
  require_relative "simple_authorize/controller"
7
7
  require_relative "simple_authorize/policy"
8
+ require_relative "simple_authorize/policy_modules"
8
9
  require_relative "simple_authorize/test_helpers"
9
10
 
10
11
  # SimpleAuthorize provides a lightweight authorization framework for Rails applications
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_authorize
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-11-04 00:00:00.000000000 Z
11
+ date: 2025-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport