simple_authorize 1.0.1 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0016cdf20b078e1d8e9d67b037742260a3e07d8d73a8056aea027c182ed422dc
4
- data.tar.gz: 4653aae4a3e588e3ebb94e6afb16518793a2e5b5c167b117f5c8bf6517e2ee7f
3
+ metadata.gz: b16462d63472a968792e6272f876b82beba2fadb48a675a5cb2d8d03dedec42f
4
+ data.tar.gz: 42553420c691366876062a93597613386542b26f997b42424d4fb8233aa09802
5
5
  SHA512:
6
- metadata.gz: 8fdc1204e73d66f005d3f19893244712013300f12529b59a28e709f21552134e52e8efbb944197403be7832521cf983c15a7c1b8c46c3b2d3808dbbdc5511f2a
7
- data.tar.gz: 5da4732fc1838f7a495336b4380f3bb90a523a5922fe6130e3e6b6c50cd0d2fd3d37f17f083de74ea8128871aac18f2d3a9ca7ef1e5196b225371c82e6ddd4cd
6
+ metadata.gz: 4013198e95c910dd1377835fab2701494666217ee87081621e6121e3a9750cf41cbad210583c6ff52ef95d5d7be7c9350e5966ccb350cc877da64b1f482ac2f8
7
+ data.tar.gz: 5fdc05879861c05aa16763b5fa7d3331e7fed2cf6d4e8443f7109fac0570f979dd584073774a557333157544f8ec223386fe6705ea64abc80f5283abb586cbd4
data/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ 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.1] - 2025-11-05
9
+
10
+ ### Fixed
11
+ - RuboCop compliance for all new policy modules
12
+ - Method naming convention (`standard_permissions` instead of `standard_permissions?`)
13
+ - Simplified conditional logic in Approvable module
14
+ - Code style improvements across test files
15
+
16
+ ## [1.1.0] - 2025-11-05
17
+
18
+ ### Added
19
+
20
+ #### Policy Composition
21
+ - **Reusable Policy Modules** - Built-in modules for common authorization patterns
22
+ - **Ownable Module** - Ownership-based authorization helpers
23
+ - **Publishable Module** - Publishing workflow authorization
24
+ - **Timestamped Module** - Time-based access control
25
+ - **Approvable Module** - Approval workflow helpers
26
+ - **SoftDeletable Module** - Soft deletion authorization
27
+ - **Custom Module Support** - Easy creation of custom authorization modules
28
+
29
+ #### Context-Aware Policies
30
+ - **Request Context** - Pass additional context to policies (IP, time, location, etc.)
31
+ - **Controller Integration** - `authorization_context` method for building context
32
+ - **Context in Scopes** - Context available in Policy::Scope classes
33
+ - **Common Patterns** - Built-in support for geographic restrictions, time-based access, rate limiting
34
+
8
35
  ## [1.0.0] - 2025-11-03
9
36
 
10
37
  ### Added
