Skip to content

Commit 69a5f7c

Browse files
Add ABAC configuration docs and examples (#220)
* Add auth z config doc * Add examples with additional attributes in ABAC
1 parent 00bc578 commit 69a5f7c

1 file changed

Lines changed: 184 additions & 0 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Configuring Authorization Policies
2+
3+
This guide explains how to configure authorization policies in the application using Attribute-Based Access Control (ABAC). We use the [Pundit](https://github.com/varvet/pundit) gem to manage our authorization logic.
4+
5+
## Attribute-Based Access Control (ABAC)
6+
7+
ABAC is an authorization model that provides access rights to users based on attributes (characteristics) of the user, the resource, and the environment. In this application, we primarily use user attributes like `role` and `region` to determine access.
8+
9+
> [!IMPORTANT]
10+
> **Implementation Note**: The user attributes currently defined in the OSCER project (like `role` and `region`) are **representations** intended to demonstrate how ABAC can be structured. In a production environment, these attributes will likely be provided by an organization's Single Sign-On (SSO) integration (e.g., via OIDC claims or SAML assertions) and mapped to the `User` model during authentication.
11+
12+
### User Attributes
13+
14+
The `User` model (`app/models/user.rb`) defines several attributes and helper methods used for authorization:
15+
16+
- **`role`**: Defines the user's primary responsibility (e.g., `admin`, `caseworker`).
17+
- **`region`**: Defines the geographical area the user is assigned to.
18+
19+
#### Helper Methods
20+
21+
The `User` model provides convenient methods to check these attributes:
22+
23+
```ruby
24+
def admin?
25+
role == "admin"
26+
end
27+
28+
def caseworker?
29+
role == "caseworker"
30+
end
31+
32+
def staff?
33+
admin? || caseworker?
34+
end
35+
```
36+
37+
### Policy Configuration
38+
39+
Policies are located in `app/policies/` and inherit from `ApplicationPolicy`. They define authorization logic for specific controllers or resources.
40+
41+
#### Example: `StaffPolicy`
42+
43+
The `StaffPolicy` (`app/policies/staff_policy.rb`) is a general-purpose policy used to restrict access to controllers that should only be accessible by staff members (admins and caseworkers).
44+
45+
```ruby
46+
class StaffPolicy < ApplicationPolicy
47+
def index?
48+
staff?
49+
end
50+
51+
def show?
52+
staff?
53+
end
54+
55+
# ... other actions ...
56+
57+
private
58+
59+
def staff_in_region?
60+
staff? && in_region?
61+
end
62+
63+
delegate :admin?, to: :user
64+
delegate :staff?, to: :user
65+
end
66+
```
67+
68+
### Configuring Access in Controllers
69+
70+
To enforce a policy in a controller, use the `authorize` method provided by Pundit.
71+
72+
```ruby
73+
class Staff::BaseController < ApplicationController
74+
before_action :authenticate_user!
75+
after_action :verify_authorized
76+
77+
def index
78+
authorize :staff, :index?
79+
# ...
80+
end
81+
end
82+
```
83+
84+
In this example, `authorize :staff, :index?` tells Pundit to use `StaffPolicy#index?` to authorize the action.
85+
86+
### Scoping Data Based on Attributes
87+
88+
ABAC is also used to restrict the data a user can see. This is handled by the `Scope` class within a policy.
89+
90+
#### Example: Regional Data Restriction
91+
92+
In `StaffPolicy`, the `Scope` class can be configured to restrict data based on the user's region:
93+
94+
```ruby
95+
class StaffPolicy < ApplicationPolicy
96+
# ...
97+
class Scope < ApplicationPolicy::Scope
98+
def resolve
99+
if user.admin?
100+
scope.all
101+
elsif user.staff?
102+
# Restrict to records in the user's region
103+
scope.where(region: user.region)
104+
else
105+
scope.none
106+
end
107+
end
108+
end
109+
end
110+
```
111+
112+
By using `policy_scope(Model)` in your controller, you ensure that users only see data they are authorized to access based on their attributes.
113+
114+
## Adding New Attributes for ABAC
115+
116+
As the application grows, you may need to add new attributes to the `User` model to support more granular authorization rules.
117+
118+
### 1. Database Migration
119+
120+
If the attribute should be persisted in the database, create a migration:
121+
122+
```bash
123+
make rails-generate GENERATE_COMMAND="migration AddDepartmentToUsers department:string"
124+
make db-migrate
125+
```
126+
127+
### 2. Update the User Model
128+
129+
Add the attribute to `app/models/user.rb` and define any necessary helper methods.
130+
131+
```ruby
132+
class User < ApplicationRecord
133+
# ...
134+
attribute :department, :string
135+
136+
def in_department?(dept_name)
137+
department == dept_name
138+
end
139+
end
140+
```
141+
142+
### 3. Use the New Attribute in a Policy
143+
144+
Now you can use this attribute in your policies to enforce specific rules. For example, you might want to allow only users from the "Finance" department to see certain reports.
145+
146+
```ruby
147+
class FinancialReportPolicy < ApplicationPolicy
148+
def show?
149+
user.admin? || (user.staff? && user.in_department?("Finance"))
150+
end
151+
end
152+
```
153+
154+
### Virtual Attributes
155+
156+
If an attribute is not stored in the database but is derived from other data (e.g., from a JWT token or an external service), you can define it as a virtual attribute in the `User` model.
157+
158+
```ruby
159+
class User < ApplicationRecord
160+
# ...
161+
attr_accessor :temporary_clearance_level
162+
163+
def high_clearance?
164+
temporary_clearance_level == "high"
165+
end
166+
end
167+
```
168+
169+
This can then be used in policies just like a persisted attribute:
170+
171+
```ruby
172+
class SensitiveDataPolicy < ApplicationPolicy
173+
def view_details?
174+
user.admin? || user.high_clearance?
175+
end
176+
end
177+
```
178+
179+
## Best Practices
180+
181+
1. **Keep Policies Simple**: Policies should only contain authorization logic. Complex business logic belongs in models or services.
182+
2. **Use Helper Methods**: Define helper methods in the `User` model for common attribute checks to keep policies readable.
183+
3. **Always Verify Authorization**: Use `after_action :verify_authorized` and `after_action :verify_policy_scoped` in your base controllers to ensure authorization is never skipped.
184+
4. **Leverage Scopes**: Always use `policy_scope` when fetching collections of records to ensure data-level security.

0 commit comments

Comments
 (0)