Massive refactor. Merge forward_auth into App, remove references to unimplemented OIDC federation and SAML features. Add group and user custom claims. Groups now allocate which apps a user can use
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-11-04 13:21:55 +11:00
parent 4d1bc1ab66
commit ef15db77f9
46 changed files with 341 additions and 2917 deletions

View File

@@ -1,6 +1,6 @@
module Admin
class ApplicationsController < BaseController
before_action :set_application, only: [:show, :edit, :update, :destroy, :regenerate_credentials, :roles, :create_role, :update_role, :assign_role, :remove_role]
before_action :set_application, only: [:show, :edit, :update, :destroy, :regenerate_credentials]
def index
@applications = Application.order(created_at: :desc)
@@ -90,53 +90,6 @@ module Admin
end
end
def roles
@application_roles = @application.application_roles.includes(:user_role_assignments)
@available_users = User.active.order(:email_address)
end
def create_role
@role = @application.application_roles.build(role_params)
if @role.save
redirect_to roles_admin_application_path(@application), notice: "Role created successfully."
else
@application_roles = @application.application_roles.includes(:user_role_assignments)
@available_users = User.active.order(:email_address)
render :roles, status: :unprocessable_entity
end
end
def update_role
@role = @application.application_roles.find(params[:role_id])
if @role.update(role_params)
redirect_to roles_admin_application_path(@application), notice: "Role updated successfully."
else
@application_roles = @application.application_roles.includes(:user_role_assignments)
@available_users = User.active.order(:email_address)
render :roles, status: :unprocessable_entity
end
end
def assign_role
user = User.find(params[:user_id])
role = @application.application_roles.find(params[:role_id])
@application.assign_role_to_user!(user, role.name, source: 'manual')
redirect_to roles_admin_application_path(@application), notice: "Role assigned successfully."
end
def remove_role
user = User.find(params[:user_id])
role = @application.application_roles.find(params[:role_id])
@application.remove_role_from_user!(user, role.name)
redirect_to roles_admin_application_path(@application), notice: "Role removed successfully."
end
private
def set_application
@@ -146,12 +99,8 @@ module Admin
def application_params
params.require(:application).permit(
:name, :slug, :app_type, :active, :redirect_uris, :description, :metadata,
:role_mapping_mode, :role_prefix, :role_claim_name, managed_permissions: {}
:domain_pattern, headers_config: {}
)
end
def role_params
params.require(:application_role).permit(:name, :display_name, :description, :active, permissions: {})
end
end
end

View File

@@ -1,84 +0,0 @@
module Admin
class ForwardAuthRulesController < BaseController
before_action :set_forward_auth_rule, only: [:show, :edit, :update, :destroy]
def index
@forward_auth_rules = ForwardAuthRule.ordered
end
def show
@allowed_groups = @forward_auth_rule.allowed_groups
end
def new
@forward_auth_rule = ForwardAuthRule.new
@available_groups = Group.order(:name)
end
def create
@forward_auth_rule = ForwardAuthRule.new(forward_auth_rule_params)
# Handle headers configuration
@forward_auth_rule.headers_config = process_headers_config(params[:headers_config])
if @forward_auth_rule.save
# Handle group assignments
if params[:forward_auth_rule][:group_ids].present?
group_ids = params[:forward_auth_rule][:group_ids].reject(&:blank?)
@forward_auth_rule.allowed_groups = Group.where(id: group_ids)
end
redirect_to admin_forward_auth_rule_path(@forward_auth_rule), notice: "Forward auth rule created successfully."
else
@available_groups = Group.order(:name)
render :new, status: :unprocessable_entity
end
end
def edit
@available_groups = Group.order(:name)
end
def update
if @forward_auth_rule.update(forward_auth_rule_params)
# Handle headers configuration
@forward_auth_rule.headers_config = process_headers_config(params[:headers_config])
@forward_auth_rule.save!
# Handle group assignments
if params[:forward_auth_rule][:group_ids].present?
group_ids = params[:forward_auth_rule][:group_ids].reject(&:blank?)
@forward_auth_rule.allowed_groups = Group.where(id: group_ids)
else
@forward_auth_rule.allowed_groups = []
end
redirect_to admin_forward_auth_rule_path(@forward_auth_rule), notice: "Forward auth rule updated successfully."
else
@available_groups = Group.order(:name)
render :edit, status: :unprocessable_entity
end
end
def destroy
@forward_auth_rule.destroy
redirect_to admin_forward_auth_rules_path, notice: "Forward auth rule deleted successfully."
end
private
def set_forward_auth_rule
@forward_auth_rule = ForwardAuthRule.find(params[:id])
end
def forward_auth_rule_params
params.require(:forward_auth_rule).permit(:domain_pattern, :active)
end
def process_headers_config(headers_params)
return {} unless headers_params.is_a?(Hash)
# Clean up headers config - remove empty values, keep only filled ones
headers_params.select { |key, value| value.present? }.symbolize_keys
end
end
end

View File

@@ -67,7 +67,7 @@ module Admin
end
def group_params
params.require(:group).permit(:name, :description)
params.require(:group).permit(:name, :description, custom_claims: {})
end
end
end

View File

@@ -76,7 +76,7 @@ module Admin
end
def user_params
params.require(:user).permit(:email_address, :password, :admin, :status)
params.require(:user).permit(:email_address, :password, :admin, :status, custom_claims: {})
end
end
end

View File