data/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ - do not automatically commit or push to the repo, please
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
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuthorize
4
+ module PolicyModules
5
+ # Provides approval workflow authorization methods
6
+ #
7
+ # Include this module for content that requires approval:
8
+ #
9
+ # class DocumentPolicy < SimpleAuthorize::Policy
10
+ # include SimpleAuthorize::PolicyModules::Approvable
11
+ #
12
+ # def update?
13
+ # not_approved? && (owner? || admin?)
14
+ # end
15
+ # end
16
+ #
17
+ # Assumes records have approval-related fields like approved, approved_at,
18
+ # approval_status, pending_approval, etc.
19
+ module Approvable
20
+ protected
21
+
22
+ # Check if the record is approved
23
+ def approved?
24
+ return false unless record
25
+
26
+ if record.respond_to?(:approved?)
27
+ record.approved?
28
+ elsif record.respond_to?(:approved)
29
+ record.approved == true
30
+ elsif record.respond_to?(:approval_status)
31
+ record.approval_status == "approved"
32
+ elsif record.respond_to?(:approved_at)
33
+ record.approved_at.present?
34
+ else
35
+ false
36
+ end
37
+ end
38
+
39
+ # Check if the record is pending approval
40
+ def pending_approval?
41
+ return false unless record
42
+
43
+ if record.respond_to?(:pending_approval?)
44
+ record.pending_approval?
45
+ elsif record.respond_to?(:pending_approval)
46
+ record.pending_approval == true
47
+ elsif record.respond_to?(:approval_status)
48
+ record.approval_status == "pending"
49
+ elsif record.respond_to?(:submitted_for_approval_at)
50
+ record.submitted_for_approval_at.present? && !approved? && !rejected?
51
+ else
52
+ false
53
+ end
54
+ end
55
+
56
+ # Check if the record is rejected
57
+ def rejected?
58
+ return false unless record
59
+
60
+ if record.respond_to?(:rejected?)
61
+ record.rejected?
62
+ elsif record.respond_to?(:rejected)
63
+ record.rejected == true
64
+ elsif record.respond_to?(:approval_status)
65
+ record.approval_status == "rejected"
66
+ elsif record.respond_to?(:rejected_at)
67
+ record.rejected_at.present?
68
+ else
69
+ false
70
+ end
71
+ end
72
+
73
+ # Check if the record is not approved
74
+ def not_approved?
75
+ !approved?
76
+ end
77
+
78
+ # Check if user can approve (cannot approve own content)
79
+ def can_approve?
80
+ return false unless logged_in?
81
+
82
+ if admin?
83
+ true
84
+ elsif contributor?
85
+ # Contributors can approve others' content but not their own
86
+ !owner?
87
+ else
88
+ false
89
+ end
90
+ end
91
+
92
+ # Check if user can reject
93
+ def can_reject?
94
+ can_approve?
95
+ end
96
+
97
+ # Check if user can submit for approval
98
+ def can_submit_for_approval?
99
+ owner? && not_approved? && !pending_approval?
100
+ end
101
+
102
+ # Check if user can withdraw from approval
103
+ def can_withdraw_approval?
104
+ owner? && pending_approval?
105
+ end
106
+
107
+ # Check if content can be edited (typically not after approval)
108
+ def can_edit_with_approval?
109
+ if approved?
110
+ admin? # Only admins can edit approved content
111
+ else
112
+ owner? || admin? # Owner can edit rejected or unapproved content
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuthorize
4
+ module PolicyModules
5
+ # Provides ownership-based authorization methods
6
+ #
7
+ # Include this module in your policy to add owner-based permissions:
8
+ #
9
+ # class PostPolicy < SimpleAuthorize::Policy
10
+ # include SimpleAuthorize::PolicyModules::Ownable
11
+ #
12
+ # def show?
13
+ # published? || owner_or_admin?
14
+ # end
15
+ # end
16
+ #
17
+ # This module assumes your record has a `user_id` field that matches
18
+ # the user's ID. Override the `owner?` method if you need different logic.
19
+ module Ownable
20
+ protected
21
+
22
+ # Check if the current user owns the record
23
+ def owner?
24
+ return false unless user && record
25
+
26
+ if record.respond_to?(:user_id)
27
+ record.user_id == user.id
28
+ elsif record.respond_to?(:user)
29
+ record.user == user
30
+ elsif record.respond_to?(:owner_id)
31
+ record.owner_id == user.id
32
+ elsif record.respond_to?(:owner)
33
+ record.owner == user
34
+ else
35
+ false
36
+ end
37
+ end
38
+
39
+ # Check if the user is the owner or an admin
40
+ def owner_or_admin?
41
+ owner? || admin?
42
+ end
43
+
44
+ # Check if the user is the owner or a contributor
45
+ def owner_or_contributor?
46
+ owner? || contributor?
47
+ end
48
+
49
+ # Common pattern: owners and admins can modify
50
+ def can_modify?
51
+ owner_or_admin?
52
+ end
53
+
54
+ # Common pattern: anyone can view, but only owners/admins can modify
55
+ def standard_permissions
56
+ {
57
+ show: true,
58
+ create: logged_in?,
59
+ update: owner_or_admin?,
60
+ destroy: owner_or_admin?
61
+ }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuthorize
4
+ module PolicyModules
5
+ # Provides publishing workflow authorization methods
6
+ #
7
+ # Include this module for content that has draft/published states:
8
+ #
9
+ # class ArticlePolicy < SimpleAuthorize::Policy
10
+ # include SimpleAuthorize::PolicyModules::Publishable
11
+ #
12
+ # def show?
13
+ # published? || can_preview?
14
+ # end
15
+ # end
16
+ #
17
+ # This module assumes your record responds to `published?` or has a
18
+ # `published` boolean field, and optionally `published_at` timestamp.
19
+ module Publishable
20
+ protected
21
+
22
+ # Check if the record is published
23
+ def published?
24
+ return false unless record
25
+
26
+ if record.respond_to?(:published?)
27
+ record.published?
28
+ elsif record.respond_to?(:published)
29
+ record.published == true
30
+ elsif record.respond_to?(:status)
31
+ record.status == "published"
32
+ elsif record.respond_to?(:published_at)
33
+ record.published_at && record.published_at <= Time.current
34
+ else
35
+ false
36
+ end
37
+ end
38
+
39
+ # Check if the record is a draft
40
+ def draft?
41
+ !published?
42
+ end
43
+
44
+ # Check if user can publish content
45
+ def can_publish?
46
+ admin? || (contributor? && owner?)
47
+ end
48
+
49
+ # Check if user can unpublish content
50
+ def can_unpublish?
51
+ admin? || (owner? && contributor?)
52
+ end
53
+
54
+ # Check if user can preview unpublished content
55
+ def can_preview?
56
+ owner? || admin? || contributor?
57
+ end
58
+
59
+ # Check if user can schedule publication
60
+ def can_schedule?
61
+ return false unless record.respond_to?(:scheduled_at) || record.respond_to?(:publish_at)
62
+
63
+ can_publish?
64
+ end
65
+
66
+ # Filter attributes based on published state
67
+ def publishable_visible_attributes(base_attributes = [])
68
+ if published? || can_preview?
69
+ base_attributes
70
+ else
71
+ base_attributes - sensitive_draft_attributes
72
+ end
73
+ end
74
+
75
+ # Attributes that should be hidden in draft state
76
+ def sensitive_draft_attributes
77
+ %i[internal_notes draft_notes review_comments]
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuthorize
4
+ module PolicyModules
5
+ # Provides soft deletion authorization methods
6
+ #
7
+ # Include this module for records that support soft deletion:
8
+ #
9
+ # class CommentPolicy < SimpleAuthorize::Policy
10
+ # include SimpleAuthorize::PolicyModules::SoftDeletable
11
+ #
12
+ # def destroy?
13
+ # soft_deletable? && (owner? || admin?)
14
+ # end
15
+ # end
16
+ #
17
+ # Assumes records have a deleted_at timestamp or similar field.
18
+ module SoftDeletable
19
+ protected
20
+
21
+ # Check if the record is soft deleted
22
+ def soft_deleted?
23
+ return false unless record
24
+
25
+ if record.respond_to?(:deleted?)
26
+ record.deleted?
27
+ elsif record.respond_to?(:deleted_at)
28
+ record.deleted_at.present?
29
+ elsif record.respond_to?(:trashed?)
30
+ record.trashed?
31
+ elsif record.respond_to?(:archived?)
32
+ record.archived?
33
+ else
34
+ false
35
+ end
36
+ end
37
+
38
+ # Check if the record is not soft deleted
39
+ def not_deleted?
40
+ !soft_deleted?
41
+ end
42
+
43
+ # Check if the record supports soft deletion
44
+ def soft_deletable?
45
+ record && (
46
+ record.respond_to?(:deleted_at) ||
47
+ record.respond_to?(:deleted?) ||
48
+ record.respond_to?(:trash!) ||
49
+ record.respond_to?(:archive!)
50
+ )
51
+ end
52
+
53
+ # Check if user can restore soft deleted records
54
+ def can_restore?
55
+ soft_deleted? && (admin? || (owner? && within_restore_window?))
56
+ end
57
+
58
+ # Check if user can permanently delete
59
+ def can_permanently_destroy?
60
+ admin?
61
+ end
62
+
63
+ # Check if we're within the restore window (default 30 days)
64
+ def within_restore_window?(days = 30)
65
+ return true unless soft_deleted?
66
+ return true unless record.respond_to?(:deleted_at)
67
+
68
+ record.deleted_at > days.days.ago
69
+ end
70
+
71
+ # Check if user can view soft deleted records
72
+ def can_view_deleted?
73
+ admin? || (owner? && within_restore_window?)
74
+ end
75
+
76
+ # Standard destroy that respects soft delete
77
+ def safe_destroy?
78
+ if soft_deletable?
79
+ # Soft delete if supported
80
+ owner_or_admin?
81
+ else
82
+ # Hard delete requires admin
83
+ admin?
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuthorize
4
+ module PolicyModules
5
+ # Provides time-based authorization methods
6
+ #
7
+ # Include this module for time-sensitive permissions:
8
+ #
9
+ # class EventPolicy < SimpleAuthorize::Policy
10
+ # include SimpleAuthorize::PolicyModules::Timestamped
11
+ #
12
+ # def update?
13
+ # not_expired? && (owner? || admin?)
14
+ # end
15
+ # end
16
+ #
17
+ # Works with records that have timestamp fields like expired_at,
18
+ # starts_at, ends_at, valid_until, etc.
19
+ module Timestamped
20
+ protected
21
+
22
+ # Check if the record has expired
23
+ def expired?
24
+ return false unless record
25
+
26
+ if record.respond_to?(:expired_at)
27
+ record.expired_at && record.expired_at < Time.current
28
+ elsif record.respond_to?(:expires_at)
29
+ record.expires_at && record.expires_at < Time.current
30
+ elsif record.respond_to?(:valid_until)
31
+ record.valid_until && record.valid_until < Time.current
32
+ elsif record.respond_to?(:ends_at)
33
+ record.ends_at && record.ends_at < Time.current
34
+ else
35
+ false
36
+ end
37
+ end
38
+
39
+ # Check if the record is not expired
40
+ def not_expired?
41
+ !expired?
42
+ end
43
+
44
+ # Check if the record is active (started but not ended)
45
+ def active?
46
+ started? && !ended?
47
+ end
48
+
49
+ # Check if the record has started
50
+ def started?
51
+ return true unless record
52
+
53
+ if record.respond_to?(:starts_at)
54
+ record.starts_at.nil? || record.starts_at <= Time.current
55
+ elsif record.respond_to?(:available_from)
56
+ record.available_from.nil? || record.available_from <= Time.current
57
+ elsif record.respond_to?(:valid_from)
58
+ record.valid_from.nil? || record.valid_from <= Time.current
59
+ else
60
+ true
61
+ end
62
+ end
63
+
64
+ # Check if the record has ended
65
+ def ended?
66
+ expired?
67
+ end
68
+
69
+ # Check if record is within a time window
70
+ def within_time_window?
71
+ started? && not_expired?
72
+ end
73
+
74
+ # Check if the record is locked (cannot be modified)
75
+ def locked?
76
+ return false unless record
77
+
78
+ if record.respond_to?(:locked_at)
79
+ record.locked_at && record.locked_at < Time.current
80
+ elsif record.respond_to?(:locked?)
81
+ record.locked?
82
+ elsif record.respond_to?(:frozen_at)
83
+ record.frozen_at && record.frozen_at < Time.current
84
+ else
85
+ false
86
+ end
87
+ end
88
+
89
+ # Check if record can be modified (not locked or expired)
90
+ def can_modify_time_based?
91
+ !locked? && not_expired?
92
+ end
93
+
94
+ # Business hours check (useful with context)
95
+ def within_business_hours?(time = Time.current)
96
+ hour = time.hour
97
+ weekday = time.wday
98
+
99
+ # Monday-Friday, 9 AM - 5 PM
100
+ weekday.between?(1, 5) && hour >= 9 && hour < 17
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Require all policy modules
4
+ require_relative "policy_modules/ownable"
5
+ require_relative "policy_modules/publishable"
6
+ require_relative "policy_modules/timestamped"
7
+ require_relative "policy_modules/approvable"
8
+ require_relative "policy_modules/soft_deletable"
9
+
10
+ module SimpleAuthorize
11
+ # Collection of reusable policy modules for common authorization patterns
12
+ #
13
+ # These modules can be mixed into your policy classes to add common
14
+ # authorization functionality without duplicating code.
15
+ #
16
+ # Example:
17
+ # class ArticlePolicy < SimpleAuthorize::Policy
18
+ # include SimpleAuthorize::PolicyModules::Ownable
19
+ # include SimpleAuthorize::PolicyModules::Publishable
20
+ #
21
+ # def show?
22
+ # published? || owner_or_admin?
23
+ # end
24
+ # end
25
+ module PolicyModules
26
+ end
27
+ 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.1"
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
data/spec/examples.txt CHANGED
@@ -3,49 +3,49 @@ example_id | status | run_time |
3
3
  ./spec/rspec_matchers_spec.rb[1:1:1:1] | passed | 0.00003 seconds |
