Skip to content

Authorization Architecture

Vulcan uses a deny-by-default authorization model enforced by an automated test. Every routed controller action must have an explicit authorize_* before_action callback — actions without one cause the test suite to fail.

Layers

  1. Authentication (authenticate_user!) — Devise, applied globally via ApplicationController. Answers: "Who are you?"
  2. Authorization (authorize_* callbacks) — Custom, per-action. Answers: "What can you do?"

Both layers are required. Authentication alone is never sufficient for a controller action.

Role Hierarchy

Site Admin (global)
    └── can do everything on every project and component

Project/Component Roles (scoped):
    admin > reviewer > author > viewer

Each level includes all permissions of lower levels. A project admin can do everything a reviewer, author, and viewer can do. Roles are assigned per-project or per-component via Membership records.

Available roles: viewer, author, reviewer, admin

Site Admin vs Project/Component Admin

  • Site admin (User#admin == true): Full access to everything — all projects, all components, user management, SRG/STIG uploads. Set via database or admin bootstrap.
  • Project admin: Full access to one project — can update/delete the project, manage members, create/delete components. Automatically assigned when a user creates a project.
  • Component admin: Full access to one component — can delete the component, lock/unlock controls, manage component members. Can be assigned by project admin.

Effective Permissions (Components)

Components have dual membership — a user's effective permission on a component is the higher of:

  1. Their project-level membership role (inherited)
  2. Their component-level membership role (direct)

Example: A user with viewer role on a project but admin role on a specific component within that project has admin permissions on that component.

This is computed by User#effective_permissions(component) and passed to Vue as the effective_permissions prop.

User-Facing Permissions Summary

Projects

ActionWho
Browse project listAny logged-in user
Create a projectSite admin, or any user when create_permission_enabled is on
View project detailsProject member (viewer+) or site admin
Export projectProject member (viewer+) or site admin
Update project name/descriptionProject admin or site admin
Delete a projectProject admin or site admin
Manage project membersProject admin or site admin

When a user creates a project, they are automatically assigned the admin role on that project. This means project creators can update, delete, and manage members on their own projects without being a site admin.

Components

ActionWho
View (unreleased)Project member (viewer+) or site admin
View (released)Any logged-in user
CreateProject admin or site admin
Edit rulesComponent author+ (not if released)
Edit advanced fields (status, severity)Component admin or site admin
DeleteComponent admin or site admin
Lock/unlock controlsComponent admin or site admin
Compare componentsViewer on both components (or released)

Released components are read-only — even authors and reviewers cannot edit rules on a released component. Only site admins bypass this restriction.

Rules

ActionWho
View rulesComponent viewer+
Create/update/revertComponent author+
DeleteComponent admin or site admin
Submit reviewComponent reviewer+ or project author+

Memberships

ActionWho
Add members to projectProject admin or site admin
Add members to componentComponent admin or site admin
Update member roleAdmin of the target project/component
Remove a memberAdmin of the target project/component

Access Requests

ActionWho
Request access to a discoverable projectAny logged-in user
Cancel own access requestThe requesting user
Approve/deny access requestsProject admin or site admin

SRGs and STIGs

ActionWho
View and exportAny logged-in user
Upload newSite admin only
DeleteSite admin only

Users

ActionWho
View user listSite admin only
Update user (promote to admin, etc.)Site admin only
Delete userSite admin only
ActionWho
Global searchAny logged-in user (results scoped to user's accessible projects)

Project Visibility

Projects have a visibility setting:

  • Discoverable: Appears in project list for all users. Non-members can see the project name/description and request access.
  • Hidden: Only visible to project members and site admins.

Visibility does NOT grant access to project contents — only membership does.

Authorization Methods Reference

Global

MethodRequirement
authorize_logged_inAny authenticated user
authorize_admincurrent_user.admin == true (site admin)
authorize_admin_or_create_permission_enabledSite admin OR Settings.project.create_permission_enabled

Project-scoped

MethodChecks
authorize_viewer_projectcan_view_project?(@project) — site admin OR membership with any role
authorize_author_projectcan_author_project?(@project) — site admin OR membership with author+
authorize_review_projectcan_review_project?(@project) — site admin OR membership with reviewer+
authorize_admin_projectcan_admin_project?(@project) — site admin OR membership with admin

Component-scoped

MethodChecks
authorize_viewer_componentcan_view_component?(@component) — site admin OR effective_permissions is any role
authorize_author_componentcan_author_component?(@component) — site admin OR effective_permissions author+ (blocked if released)
authorize_review_componentcan_review_component?(@component) — site admin OR effective_permissions reviewer+
authorize_admin_componentcan_admin_component?(@component) — site admin OR effective_permissions admin

Special

MethodChecks
authorize_component_accessViewer if unreleased, logged_in if released
authorize_compare_accessViewer on both components being compared
check_admin_for_advanced_fieldsAdmin required only when updating status/severity fields
authorize_membership_createAdmin on the target project or component
set_and_authorize_access_requestRequest owner or project admin

Controller Authorization Map

ProjectsController

ActionAuthorization
indexauthorize_logged_in
searchauthorize_logged_in
createauthorize_admin_or_create_permission_enabled
showauthorize_viewer_project
exportauthorize_viewer_project
updateauthorize_admin_project
destroyauthorize_admin_project

ComponentsController

ActionAuthorization
indexauthorize_logged_in
searchauthorize_logged_in
based_on_same_srgauthorize_logged_in
showauthorize_component_access (viewer if unreleased, logged_in if released)
exportauthorize_component_access
findauthorize_component_access
compareauthorize_compare_access (checks viewer on both components)
historyauthorize_viewer_project
createauthorize_admin_project
updateauthorize_author_component + check_admin_for_advanced_fields
destroyauthorize_admin_component

RulesController

ActionAuthorization
indexauthorize_viewer_component
showauthorize_viewer_component
related_rulesauthorize_viewer_component
searchauthorize_logged_in
createauthorize_author_component
updateauthorize_author_component
revertauthorize_author_component
destroyauthorize_admin_component

ReviewsController

ActionAuthorization
createauthorize_author_project
lock_controlsauthorize_admin_component

MembershipsController

ActionAuthorization
createauthorize_membership_create (admin on target project/component)
updateauthorize_admin_membership
destroyauthorize_admin_membership

ProjectAccessRequestsController

ActionAuthorization
createauthorize_logged_in
destroyset_and_authorize_access_request (owner or project admin)

SecurityRequirementsGuidesController

ActionAuthorization
indexauthorize_logged_in
showauthorize_logged_in
exportauthorize_logged_in
createauthorize_admin
destroyauthorize_admin

StigsController

ActionAuthorization
indexauthorize_logged_in
showauthorize_logged_in
exportauthorize_logged_in
createauthorize_admin
destroyauthorize_admin

UsersController

ActionAuthorization
indexauthorize_admin
updateauthorize_admin
destroyauthorize_admin

RuleSatisfactionsController

ActionAuthorization
createauthorize_author_component
destroyauthorize_author_component

Api::SearchController

ActionAuthorization
globalauthenticate_user! (data-scoped via current_user.available_projects)

Deny-by-Default Safety Net

spec/requests/authorization_coverage_spec.rb automatically verifies authorization coverage:

  1. Introspects the Rails route table to find all routable controller#action pairs
  2. For each, checks that at least one authorize_* before_action callback covers it
  3. Skips Devise controllers (they handle their own auth)
  4. Maintains a documented allowlist for actions that intentionally use only authenticate_user!

If you add a new controller action without an authorize_* callback, this test fails with a clear message telling you exactly which action is uncovered.

Adding a New Action

  1. Add the action to your controller
  2. Add an appropriate authorize_* before_action for it
  3. Run bundle exec rspec spec/requests/authorization_coverage_spec.rb
  4. If the test fails, it will tell you which action needs authorization

Rails Callback De-duplication Warning

Rails de-duplicates before_action callbacks with the same method name. If you write:

ruby
before_action :authorize_admin_component, only: %i[destroy]
before_action :authorize_admin_component, only: %i[update], if: -> { ... }

Only the LAST declaration survives. The destroy action will be unprotected. Use unique method names for callbacks that need different :only/:except/:if configurations:

ruby
before_action :authorize_admin_component, only: %i[destroy]
before_action :check_admin_for_advanced_fields, only: %i[update]

Error Handling

NotAuthorizedError is rescued globally in ApplicationController:

  • HTML requests: Flash alert + redirect back
  • JSON requests: 401 status with toast message
  • API requests (Api::BaseController): 403 Forbidden with JSON error

Part of the MITRE Security Automation Framework (SAF)