|
| 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