4
4
  ./spec/rspec_matchers_spec.rb[1:1:1:2] | passed | 0.00003 seconds |
5
5
  ./spec/rspec_matchers_spec.rb[1:1:1:3] | passed | 0.00004 seconds |
6
- ./spec/rspec_matchers_spec.rb[1:1:2:1] | passed | 0.00004 seconds |
6
+ ./spec/rspec_matchers_spec.rb[1:1:2:1] | passed | 0.00003 seconds |
7
7
  ./spec/rspec_matchers_spec.rb[1:1:2:2] | passed | 0.00004 seconds |
8
8
  ./spec/rspec_matchers_spec.rb[1:1:3:1] | passed | 0.00004 seconds |
9
9
  ./spec/rspec_matchers_spec.rb[1:1:3:2] | passed | 0.00004 seconds |
10
10
  ./spec/rspec_matchers_spec.rb[1:1:4:1] | passed | 0.00003 seconds |
11
- ./spec/rspec_matchers_spec.rb[1:1:4:2] | passed | 0.00012 seconds |
12
- ./spec/rspec_matchers_spec.rb[1:2:1:1] | passed | 0.00003 seconds |
11
+ ./spec/rspec_matchers_spec.rb[1:1:4:2] | passed | 0.00004 seconds |
12
+ ./spec/rspec_matchers_spec.rb[1:2:1:1] | passed | 0.00004 seconds |
13
13
  ./spec/rspec_matchers_spec.rb[1:2:1:2] | passed | 0.00004 seconds |
