Skip to content

Instantly share code, notes, and snippets.

@absyah
Created September 4, 2025 05:59
Show Gist options
  • Save absyah/45e8eef4d31c3b2c3e91039e83d5883a to your computer and use it in GitHub Desktop.
Save absyah/45e8eef4d31c3b2c3e91039e83d5883a to your computer and use it in GitHub Desktop.
## Project [Your App Name]: RBAC Module Design Document
* **Author:** [Your Name]
* **Date:** September 4, 2025
* **Status:** Proposal
* **Version:** 1.0
### 1\. Abstract (TL;DR)
This document proposes the creation of a centralized, configuration-driven Role-Based Access Control (RBAC) module for our Sinatra application. The new module will replace the current system of scattered `current_account_can_...` helper methods.
The proposed solution moves role and permission definitions into a human-readable `roles.yml` file, providing a single source of truth. The core logic will be encapsulated in a single `RBAC` Ruby module, which supports role inheritance and instance-level permissions (e.g., a user can only edit their *own* posts). This will significantly improve maintainability, reduce code duplication, and make our authorization logic easier to understand and manage.
### 2\. Background and Problem Statement
Currently, authorization logic is implemented via a series of helper methods in `application_helpers`, such as `current_account_can_view_xxx?` and `current_account_can_edit_yyy?`. This approach has led to several challenges as the application has grown:
* **Scattered Logic:** Permissions are defined in Ruby code and spread across many different helper methods, making it difficult to get a holistic view of what a specific role can do.
* **Code Duplication:** Similar logic is often repeated with minor variations across different helpers.
* **Difficult to Maintain:** Changing a role's permissions can require finding and modifying multiple methods. Onboarding new developers is also more difficult as they must learn the location and naming convention of all authorization helpers.
* **Lack of Flexibility:** The current system does not easily support complex rules, such as object-level permissions (e.g., "an `editor` can delete their own `post`, but not others'").
### 3\. Goals and Scope
#### 3.1. Goals
* **Centralize** all role and permission definitions.
* **Decouple** permission configuration (the "what") from authorization logic (the "how").
* Provide a clean, readable API for checking permissions in both routes and views.
* Support **role inheritance** to keep definitions DRY (Don't Repeat Yourself).
* Support **instance-level permissions** based on object attributes.
* Improve the overall maintainability and scalability of our access control system.
#### 3.2. Non-Goals (Out of Scope for this Version)
* A user interface for managing roles and permissions.
* Integration with external authentication/directory systems (e.g., LDAP, OAuth scopes).
* Real-time permission updates without an application restart.
### 4\. Proposed Solution: High-Level Design
The proposed system consists of three core components:
1. **Configuration (`config/roles.yml`):** A YAML file that defines all roles, their permissions, and any inheritance rules. This file serves as the single, human-readable source of truth for our permission structure.
2. **Core Logic (`lib/rbac.rb`):** A self-contained Ruby module responsible for loading the YAML configuration, resolving permission inheritance, and providing a single method (`RBAC.can?`) to perform authorization checks.
3. **Sinatra Integration (Application Helpers):** Two simple helper methods that bridge the `RBAC` module with our application:
* `authorize!`: For use in routes to halt execution if permission is denied.
* `can?`: For use in views to conditionally render UI elements, returning `true` or `false`.
#### System Flow Diagram
This diagram illustrates the flow of an authorization check: a request comes into a route, which calls the `authorize!` helper. The helper uses the `RBAC.can?` method, which first consults the pre-loaded roles from the YAML file. If necessary, it then executes specific conditional logic to make a final "Allow" or "Deny" decision.
```mermaid
graph TD
A[Request to Sinatra Route] --> B{Route calls authorize!(:edit, @post)};
B --> C[RBAC.can?(user, :edit, @post)];
C --> D{Is "edit:post" in user's roles from roles.yml?};
D -- Yes --> F[Return true];
D -- No --> E{Is there a conditional rule for "edit:post"?};
E -- Yes --> G{Execute conditional block: user.id == @post.author_id};
G -- true --> F;
G -- false --> H[Return false];
E -- No --> H;
F --> I[Allow Request];
H --> J[Deny Request / Halt 403];
```
### 5\. Detailed Design
#### 5.1. The Configuration File (`config/roles.yml`)
Roles and permissions will be defined in a simple key-value format.
```yaml
# config/roles.yml
admin:
permissions:
- "*:*" # Wildcard grants all permissions
editor:
inherits: viewer # Inherits all permissions from 'viewer'
permissions:
- "create:post"
- "edit:post"
# Note: "delete:post" is intentionally omitted for editors.
viewer:
permissions:
- "read:post"
```
#### 5.2. The Core Module (`lib/rbac.rb`)
The module will expose three primary public methods.
* `RBAC.load_roles(file_path)`: Called once at application startup to load the YAML file, process inheritance, and cache the resulting permissions hash.
* `RBAC.can?(user, action, resource_or_type, roles)`: The main authorization method. It checks for both static role permissions and dynamic, conditional permissions.
* `RBAC.define_conditional_permission(action, resource, &block)`: A DSL-style method used during application startup to define complex, instance-level rules in pure Ruby.
**Example: Defining a conditional rule in `app.rb`**
```ruby
# configure block in app.rb
configure do
# ...
RBAC.define_conditional_permission(:delete, :post) do |user, post|
user.id == post.author_id
end
end
```
#### 5.3. Sinatra Helpers
The helpers provide a clean and expressive API for the rest of the application.
* **`authorize!(action, resource)`**: Used in routes. Fetches the resource first, then checks permission.
```ruby
# In a route
delete '/posts/:id' do
@post = Post.find(params[:id])
authorize! :delete, @post # Halts with 403 if permission is denied
# ... proceed with deletion
end
```
* **`can?(action, resource)`**: Used in views. Returns a boolean.
```html
<!-- In a view (ERB) -->
<% if can?(:delete, @post) %>
<button>Delete Post</button>
<% end %>
```
### 6\. Migration Plan
We will adopt a phased approach to migrate from the old system to the new one, ensuring no disruption.
1. **Phase 1 (Introduction):** Add the `rbac.rb` module, `roles.yml` file, and the new `authorize!` and `can?` helpers to the application. The old helpers will remain functional.
2. **Phase 2 (Gradual Refactoring):** Go through the application controller by controller, and view by view, replacing calls to old helpers (`current_account_can_...`) with calls to the new helpers. This can be done incrementally through multiple pull requests.
3. **Phase 3 (Deprecation):** Once all calls to the old helpers have been replaced, perform a codebase search to ensure no usages remain, and then delete the old helper methods.
### 7\. Alternatives Considered
1. **Keep the Existing System:** This was rejected due to the maintainability and scalability issues outlined in the problem statement.
2. **Use an Existing Gem (e.g., Pundit, CanCanCan):** While powerful, these gems often introduce a higher level of complexity and dependencies than required for our current needs. The proposed lightweight, custom solution is tailored specifically to our application's structure and avoids "dependency bloat." It provides the exact functionality we need without any overhead.
### 8\. Open Questions
* Should we implement caching for the loaded roles configuration for performance, or is the on-startup load sufficient? (Initial assessment: startup load is fine for now).
-----
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment