Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# AGENTS.md

This file provides guidance to WARP (warp.dev) when working with code in this repository.

## Project Overview
ae_declarative_authorization is a Rails authorization gem that provides role-based access control (RBAC) with a declarative approach. Authorization rules are defined in `config/authorization_rules.rb` rather than scattered throughout controllers, views, and models.

## Core Architecture

### Main Components
- **Authorization Engine** (`lib/declarative_authorization/authorization.rb`): Reference monitor that evaluates permissions and enforces authorization rules. The `Engine` class is the central orchestrator.
- **DSL Reader** (`lib/declarative_authorization/reader.rb`): Parses authorization rules from the DSL configuration file. Key classes:
- `DSLReader`: Top-level parser for authorization configuration
- `AuthorizationRulesReader`: Parses role definitions and permission rules
- `PrivilegesReader`: Handles privilege hierarchies
- **Controller Integration** (`lib/declarative_authorization/controller/`):
- `dsl.rb`: Provides `filter_access_to` and `filter_resource_access` methods for controllers
- `rails.rb`: Rails-specific controller integration
- `grape.rb`: Grape API framework integration
- **Model Integration** (`lib/declarative_authorization/in_model.rb`): Enables model security with `using_access_control` and query rewriting via `with_permissions_to`
- **ObligationScope** (`lib/declarative_authorization/obligation_scope.rb`): Translates authorization obligations into SQL joins and conditions for efficient query rewriting
- **View Helpers** (`lib/declarative_authorization/helper.rb`): Provides `permitted_to?` and role checking helpers for views

### Authorization Flow
1. Request hits controller → `filter_access_to` or `filter_resource_access` intercepts
2. Engine evaluates user's roles against authorization rules from DSL
3. For attribute-based rules, loads object and checks conditions
4. Raises `Authorization::NotAuthorized` or `Authorization::AttributeAuthorizationError` on denial
5. For model queries, `ObligationScope` rewrites queries to only return authorized records

### Key Patterns
- **Thread-safe user storage**: `Authorization.current_user` uses `Thread.current` for model-level security
- **Privilege hierarchies**: E.g., `:manage` includes `:create`, `:read`, `:update`, `:delete`
- **Role hierarchies**: Roles can include other roles via `includes`
- **Attribute conditions**: Rules can check object attributes (e.g., `if_attribute :branch => is {user.branch}`)
- **Permission dependencies**: Rules can depend on permissions of associated objects via `if_permitted_to`

## Development Commands

### Running Tests
```bash
# Run all tests
rake test

# Run specific test file
ruby test/authorization_test.rb

# Run single test (using Minitest)
ruby test/authorization_test.rb -n test_name_here
```

### Testing Multiple Rails/Grape Versions
This gem uses Appraisal to test against multiple dependency combinations:
```bash
# Install all appraisal gemfiles
bundle exec appraisal install

# Run tests for all combinations
bundle exec appraisal rake test

# Run tests for specific combination
bundle exec appraisal ruby-2.7.5-rails_6.1-grape_1.6 rake test
```
Supported combinations are defined in `Appraisals` (Ruby 2.6.9, 2.7.5, 3.1.0 with various Rails and Grape versions).

### Building and Releasing
```bash
# Build gem
gem build declarative_authorization.gemspec

# Install locally for testing
gem install ae_declarative_authorization-*.gem

# Release (requires appropriate permissions)
bundle exec rake release
```

### Generating Documentation
```bash
rake rdoc
```
Documentation is generated to `rdoc/` directory.

## Testing Infrastructure
- Uses **Minitest** (not RSpec)
- Test helpers in `lib/declarative_authorization/test/helpers.rb` provide `with_user`, `without_access_control`, and HTTP verb helpers like `get_with`, `post_with`
- Mock objects in `test/test_helper.rb` (`MockUser`, `MockDataObject`)
- `test/test_support/` contains Rails and Grape test infrastructure

## Installation Generators
The gem provides Rails generators:
- `rails g authorization:install [UserModel]`: Sets up Role model, associations, and authorization rules
- `rails g authorization:rules`: Copies default authorization rules to `config/authorization_rules.rb`

## Important Configuration
- **Authorization rules file**: `config/authorization_rules.rb` (configurable via `Authorization::AUTH_DSL_FILES`)
- **Default role**: `:guest` (configurable via `Authorization.default_role=`)
- **Current user**: Controllers must implement `current_user` method
- **User role method**: User model must implement `role_symbols` returning array of role symbols

## Special Considerations
- This gem supports **Rails 4.2.5.2 through 7.0** and **Ruby >= 2.6.3**
- Model security is opt-in via `using_access_control` on individual models
- Read checks on models require explicit `:include_read => true` option due to performance implications
- `strong_parameters` integration is enabled by default for `filter_resource_access`
- The gem supports both Rails controllers and Grape APIs

## Common Patterns in Tests
- Use `Authorization::Engine.new(reader)` with custom DSL for isolated testing
- Access control can be globally disabled in tests via `Authorization.ignore_access_control`
- Create mock users with role symbols: `MockUser.new(:admin, :user => { :id => 1 })`
9 changes: 9 additions & 0 deletions Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ when '3.2.5', '3.3.6', '3.4.1'
end
end
end
when '3.1.7'
['7.0', '7.1', '7.2'].product(['1.6']).each do |rails_version, grape_version|
appraise "ruby-#{RUBY_VERSION}-rails_#{rails_version}-grape_#{grape_version}" do
source 'https://rubygems.org' do
gem 'rails', "~> #{rails_version}.0"
gem 'grape', "~> #{grape_version}.0"
end
end
end
else
raise "Unsupported Ruby version #{RUBY_VERSION}"
end
13 changes: 13 additions & 0 deletions gemfiles/ruby_2.6.6_rails5.2.6_grape1.3.0.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This file was generated by Appraisal

source "http://rubygems.org"

gem "appraisal", "~> 2.1"
gem "mocha", "~> 1.0", require: false
gem "sprockets", "< 4"
gem "rails-controller-testing"
gem "rails", "5.2.6"
gem "grape", "1.3.0"
gem "sqlite3", "~> 1.3.0"

gemspec path: "../"
13 changes: 13 additions & 0 deletions gemfiles/ruby_2.7.2_rails6.0.2.1_grape1.3.0.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This file was generated by Appraisal

source "http://rubygems.org"

gem "appraisal", "~> 2.1"
gem "mocha", "~> 1.0", require: false
gem "sprockets", "< 4"
gem "rails-controller-testing"
gem "rails", "6.0.2.1"
gem "grape", "1.3.0"
gem "sqlite3", "~> 1.4"

gemspec path: "../"
18 changes: 18 additions & 0 deletions gemfiles/ruby_3.1.7_rails_7.0_grape_1.6.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# This file was generated by Appraisal

source "https://rubygems.org"

source "https://rubygems.org" do
gem "appraisal", ">= 2.3", "< 3"
gem "bundler", ">= 2.2", "< 3"
gem "minitest", ">= 5.15", "< 6"
gem "mocha", ">= 1.13"
gem "rake", ">= 13", "< 14"
gem "simplecov", ">= 0.21", "< 1", group: :test, require: false
gem "sprockets", ">= 3.4", "< 4"
gem "sqlite3", ">= 1.4", "< 2"
gem "rails", "~> 7.0.0"
gem "grape", "~> 1.6.0"
end

gemspec path: "../"
18 changes: 18 additions & 0 deletions gemfiles/ruby_3.1.7_rails_7.1_grape_1.6.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# This file was generated by Appraisal

source "https://rubygems.org"

source "https://rubygems.org" do
gem "appraisal", ">= 2.3", "< 3"
gem "bundler", ">= 2.2", "< 3"
gem "minitest", ">= 5.15", "< 6"
gem "mocha", ">= 1.13"
gem "rake", ">= 13", "< 14"
gem "simplecov", ">= 0.21", "< 1", group: :test, require: false
gem "sprockets", ">= 3.4", "< 4"
gem "sqlite3", ">= 1.4", "< 2"
gem "rails", "~> 7.1.0"
gem "grape", "~> 1.6.0"
end

gemspec path: "../"
18 changes: 18 additions & 0 deletions gemfiles/ruby_3.1.7_rails_7.2_grape_1.6.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# This file was generated by Appraisal