14
- ./spec/rspec_matchers_spec.rb[1:2:2:1] | passed | 0.00004 seconds |
15
- ./spec/rspec_matchers_spec.rb[1:2:2:2] | passed | 0.00003 seconds |
14
+ ./spec/rspec_matchers_spec.rb[1:2:2:1] | passed | 0.00005 seconds |
15
+ ./spec/rspec_matchers_spec.rb[1:2:2:2] | passed | 0.00014 seconds |
16
16
  ./spec/rspec_matchers_spec.rb[1:2:3:1] | passed | 0.00004 seconds |
17
- ./spec/rspec_matchers_spec.rb[1:2:3:2] | passed | 0.00004 seconds |
18
- ./spec/rspec_matchers_spec.rb[1:2:4:1] | passed | 0.00003 seconds |
19
- ./spec/rspec_matchers_spec.rb[1:2:4:2] | passed | 0.00003 seconds |
20
- ./spec/rspec_matchers_spec.rb[1:3:1:1] | passed | 0.00004 seconds |
21
- ./spec/rspec_matchers_spec.rb[1:3:1:2] | passed | 0.00011 seconds |
17
+ ./spec/rspec_matchers_spec.rb[1:2:3:2] | passed | 0.00006 seconds |
18
+ ./spec/rspec_matchers_spec.rb[1:2:4:1] | passed | 0.00004 seconds |
19
+ ./spec/rspec_matchers_spec.rb[1:2:4:2] | passed | 0.00004 seconds |
20
+ ./spec/rspec_matchers_spec.rb[1:3:1:1] | passed | 0.00028 seconds |
21
+ ./spec/rspec_matchers_spec.rb[1:3:1:2] | passed | 0.00028 seconds |
22
22
  ./spec/rspec_matchers_spec.rb[1:3:1:3] | passed | 0.00005 seconds |
