Permission Rules
Learn how to implement authorization and access control using Ruleur.
Access Control Principle: Deny by Default
In access control, the default should always be deny. Only grant access when conditions are explicitly met:
engine = Ruleur.define do
# Access is only granted if this rule fires
rule 'admin_update' do
match { all?(user(:admin?)) }
actions { set :update, true }
end
end
result = engine.run(user: guest, record: doc)
result[:update] # => nil (no rule matched, so denied)2
3
4
5
6
7
8
9
10
11
This is a fundamental security principle: assume no access unless explicitly granted.
Overview
Permission rules help you:
- Implement role-based access control (RBAC)
- Check resource ownership
- Enforce hierarchical permissions
- Combine multiple authorization factors
Basic Permission Check
Simple Role Check
engine = Ruleur.define do
rule 'admin_access' do
match { all?(user(:admin?)) }
actions { set :admin_access, true }
end
end
result = engine.run(user: current_user)
result[:admin_access] # => true or nil2
3
4
5
6
7
8
9
10
Multiple Roles
engine = Ruleur.define do
rule 'staff_access' do
conditions do
any?(
user(:admin?),
user(:moderator?),
user(:support?)
)
end
actions { set :staff_access, true }
end
end2
3
4
5
6
7
8
9
10
11
12
13
Resource Ownership
Owner Can Update
engine = Ruleur.define do
rule 'owner_update' do
conditions do
all?(
user(:owns?, record),
not?(record(:locked?))
)
end
actions do
set :update, true
end
end
end
result = engine.run(user: current_user, record: post)
result[:update] # => true or nil2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Admin or Owner
engine = Ruleur.define do
rule 'admin_or_owner_destroy' do
conditions do
any?(
user(:admin?),
all?(
user(:owns?, record),
record(:deletable?)
)
)
end
actions do
set :destroy, true
end
end
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Hierarchical Permissions
Tiered Access
engine = Ruleur.define do
rule 'authenticated_show' do
conditions do
all?(user(:authenticated?))
end
actions do
set :show, true
end
end
rule 'contributor_update' do
conditions do
any?(
user(:contributor?),
user(:maintainer?),
user(:admin?)
)
end
actions do
set :update, true
end
end
rule 'maintainer_destroy' do
conditions do
any?(
user(:maintainer?),
user(:admin?)
)
end
actions do
set :destroy, true
end
end
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Complex Permission Logic
Multi-Factor Authorization
engine = Ruleur.define do
rule 'standard_approve', salience: 10 do
conditions do
all?(
include?(user_value(:role), %w[approver admin]),
not?(eq?(record_value(:author_id), user_value(:id))),
eq?(record_value(:status), 'pending_approval'),
record(:complete?),
gte(Time.current.hour, 9),
lt(Time.current.hour, 17)
)
end
actions do
set :approve, true
end
end
rule 'emergency_approve', salience: 20 do
conditions do
all?(
user(:admin?),
eq?(record_value(:status), 'pending_approval'),
flag(:emergency_mode)
)
end
actions do
set :approve, true
end
end
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Feature Flags
Tiered Features
engine = Ruleur.define do
rule 'basic_features' do
conditions do
all?(user(:subscription_active?))
end
actions do
set :basic_export, true
set :basic_analytics, true
end
end
rule 'premium_features' do
conditions do
all?(
include?(user_value(:subscription_tier), %w[premium enterprise]),
user(:subscription_active?)
)
end
actions do
set :advanced_export, true
set :custom_reports, true
set :api_access, true
end
end
rule 'enterprise_features' do
conditions do
all?(
eq?(user_value(:subscription_tier), 'enterprise'),
user(:subscription_active?)
)
end
actions do
set :white_label, true
set :sso, true
set :audit_logs, true
end
end
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Time-Based Permissions
Business Hours
engine = Ruleur.define do
rule 'business_hours_access' do
conditions do
all?(
user(:employee?),
in?([1, 2, 3, 4, 5], [Time.current.wday]),
gte(Time.current.hour, 9),
lt(Time.current.hour, 17)
)
end
actions do
set :system_access, true
end
end
rule 'after_hours_admin' do
conditions do
all?(user(:admin?))
end
actions do
set :system_access, true
end
end
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Real-World Example: Blog Authorization
class BlogPolicy
def self.engine
@engine ||= Ruleur.define do
rule 'published_show' do
conditions do
all?(record(:published?))
end
actions do
set :show, true
end
end
rule 'own_draft_show' do
conditions do
all?(
user(:owns?, record),
record(:draft?)
)
end
actions do
set :show, true
end
end
rule 'own_draft_update' do
conditions do
all?(
user(:owns?, record),
record(:draft?),
not?(record(:locked?))
)
end
actions do
set :update, true
set :destroy, true
end
end
rule 'editor_update' do
conditions do
all?(
include?(user_value(:role), %w[editor admin]),
not?(record(:archived?))
)
end
actions do
set :update, true
set :publish, true
end
end
rule 'admin_crud' do
conditions do
all?(user(:admin?))
end
actions do
set :show, true
set :update, true
set :destroy, true
set :publish, true
end
end
end
end
def self.authorize?(user, record, action)
result = engine.run(user: user, record: record)
result[action] == true
end
end
if BlogPolicy.authorize(current_user, @post, :update)
# Allow updating
else
# Deny access (implicit by default)
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
Testing Permissions
RSpec.describe BlogPolicy do
let(:engine) { BlogPolicy.engine }
describe ':update' do
it 'grants update to owner of draft' do
user = User.new(id: 1)
record = Post.new(author_id: 1, status: 'draft')
result = engine.run(user: user, record: record)
expect(result[:update]).to be true
end
it 'denies update to non-owner of published post' do
user = User.new(id: 1)
record = Post.new(author_id: 2, status: 'published')
result = engine.run(user: user, record: record)
expect(result[:update]).to be_nil
end
end
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Complex Pundit Comparison
Let's compare Pundit's traditional if-then-else approach with a Business Rules Management System using a complex, real-world scenario.
The Scenario: Enterprise Document Permissions
Imagine a document management system with complex permission rules:
- Admins have full access
- Editors can modify documents they own or documents in their department
- Viewers can see documents shared with them or public documents
- Documents have lifecycle states: draft, review, published, archived
- Special rules for confidential documents
- Department-specific overrides
- Time-based rules (documents expire after certain dates)
- Audit requirements for sensitive operations
The Pundit Approach (If-Then-Else)
class DocumentPolicy < ApplicationPolicy
def create?
user.present?
end
def show?
return true if user.admin?
return false if record.confidential? && !user_clearance?
return true if record.public?
return true if record.owner == user
return true if user.department == record.department && record.visible_to_department?
user.document_accesses.exists?(document_id: record.id)
end
def update?
return false if record.archived?
return true if user.admin?
if record.draft?
return record.owner == user
elsif record.include? review?
return user.reviewer? || record.owner == user
elsif record.published?
return record.owner == user if user.editor?
return false
end
false
end
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Problems with this approach:
- Logic duplication: Similar checks appear in multiple methods
- Hidden dependencies: Private helpers aren't obvious
- Impossible to audit: Where do you even start to understand what an "admin" can do?
- Testing nightmare: Complex mocks required
- No versioning: Changing one rule might break another silently
- Business analysts can't read it: Ruby code isn't business-friendly
The Ruleur Approach
With Ruleur, you only define when a value is set. If no rule matches, the value remains unset:
engine = Ruleur.define do
rule 'admin_crud', salience: 100, no_loop: true, tags: [:admin] do
conditions do
all?(user(:admin?))
end
actions do
set :show, true
set :create, true
set :update, true
set :destroy, true
end
end
rule 'draft_owner_crud', salience: 50, no_loop: true, tags: %i[ownership draft] do
conditions do
all?(
record(:draft?),
eq?(record_value(:owner_id), user_value(:id))
)
end
actions do
set :show, true
set :update, true
set :destroy, true
end
end
rule 'review_owner_update', salience: 50, no_loop: true, tags: %i[lifecycle review] do
conditions do
all?(
record(:in_review?),
eq?(record_value(:owner_id), user_value(:id))
)
end
actions do
set :update, true
end
end
rule 'review_approver_update', salience: 45, no_loop: true, tags: %i[lifecycle review] do
conditions do
all?(
record(:in_review?),
user(:approver?),
eq?(record_value(:department_id), user_value(:department_id))
)
end
actions do
set :update, true
end
end
rule 'published_show', salience: 50, no_loop: true, tags: %i[lifecycle published] do
conditions do
all?(record(:published?))
end
actions do
set :show, true
end
end
rule 'published_owner_destroy', salience: 45, no_loop: true, tags: %i[lifecycle published] do
conditions do
all?(
record(:published?),
eq?(record_value(:owner_id), user_value(:id))
)
end
actions do
set :destroy, true
end
end
rule 'owner_crud', salience: 40, no_loop: true, tags: [:ownership] do
conditions do
all?(eq?(record_value(:owner_id), user_value(:id)))
end
actions do
set :show, true
set :update, true
set :destroy, true
end
end
rule 'department_show', salience: 30, no_loop: true, tags: [:department] do
conditions do
all?(
record(:visible_to_department?),
eq?(record_value(:department_id), user_value(:department_id))
)
end
actions do
set :show, true
end
end
rule 'shared_show', salience: 25, no_loop: true, tags: [:sharing] do
conditions do
all?(record(:shared_with_user))
end
actions do
set :show, true
end
end
rule 'public_show', salience: 20, no_loop: true, tags: [:visibility] do
conditions do
all?(record(:public?))
end
actions do
set :show, true
end
end
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
Pundit Integration with Ruleur
class DocumentPolicy < ApplicationPolicy
def create?
ctx[:create] == true
end
def show?
ctx[:show] == true
end
def update?
ctx[:update] == true
end
def destroy?
ctx[:destroy] == true
end
private
def ctx
@ctx ||= Ruleur::Config.engine.run(record: record, user: user)
end
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Benefits of the BRMS Approach
| Aspect | Pundit (If-Then-Else) | Ruleur (BRMS) |
|---|---|---|
| Readability | Logic buried in Ruby code | Business rules in declarative DSL |
| Auditability | Scattered across methods | All rules visible in one place |
| Testability | Need complex mocks | Test rules in isolation with simple data |
| Versioning | Git diffs are unreadable | Dedicated versioning with audit trail |
| Business User Access | Requires developer | YAML/UI makes it accessible |
| Conflict Resolution | Implicit (first match wins) | Explicit via salience |
| Testing | Integration tests required | Unit test each rule independently |
| Security | Easy to miss implicit denies | Deny-by-default is explicit |
Testing: Pundit vs Ruleur
Pundit test (complex setup required):
RSpec.describe DocumentPolicy do
let(:user) { User.new(admin: false, department: dept) }
let(:document) { Document.new(owner: user, state: :draft) }
describe '#update?' do
context 'when user is owner and document is draft' do
it { expect(subject.update?).to be true }
end
context 'when document is published' do
let(:document) { Document.new(owner: user, state: :published) }
it { expect(subject.update?).to be false }
end
# ... dozens more contexts
end
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Ruleur test (simple, focused):
RSpec.describe 'Document Permissions' do
let(:engine) { DocumentPolicy.engine }
it 'grants update to owner of draft' do
result = engine.run(user: owner, record: draft_doc)
expect(result[:update]).to be true
end
it 'denies update to non-owner of published' do
result = engine.run(user: other_user, record: published_doc)
expect(result[:update]).to be_nil
end
it 'grants update to admin regardless of document state' do
result = engine.run(user: admin, record: published_doc)
expect(result[:update]).to be true
end
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Security Best Practices
1. Set Values Explicitly
Only set values when conditions are met. Don't use set :key, false:
# Avoid: Using false values
rule 'not_authenticated' do
conditions do
all?(not?(user(:authenticated?)))
end
actions do
set :update, false
end
end
# Better: Only set when true
rule 'authenticated_update' do
conditions do
all?(user(:authenticated?))
end
actions do
set :update, true
end
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2. Order Conditions by Cost
Place cheap/fast checks before expensive ones. This avoids unnecessary work:
# Bad: Expensive check first
rule 'check_permission' do
conditions do
all?(
expensive_database_query(:has_permission?), # Expensive - do last
user(:admin?) # Cheap - check first
)
end
actions do
set :update, true
end
end
# Good: Cheap checks first
rule 'check_permission' do
conditions do
all?(
user(:admin?), # Cheap - check first
expensive_database_query(:has_permission?) # Expensive - only if needed
)
end
actions do
set :update, true
end
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
3. Use Salience for Priority
Place high-priority rules (like admin bypass) at high salience so they fire first:
rule 'admin_crud', salience: 100 do
conditions do
all?(user(:admin?))
end
actions do
set :show, true
set :create, true
set :update, true
set :destroy, true
end
end2
3
4
5
6
7
8
9
10
11
12
4. Test Both Set and Unset Cases
it 'grants update to admin' do
expect(engine.run(user: admin, record: doc)[:update]).to be true
end
it 'denies update to guest' do
expect(engine.run(user: guest, record: doc)[:update]).to be_nil
end2
3
4
5
6
7
5. Audit Your Rules
Ruleur makes it easy to review all access rules in one place. Regularly audit:
- Are all permission grants intentional?
- Is the salience ordering correct?
- Are there any gaps in coverage?
See Also
- OWASP: Broken Access Control - Security principle
- Workflow Automation - Approval workflows
- Conditions Guide - Complex conditions
- DSL Basics - DSL syntax