saas: Add bearer token authentication for audit console

- Rename SaasAdminController to Admin::AuditController
- Override require_authentication to support bearer tokens alongside
  session auth, allowing API access to audit console
- Add migration for audits1984_auditor_tokens table
- Add controller tests for authentication scenarios including staff
  validation on token-based access

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mike Dalessio
2026-01-27 12:37:47 -05:00
parent 4e61b64d9a
commit 89d2f2d0fc
6 changed files with 77 additions and 8 deletions
@@ -0,0 +1,17 @@
class Admin::AuditsController < ::AdminController
private
# Extend Fizzy's authentication to support auditor bearer tokens.
def require_authentication
authenticate_by_audit_bearer_token || super
end
def authenticate_by_audit_bearer_token
if auditor = auditor_from_bearer_token
Current.identity = auditor
end
end
def find_current_auditor
Current.identity
end
end
@@ -1,6 +0,0 @@
class SaasAdminController < ::AdminController
private
def find_current_auditor
Current.identity
end
end
@@ -0,0 +1,14 @@
# This migration comes from audits1984 (originally 20260126000000)
class CreateAuditorTokens < ActiveRecord::Migration[7.0]
def change
create_table :audits1984_auditor_tokens do |t|
t.uuid :auditor_id, null: false, index: { unique: true }
t.string :token_digest, null: false
t.datetime :expires_at, null: false
t.timestamps
t.index :token_digest, unique: true
end
end
end
+11 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.2].define(version: 2025_12_16_000000) do
ActiveRecord::Schema[8.2].define(version: 2026_01_26_230838) do
create_table "account_billing_waivers", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "account_id", null: false
t.datetime "created_at", null: false
@@ -43,6 +43,16 @@ ActiveRecord::Schema[8.2].define(version: 2025_12_16_000000) do
t.index ["stripe_subscription_id"], name: "index_account_subscriptions_on_stripe_subscription_id", unique: true
end
create_table "audits1984_auditor_tokens", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "auditor_id", null: false
t.datetime "created_at", null: false
t.datetime "expires_at", null: false
t.string "token_digest", null: false
t.datetime "updated_at", null: false
t.index ["auditor_id"], name: "index_audits1984_auditor_tokens_on_auditor_id", unique: true
t.index ["token_digest"], name: "index_audits1984_auditor_tokens_on_token_digest", unique: true
end
create_table "audits1984_audits", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "auditor_id", null: false
t.datetime "created_at", null: false
+1 -1
View File
@@ -126,7 +126,7 @@ module Fizzy
config.console1984.base_record_class = "::SaasRecord"
config.console1984.incinerate_after = 60.days
config.audits1984.base_controller_class = "::SaasAdminController"
config.audits1984.base_controller_class = "::Admin::AuditsController"
config.audits1984.auditor_class = "::Identity"
config.audits1984.auditor_name_attribute = :email_address
@@ -38,4 +38,38 @@ class Admin::AuditsControllerTest < ActionDispatch::IntegrationTest
assert_response :unauthorized
end
test "valid bearer token is allowed" do
token = Audits1984::AuditorToken.generate_for(identities(:david))
untenanted do
get saas.admin_audits1984_path, headers: { "Authorization" => "Bearer #{token}" }
end
assert_response :success
end
test "expired bearer token is forbidden" do
token = Audits1984::AuditorToken.generate_for(identities(:david))
Audits1984::AuditorToken.update_all(expires_at: 1.day.ago)
untenanted do
get saas.admin_audits1984_path, headers: { "Authorization" => "Bearer #{token}" }
end
assert_response :unauthorized
end
test "bearer token for non-staff user is forbidden" do
# Even with a valid token, non-staff users should be denied access.
# This handles the case where a user's staff privileges are revoked
# after a token was issued.
token = Audits1984::AuditorToken.generate_for(identities(:jz))
untenanted do
get saas.admin_audits1984_path, headers: { "Authorization" => "Bearer #{token}" }
end
assert_response :forbidden
end
end