23
- ./spec/rspec_matchers_spec.rb[1:3:2:1] | passed | 0.00004 seconds |
23
+ ./spec/rspec_matchers_spec.rb[1:3:2:1] | passed | 0.00034 seconds |
24
24
  ./spec/rspec_matchers_spec.rb[1:3:3:1] | passed | 0.00004 seconds |
25
- ./spec/rspec_matchers_spec.rb[1:3:3:2] | passed | 0.00005 seconds |
26
- ./spec/rspec_matchers_spec.rb[1:3:4:1] | passed | 0.00003 seconds |
25
+ ./spec/rspec_matchers_spec.rb[1:3:3:2] | passed | 0.00027 seconds |
26
+ ./spec/rspec_matchers_spec.rb[1:3:4:1] | passed | 0.0004 seconds |
27
27
  ./spec/rspec_matchers_spec.rb[1:3:4:2] | passed | 0.00003 seconds |
28
- ./spec/rspec_matchers_spec.rb[1:4:1:1] | passed | 0.00003 seconds |
28
+ ./spec/rspec_matchers_spec.rb[1:4:1:1] | passed | 0.00004 seconds |
29
29
  ./spec/rspec_matchers_spec.rb[1:4:2:1] | passed | 0.00003 seconds |
30
30
  ./spec/rspec_matchers_spec.rb[1:4:2:2] | passed | 0.00004 seconds |
31
31
  ./spec/rspec_matchers_spec.rb[1:4:3:1] | passed | 0.00004 seconds |
32
32
  ./spec/rspec_matchers_spec.rb[1:4:4:1] | passed | 0.00003 seconds |