source "https://rubygems.org"

source "https://rubygems.org" do
gem "appraisal", ">= 2.3", "< 3"
gem "bundler", ">= 2.2", "< 3"
gem "minitest", ">= 5.15", "< 6"
gem "mocha", ">= 1.13"
gem "rake", ">= 13", "< 14"
gem "simplecov", ">= 0.21", "< 1", group: :test, require: false
gem "sprockets", ">= 3.4", "< 4"
gem "sqlite3", ">= 1.4", "< 2"
gem "rails", "~> 7.2.0"
gem "grape", "~> 1.6.0"
end

gemspec path: "../"
2 changes: 1 addition & 1 deletion lib/declarative_authorization/controller/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def filter_resource_access(options = {})
collections = actions_from_option(options[:collection]).merge(
actions_from_option(options[:additional_collection]))

no_attribute_check_actions = options[:strong_parameters] ? actions_from_option(options[:collection]).merge(actions_from_option([:create])) : collections
no_attribute_check_actions = options[:strong_parameters] ? collections.merge(actions_from_option([:create])) : collections

options[:no_attribute_check] ||= no_attribute_check_actions.keys unless options[:nested_in]

Expand Down
84 changes: 84 additions & 0 deletions test/controller_filter_resource_access_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,90 @@ def test_additional_members_filter_other_new
end
end

class AdditionalMembersCollectionsStrongParamsController < MocksController
def self.controller_name
"basic_resources"
end
filter_resource_access :additional_member => :other_show,
:additional_collection => [:search], :additional_new => {:other_new => :new}, :strong_parameters => true
define_resource_actions
define_action_methods :other_new, :search, :other_show
end
class AdditionalMembersCollectionsStrongParamsControllerTest < ActionController::TestCase
def test_additional_members_filter_search_index
reader = Authorization::Reader::DSLReader.new
reader.parse %{
authorization do
role :allowed_role do
has_permission_on :basic_resources, :to => [:search, :index] do
if_attribute :id => is {"1"}
end
end
end
}

request!(MockUser.new(:another_role), :search, reader)
assert !@controller.authorized?
request!(MockUser.new(:another_role), :index, reader)
assert !@controller.authorized?
request!(MockUser.new(:allowed_role), :search, reader)
assert @controller.authorized?
request!(MockUser.new(:allowed_role), :index, reader)
assert @controller.authorized?
end

def test_additional_members_filter_other_show
reader = Authorization::Reader::DSLReader.new
reader.parse %{
authorization do
role :allowed_role do
has_permission_on :basic_resources, :to => [:show, :other_show] do
if_attribute :id => is {"1"}
end
end
end
}

allowed_user = MockUser.new(:allowed_role)
request!(allowed_user, :other_show, reader, :id => "2")
assert !@controller.authorized?
request!(allowed_user, :show, reader, :id => "2", :clear => [:@basic_resource])
assert !@controller.authorized?
request!(allowed_user, :other_show, reader, :id => "1", :clear => [:@basic_resource])
assert @controller.authorized?
request!(allowed_user, :show, reader, :id => "1", :clear => [:@basic_resource])
assert @controller.authorized?
end

def test_additional_members_filter_other_new
reader = Authorization::Reader::DSLReader.new
reader.parse %{
authorization do
role :allowed_role do
has_permission_on :basic_resources, :to => :new do
if_attribute :id => is {"1"}
end
end
end
}

allowed_user = MockUser.new(:allowed_role)
request!(allowed_user, :other_new, reader, :basic_resource => {:id => "2"})
assert !@controller.authorized?
request!(allowed_user, :new, reader, :basic_resource => {:id => "2"},
:clear => [:@basic_resource])
assert !@controller.authorized?

# strong_parameters (as mocked) never set parameters on new object, so attribute condition is never met
request!(allowed_user, :other_new, reader, :basic_resource => {:id => "1"},
:clear => [:@basic_resource])
assert !@controller.authorized?
request!(allowed_user, :new, reader, :basic_resource => {:id => "1"},
clear: [:@basic_resource])
assert !@controller.authorized?
end
end


class CustomMethodsResourceController < MocksController
# not implemented yet
Expand Down