Switch user status to enum
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled

This commit is contained in:
Dan Milne
2025-10-23 20:24:19 +11:00
parent 91573ee2b9
commit 7f075391c1
6 changed files with 146 additions and 6 deletions

View File

@@ -0,0 +1,130 @@
module Api
class ForwardAuthController < ApplicationController
# ForwardAuth endpoints don't use sessions or CSRF
allow_unauthenticated_access
skip_before_action :verify_authenticity_token
# GET /api/verify
# This endpoint is called by reverse proxies (Traefik, Caddy, nginx)
# to verify if a user is authenticated and authorized to access an application
def verify
# Get the application slug from query params or X-Forwarded-Host header
app_slug = params[:app] || extract_app_from_headers
# Get the session from cookie
session_id = extract_session_id
unless session_id
# No session cookie - user is not authenticated
return render_unauthorized("No session cookie")
end
# Find the session
session = Session.find_by(id: session_id)
unless session
# Invalid session
return render_unauthorized("Invalid session")
end
# Check if session is expired
if session.expired?
session.destroy
return render_unauthorized("Session expired")
end
# Update last activity
session.update_column(:last_activity_at, Time.current)
# Get the user
user = session.user
unless user.active?
return render_unauthorized("User account is not active")
end
# If an application is specified, check authorization
if app_slug.present?
application = Application.find_by(slug: app_slug, app_type: "trusted_header", active: true)
unless application
Rails.logger.warn "ForwardAuth: Application not found or not configured for trusted_header: #{app_slug}"
return render_forbidden("Application not found or not configured")
end
# Check if user is allowed to access this application
unless application.user_allowed?(user)
Rails.logger.info "ForwardAuth: User #{user.email_address} denied access to #{app_slug}"
return render_forbidden("You do not have permission to access this application")
end
Rails.logger.info "ForwardAuth: User #{user.email_address} granted access to #{app_slug}"
else
Rails.logger.info "ForwardAuth: User #{user.email_address} authenticated (no app specified)"
end
# User is authenticated and authorized
# Return 200 with user information headers
response.headers["Remote-User"] = user.email_address
response.headers["Remote-Email"] = user.email_address
response.headers["Remote-Name"] = user.email_address
# Add groups if user has any
if user.groups.any?
response.headers["Remote-Groups"] = user.groups.pluck(:name).join(",")
end
# Add admin flag
response.headers["Remote-Admin"] = user.admin? ? "true" : "false"
# Return 200 OK with no body
head :ok
end
private
def extract_session_id
# Extract session ID from cookie
# Rails uses signed cookies by default
cookies.signed[:session_id]
end
def extract_app_from_headers
# Try to extract application slug from forwarded headers
# This is useful when the proxy doesn't pass ?app= param
# X-Forwarded-Host might contain the hostname
host = request.headers["X-Forwarded-Host"] || request.headers["Host"]
# Try to match hostname to application
# Format: app-slug.domain.com -> app-slug
if host.present?
# Extract subdomain as potential app slug
parts = host.split(".")
if parts.length >= 2
return parts.first if parts.first != "www"
end
end
nil
end
def render_unauthorized(reason = nil)
Rails.logger.info "ForwardAuth: Unauthorized - #{reason}"
# Set header to help with debugging
response.headers["X-Auth-Reason"] = reason if reason
# Return 401 Unauthorized
# The reverse proxy should redirect to login
head :unauthorized
end
def render_forbidden(reason = nil)
Rails.logger.info "ForwardAuth: Forbidden - #{reason}"
# Set header to help with debugging
response.headers["X-Auth-Reason"] = reason if reason
# Return 403 Forbidden
head :forbidden
end
end
end

View File

@@ -17,7 +17,7 @@ class SessionsController < ApplicationController
end
# Check if user is active
unless user.status == "active"
unless user.active?
redirect_to signin_path, alert: "Your account is not active. Please contact an administrator."
return
end

View File

@@ -14,11 +14,11 @@ class User < ApplicationRecord
validates :email_address, presence: true, uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, length: { minimum: 8 }, allow_nil: true
validates :status, presence: true,
inclusion: { in: %w[active disabled pending_invitation] }
# Enum - automatically creates scopes (User.active, User.disabled, etc.)
enum :status, { active: 0, disabled: 1, pending_invitation: 2 }
# Scopes
scope :active, -> { where(status: "active") }
scope :admins, -> { where(admin: true) }
# TOTP methods

View File

@@ -26,6 +26,11 @@ Rails.application.routes.draw do
post "/oauth/token", to: "oidc#token"
get "/oauth/userinfo", to: "oidc#userinfo"
# ForwardAuth / Trusted Header SSO
namespace :api do
get "/verify", to: "forward_auth#verify"
end
# Authenticated routes
root "dashboard#index"
resource :profile, only: [:show, :update]

View File

@@ -0,0 +1,5 @@
class ChangeUserStatusToInteger < ActiveRecord::Migration[8.1]
def change
change_column :users, :status, :integer
end
end

4
db/schema.rb generated
View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2025_10_23_054039) do
ActiveRecord::Schema[8.1].define(version: 2025_10_23_091355) do
create_table "application_groups", force: :cascade do |t|
t.integer "application_id", null: false
t.datetime "created_at", null: false
@@ -108,7 +108,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_10_23_054039) do
t.datetime "created_at", null: false
t.string "email_address", null: false
t.string "password_digest", null: false
t.string "status", default: "active", null: false
t.integer "status"
t.boolean "totp_required", default: false, null: false
t.string "totp_secret"
t.datetime "updated_at", null: false