33
33
  ./spec/rspec_matchers_spec.rb[1:4:4:2] | passed | 0.00003 seconds |
34
- ./spec/rspec_matchers_spec.rb[1:5:1:1] | passed | 0.00003 seconds |
34
+ ./spec/rspec_matchers_spec.rb[1:5:1:1] | passed | 0.00004 seconds |
35
35
  ./spec/rspec_matchers_spec.rb[1:5:1:2] | passed | 0.00003 seconds |
36
36
  ./spec/rspec_matchers_spec.rb[1:5:2:1] | passed | 0.00004 seconds |
37
- ./spec/rspec_matchers_spec.rb[1:5:3:1] | passed | 0.00004 seconds |
38
- ./spec/rspec_matchers_spec.rb[1:5:3:2] | passed | 0.00003 seconds |
37
+ ./spec/rspec_matchers_spec.rb[1:5:3:1] | passed | 0.00006 seconds |
38
+ ./spec/rspec_matchers_spec.rb[1:5:3:2] | passed | 0.00004 seconds |
39
39
  ./spec/rspec_matchers_spec.rb[1:5:4:1] | passed | 0.00003 seconds |
40
- ./spec/rspec_matchers_spec.rb[1:5:4:2] | passed | 0.00003 seconds |
41
- ./spec/rspec_matchers_spec.rb[1:6:1:1] | passed | 0.00003 seconds |
42
- ./spec/rspec_matchers_spec.rb[1:6:2:1] | passed | 0.00003 seconds |
40
+ ./spec/rspec_matchers_spec.rb[1:5:4:2] | passed | 0.00004 seconds |
41
+ ./spec/rspec_matchers_spec.rb[1:6:1:1] | passed | 0.0004 seconds |
42
+ ./spec/rspec_matchers_spec.rb[1:6:2:1] | passed | 0.00037 seconds |
43
43
  ./spec/rspec_matchers_spec.rb[1:6:2:2] | passed | 0.00004 seconds |
44
44
  ./spec/rspec_matchers_spec.rb[1:6:3:1] | passed | 0.00004 seconds |
45
45
  ./spec/rspec_matchers_spec.rb[1:6:3:2] | passed | 0.00004 seconds |
46
46
  ./spec/rspec_matchers_spec.rb[1:6:4:1] | passed | 0.00003 seconds |
47
- ./spec/rspec_matchers_spec.rb[1:6:4:2] | passed | 0.00003 seconds |
47
+ ./spec/rspec_matchers_spec.rb[1:6:4:2] | passed | 0.00035 seconds |
48
48
  ./spec/rspec_matchers_spec.rb[1:7:1:1] | passed | 0.00004 seconds |
49
- ./spec/rspec_matchers_spec.rb[1:7:1:2] | passed | 0.00004 seconds |
50
- ./spec/rspec_matchers_spec.rb[1:7:1:3] | passed | 0.00005 seconds |
51
- ./spec/rspec_matchers_spec.rb[1:7:2:1] | passed | 0.00118 seconds |
49
+ ./spec/rspec_matchers_spec.rb[1:7:1:2] | passed | 0.00003 seconds |
50
+ ./spec/rspec_matchers_spec.rb[1:7:1:3] | passed | 0.00035 seconds |
51
+ ./spec/rspec_matchers_spec.rb[1:7:2:1] | passed | 0.00128 seconds |
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.1
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
@@ -92,6 +92,7 @@ files:
92
92
  - ".overcommit.yml"
93
93
  - ".simplecov"
94
94
  - CHANGELOG.md
95
+ - CLAUDE.md
95
96
  - CODE_OF_CONDUCT.md
96
97
  - CONTRIBUTING.md
97
98
  - LICENSE.txt
@@ -110,6 +111,12 @@ files:
110
111
  - lib/simple_authorize/configuration.rb
111
112
  - lib/simple_authorize/controller.rb
112
113
  - lib/simple_authorize/policy.rb
114
+ - lib/simple_authorize/policy_modules.rb
115
+ - lib/simple_authorize/policy_modules/approvable.rb
116
+ - lib/simple_authorize/policy_modules/ownable.rb
117
+ - lib/simple_authorize/policy_modules/publishable.rb
118
+ - lib/simple_authorize/policy_modules/soft_deletable.rb
119
+ - lib/simple_authorize/policy_modules/timestamped.rb
113
120
  - lib/simple_authorize/railtie.rb
114
121
  - lib/simple_authorize/rspec.rb
115
122
  - lib/simple_authorize/test_helpers.rb