Documents
Authorization and List Deletion Permissions
Authorization and List Deletion Permissions
Type
Document
Status
Published
Created
Oct 31, 2025
Updated
Mar 16, 2026
Updated by
Dosu Bot

CipherSwarm uses the CanCanCan gem to enforce authorization for deleting WordLists, RuleLists, and MaskLists. The authorization rules are defined in app/models/ability.rb and are enforced in the respective controllers using load_and_authorize_resource and before_action :authenticate_user!.

Authorization Model#

The authorization model distinguishes between creators of a list and admin users:

  • Creators can delete (destroy) their own WordLists, RuleLists, and MaskLists. This is enforced by a rule in ability.rb that grants the :destroy action on these resources when the creator attribute matches the current user.
  • Admins can delete any list, regardless of who created it. Admins are granted the :manage action on all resources.

Example from ability.rb:

can %i[read create update destroy view_file view_file_content], [WordList, RuleList, MaskList], creator: user

return unless user.admin?
can :manage, :all

View in source

Controller Enforcement#

Each controller for WordLists, RuleLists, and MaskLists includes:

before_action :authenticate_user!
load_and_authorize_resource

This ensures that only authenticated users can access actions, and that authorization rules from ability.rb are applied to every action, including destroy. When a list is created, the creator is set to current_user, which is used for subsequent authorization checks.

Example from WordListsController:

def create
  @word_list = WordList.new(word_list_params)
  @word_list.creator = current_user
  # ...
end

def destroy
  @word_list.destroy!
end

View in source

The same pattern is used in RuleListsController and MaskListsController.

Downloadable Concern Authorization#

The Downloadable concern provides downloadable resource capabilities for WordLists, RuleLists, and MaskLists. All three actions in this concern enforce authorization checks:

  • download: Requires :read ability on the resource
  • view_file: Requires :view_file ability on the resource
  • view_file_content: Requires :view_file_content ability on the resource

Each method calls authorize! to verify that the current user has the necessary ability before proceeding. If authorization fails, a CanCan::AccessDenied exception is raised and handled by the application controller, returning HTTP 403 Forbidden.

The concern uses a private resource_class helper method that extracts the resource class name from the controller path (e.g., WordListsControllerWordList). This method includes NameError handling to provide clear error messages if the controller name cannot be resolved to a valid model.

Example from Downloadable concern:

def download
  @resource = resource_class.find(params[:id])
  authorize! :read, @resource
  redirect_to rails_blob_url @resource.file
end

def view_file
  @resource = resource_class.find(params[:id])
  authorize! :view_file, @resource
  render "shared/attack_resource/view_file"
end

def view_file_content
  @resource = resource_class.find(params[:id])
  authorize! :view_file_content, @resource
  # ...
end

private

def resource_class
  controller_path.classify.constantize
rescue NameError => e
  raise ArgumentError,
    "Downloadable: cannot resolve model from controller_path '#{controller_path}'. " \
    "Ensure the controller name maps to a valid model. (#{e.message})"
end

View in source

Relevant Tests#

Request specs for WordLists verify the authorization model:

  • A signed-in creator can delete their own WordList.
  • A non-admin user cannot delete WordLists created by others (receives a forbidden response).
  • An admin can delete any WordList.
  • An unauthenticated user is redirected to the sign-in page when attempting to delete a WordList.

Example from spec/requests/word_lists_spec.rb:

context "when non-admin user is signed in" do
  before { sign_in creator_user }

  it "successfully deletes their own word list" do
    expect {
      delete word_list_path(user_owned_word_list)
    }.to change(WordList, :count).by(-1)
  end

  it "fails to delete a word list they did not create" do
    other_user_word_list = create(:word_list, creator: create(:user))
    expect {
      delete word_list_path(other_user_word_list)
    }.not_to change(WordList, :count)
    expect(response).to have_http_status(:forbidden)
  end
end

context "when admin user is signed in" do
  before { sign_in admin }

  it "successfully deletes any word list" do
    word_list_to_delete = create(:word_list, creator: create(:user))
    expect {
      delete word_list_path(word_list_to_delete)
    }.to change(WordList, :count).by(-1)
  end
end

context "when user is not signed in" do
  it "redirects to sign in page" do
    delete word_list_path(user_owned_word_list)
    expect(response).to redirect_to(new_user_session_path)
  end
end

View in source

The authorization and authentication logic for RuleLists and MaskLists follows the same structure as for WordLists, with parallel controller logic and expected test coverage.

HTTP Status Codes for Authorization#

CipherSwarm follows proper HTTP semantics for authorization failures:

  • HTTP 401 Unauthorized: Returned when an unauthenticated user attempts to access a protected resource. These users are redirected to the sign-in page.
  • HTTP 403 Forbidden: Returned when an authenticated user lacks the necessary permissions to access a resource. This applies when CanCanCan's authorize! check fails and a CanCan::AccessDenied exception is raised. JSON responses for 403 errors return {"error": "Forbidden", "status": 403} to accurately reflect the forbidden status.

Turbo Frame Error Handling#

When authorization fails during a Turbo Frame request, the application renders a special partial to prevent perpetual "Loading..." states in the UI. The ApplicationController#not_authorized handler detects Turbo Frame requests and responds accordingly:

def not_authorized(error)
  logger.error "not_authorized #{error}"
  respond_to do |format|
    format.html do
      if turbo_frame_request?
        render partial: "errors/not_authorized_frame",
               locals: { frame_id: request.headers["Turbo-Frame"] },
               status: :forbidden, layout: false
      else
        render template: "errors/not_authorized", status: :forbidden
      end
    end
    format.json { render json: { error: "Forbidden", status: 403 }, status: :forbidden }
    format.all { head :forbidden }
  end
end

The rendered partial (errors/_not_authorized_frame.html.erb) wraps the error message in a turbo-frame element matching the requesting frame's ID, ensuring the error is displayed within the frame context:

<turbo-frame id="<%= frame_id %>">
  <div class="alert alert-danger"><%= t("errors.not_authorized") %></div>
</turbo-frame>

This prevents UI issues where authorization failures would otherwise leave Turbo Frames in an indefinite loading state.