@@ -0,0 +1,31 @@
module Api
class CspController < ApplicationController
# CSP violation reports don't need authentication
skip_before_action :verify_authenticity_token
allow_unauthenticated_access
# POST /api/csp-violation-report
def violation_report
# Parse CSP violation report
report_data = JSON.parse(request.body.read)
# Log the violation for security monitoring
Rails.logger.warn "CSP Violation Report:"
Rails.logger.warn " Blocked URI: #{report_data.dig('csp-report', 'blocked-uri')}"
Rails.logger.warn " Document URI: #{report_data.dig('csp-report', 'document-uri')}"
Rails.logger.warn " Referrer: #{report_data.dig('csp-report', 'referrer')}"
Rails.logger.warn " Violated Directive: #{report_data.dig('csp-report', 'violated-directive')}"
Rails.logger.warn " Original Policy: #{report_data.dig('csp-report', 'original-policy')}"
Rails.logger.warn " User Agent: #{request.user_agent}"
Rails.logger.warn " IP Address: #{request.remote_ip}"
# In production, you might want to send this to a security monitoring service
# For now, we'll just log it and return a success response
head :no_content
rescue JSON::ParserError => e
Rails.logger.error "Invalid CSP violation report: #{e.message}"
head :bad_request
end
end
end

View File

@@ -9,7 +9,7 @@ module Api
# This endpoint is called by reverse proxies (Traefik, Caddy, nginx)
# to verify if a user is authenticated and authorized to access a domain
def verify
# Note: app_slug parameter is no longer used - we match domains directly with ForwardAuthRule
# Note: app_slug parameter is no longer used - we match domains directly with Application (forward_auth type)
# Check for one-time forward auth token first (to handle race condition)
session_id = check_forward_auth_token
@@ -44,37 +44,37 @@ module Api
return render_unauthorized("User account is not active")
end
# Check for forward auth rule authorization
# Check for forward auth application authorization
# Get the forwarded host for domain matching
forwarded_host = request.headers["X-Forwarded-Host"] || request.headers["Host"]
if forwarded_host.present?
# Load active rules with their associations for better performance
# Load active forward auth applications with their associations for better performance
# Preload groups to avoid N+1 queries in user_allowed? checks
rules = ForwardAuthRule.includes(:allowed_groups).active
apps = Application.forward_auth.includes(:allowed_groups).active
# Find matching forward auth rule for this domain
rule = rules.find { |r| r.matches_domain?(forwarded_host) }
# Find matching forward auth application for this domain
app = apps.find { |a| a.matches_domain?(forwarded_host) }
if rule
# Check if user is allowed by this rule
unless rule.user_allowed?(user)
Rails.logger.info "ForwardAuth: User #{user.email_address} denied access to #{forwarded_host} by rule #{rule.domain_pattern}"
if app
# Check if user is allowed by this application
unless app.user_allowed?(user)
Rails.logger.info "ForwardAuth: User #{user.email_address} denied access to #{forwarded_host} by app #{app.domain_pattern}"
return render_forbidden("You do not have permission to access this domain")
end
Rails.logger.info "ForwardAuth: User #{user.email_address} granted access to #{forwarded_host} by rule #{rule.domain_pattern} (policy: #{rule.policy_for_user(user)})"
Rails.logger.info "ForwardAuth: User #{user.email_address} granted access to #{forwarded_host} by app #{app.domain_pattern} (policy: #{app.policy_for_user(user)})"
else
# No rule found - allow access with default headers (original behavior)
Rails.logger.info "ForwardAuth: No rule found for domain: #{forwarded_host}, allowing with default headers"
# No application found - allow access with default headers (original behavior)
Rails.logger.info "ForwardAuth: No application found for domain: #{forwarded_host}, allowing with default headers"
end
else
Rails.logger.info "ForwardAuth: User #{user.email_address} authenticated (no domain specified)"
end
# User is authenticated and authorized
# Return 200 with user information headers using rule-specific configuration
headers = rule ? rule.headers_for_user(user) : ForwardAuthRule::DEFAULT_HEADERS.map { |key, header_name|
# Return 200 with user information headers using app-specific configuration
headers = app ? app.headers_for_user(user) : Application::DEFAULT_HEADERS.map { |key, header_name|
case key
when :user, :email, :name
[header_name, user.email_address]
@@ -127,7 +127,7 @@ module Api
end
def extract_app_from_headers
# This method is deprecated since we now use ForwardAuthRule domain matching
# This method is deprecated since we now use Application (forward_auth type) domain matching
# Keeping it for backward compatibility but it's no longer used
nil
end
@@ -195,12 +195,12 @@ module Api
redirect_domain = uri.host.downcase
return nil unless redirect_domain.present?
# Check against our ForwardAuthRules
matching_rule = ForwardAuthRule.active.find do |rule|
rule.matches_domain?(redirect_domain)
# Check against our ForwardAuth applications
matching_app = Application.forward_auth.active.find do |app|
app.matches_domain?(redirect_domain)
end
matching_rule ? url : nil
matching_app ? url : nil
rescue URI::InvalidURIError
nil
@@ -210,8 +210,8 @@ module Api
def domain_has_forward_auth_rule?(domain)
return false if domain.blank?
ForwardAuthRule.active.any? do |rule|
rule.matches_domain?(domain.downcase)
Application.forward_auth.active.any? do |app|
app.matches_domain?(domain.downcase)
end
end
end

View File

@@ -302,6 +302,14 @@ class OidcController < ApplicationController
# Add admin claim if user is admin
claims[:admin] = true if user.admin?
# Merge custom claims from groups
user.groups.each do |group|
claims.merge!(group.parsed_custom_claims)
end
# Merge custom claims from user (overrides group claims)
claims.merge!(user.parsed_custom_claims)
render json: claims
end