StandardRB fixes
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
2026-01-01 13:29:44 +11:00
parent 7d3af2bcec
commit 93a0edb0a2
79 changed files with 779 additions and 786 deletions

View File

@@ -19,16 +19,16 @@ class Application < ApplicationRecord
has_many :oidc_user_consents, dependent: :destroy
validates :name, presence: true
validates :slug, presence: true, uniqueness: { case_sensitive: false },
format: { with: /\A[a-z0-9\-]+\z/, message: "only lowercase letters, numbers, and hyphens" }
validates :slug, presence: true, uniqueness: {case_sensitive: false},
format: {with: /\A[a-z0-9-]+\z/, message: "only lowercase letters, numbers, and hyphens"}
validates :app_type, presence: true,
inclusion: { in: %w[oidc forward_auth] }
validates :client_id, uniqueness: { allow_nil: true }
inclusion: {in: %w[oidc forward_auth]}
validates :client_id, uniqueness: {allow_nil: true}
validates :client_secret, presence: true, on: :create, if: -> { oidc? && confidential_client? }
validates :domain_pattern, presence: true, uniqueness: { case_sensitive: false }, if: :forward_auth?
validates :landing_url, format: { with: URI::regexp(%w[http https]), allow_nil: true, message: "must be a valid URL" }
validates :domain_pattern, presence: true, uniqueness: {case_sensitive: false}, if: :forward_auth?
validates :landing_url, format: {with: URI::RFC2396_PARSER.make_regexp(%w[http https]), allow_nil: true, message: "must be a valid URL"}
validates :backchannel_logout_uri, format: {
with: URI::regexp(%w[http https]),
with: URI::RFC2396_PARSER.make_regexp(%w[http https]),
allow_nil: true,
message: "must be a valid HTTP or HTTPS URL"
}
@@ -38,9 +38,9 @@ class Application < ApplicationRecord
validate :icon_validation, if: -> { icon.attached? }
# Token TTL validations (for OIDC apps)
validates :access_token_ttl, numericality: { greater_than_or_equal_to: 300, less_than_or_equal_to: 86400 }, if: :oidc? # 5 min - 24 hours
validates :refresh_token_ttl, numericality: { greater_than_or_equal_to: 86400, less_than_or_equal_to: 7776000 }, if: :oidc? # 1 day - 90 days
validates :id_token_ttl, numericality: { greater_than_or_equal_to: 300, less_than_or_equal_to: 86400 }, if: :oidc? # 5 min - 24 hours
validates :access_token_ttl, numericality: {greater_than_or_equal_to: 300, less_than_or_equal_to: 86400}, if: :oidc? # 5 min - 24 hours
validates :refresh_token_ttl, numericality: {greater_than_or_equal_to: 86400, less_than_or_equal_to: 7776000}, if: :oidc? # 1 day - 90 days
validates :id_token_ttl, numericality: {greater_than_or_equal_to: 300, less_than_or_equal_to: 86400}, if: :oidc? # 5 min - 24 hours
normalizes :slug, with: ->(slug) { slug.strip.downcase }
normalizes :domain_pattern, with: ->(pattern) {
@@ -56,11 +56,11 @@ class Application < ApplicationRecord
# Default header configuration for ForwardAuth
DEFAULT_HEADERS = {
user: 'X-Remote-User',
email: 'X-Remote-Email',
name: 'X-Remote-Name',
groups: 'X-Remote-Groups',
admin: 'X-Remote-Admin'
user: "X-Remote-User",
email: "X-Remote-Email",
name: "X-Remote-Name",
groups: "X-Remote-Groups",
admin: "X-Remote-Admin"
}.freeze
# Scopes
@@ -135,8 +135,8 @@ class Application < ApplicationRecord
def matches_domain?(domain)
return false if domain.blank? || !forward_auth?
pattern = domain_pattern.gsub('.', '\.')
pattern = pattern.gsub('*', '[^.]*')
pattern = domain_pattern.gsub(".", '\.')
pattern = pattern.gsub("*", "[^.]*")
regex = Regexp.new("^#{pattern}$", Regexp::IGNORECASE)
regex.match?(domain.downcase)
@@ -144,18 +144,18 @@ class Application < ApplicationRecord
# Policy determination based on user status (for ForwardAuth)
def policy_for_user(user)
return 'deny' unless active?
return 'deny' unless user.active?
return "deny" unless active?
return "deny" unless user.active?
# If no groups specified, bypass authentication
return 'bypass' if allowed_groups.empty?
return "bypass" if allowed_groups.empty?
# If user is in allowed groups, determine auth level
if user_allowed?(user)
# Require 2FA if user has TOTP configured, otherwise one factor
user.totp_enabled? ? 'two_factor' : 'one_factor'
user.totp_enabled? ? "two_factor" : "one_factor"
else
'deny'
"deny"
end
end
@@ -197,7 +197,7 @@ class Application < ApplicationRecord
def generate_new_client_secret!
secret = SecureRandom.urlsafe_base64(48)
self.client_secret = secret
self.save!
save!
secret
end
@@ -242,7 +242,7 @@ class Application < ApplicationRecord
# (i.e., has valid, non-revoked tokens)
def user_has_active_session?(user)
oidc_access_tokens.where(user: user).valid.exists? ||
oidc_refresh_tokens.where(user: user).valid.exists?
oidc_refresh_tokens.where(user: user).valid.exists?
end
private
@@ -260,14 +260,14 @@ class Application < ApplicationRecord
return unless icon.attached?
# Check content type
allowed_types = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'image/svg+xml']
allowed_types = ["image/png", "image/jpg", "image/jpeg", "image/gif", "image/svg+xml"]
unless allowed_types.include?(icon.content_type)
errors.add(:icon, 'must be a PNG, JPG, GIF, or SVG image')
errors.add(:icon, "must be a PNG, JPG, GIF, or SVG image")
end
# Check file size (2MB limit)
if icon.blob.byte_size > 2.megabytes
errors.add(:icon, 'must be less than 2MB')
errors.add(:icon, "must be less than 2MB")
end
end
@@ -302,8 +302,8 @@ class Application < ApplicationRecord
begin
uri = URI.parse(backchannel_logout_uri)
unless uri.scheme == 'https'
errors.add(:backchannel_logout_uri, 'must use HTTPS in production')
unless uri.scheme == "https"
errors.add(:backchannel_logout_uri, "must use HTTPS in production")
end
rescue URI::InvalidURIError
# Let the format validator handle invalid URIs

View File

@@ -2,5 +2,5 @@ class ApplicationGroup < ApplicationRecord
belongs_to :application
belongs_to :group
validates :application_id, uniqueness: { scope: :group_id }
validates :application_id, uniqueness: {scope: :group_id}
end

View File

@@ -9,7 +9,7 @@ class ApplicationUserClaim < ApplicationRecord
groups
].freeze
validates :user_id, uniqueness: { scope: :application_id }
validates :user_id, uniqueness: {scope: :application_id}
validate :no_reserved_claim_names
# Parse custom_claims JSON field
@@ -25,7 +25,7 @@ class ApplicationUserClaim < ApplicationRecord
reserved_used = parsed_custom_claims.keys.map(&:to_s) & RESERVED_CLAIMS
if reserved_used.any?
errors.add(:custom_claims, "cannot override reserved OIDC claims: #{reserved_used.join(', ')}")
errors.add(:custom_claims, "cannot override reserved OIDC claims: #{reserved_used.join(", ")}")
end
end
end

View File

@@ -11,7 +11,7 @@ class Group < ApplicationRecord
groups
].freeze
validates :name, presence: true, uniqueness: { case_sensitive: false }
validates :name, presence: true, uniqueness: {case_sensitive: false}
normalizes :name, with: ->(name) { name.strip.downcase }
validate :no_reserved_claim_names
@@ -28,7 +28,7 @@ class Group < ApplicationRecord
reserved_used = parsed_custom_claims.keys.map(&:to_s) & RESERVED_CLAIMS
if reserved_used.any?
errors.add(:custom_claims, "cannot override reserved OIDC claims: #{reserved_used.join(', ')}")
errors.add(:custom_claims, "cannot override reserved OIDC claims: #{reserved_used.join(", ")}")
end
end
end

View File

@@ -25,7 +25,7 @@ class OidcAccessToken < ApplicationRecord
# Compute HMAC for token lookup
def self.compute_token_hmac(plaintext_token)
OpenSSL::HMAC.hexdigest('SHA256', TokenHmac::KEY, plaintext_token)
OpenSSL::HMAC.hexdigest("SHA256", TokenHmac::KEY, plaintext_token)
end
def expired?

View File

@@ -9,7 +9,7 @@ class OidcAuthorizationCode < ApplicationRecord
validates :code_hmac, presence: true, uniqueness: true
validates :redirect_uri, presence: true
validates :code_challenge_method, inclusion: { in: %w[plain S256], allow_nil: true }
validates :code_challenge_method, inclusion: {in: %w[plain S256], allow_nil: true}
validate :validate_code_challenge_format, if: -> { code_challenge.present? }
scope :valid, -> { where(used: false).where("expires_at > ?", Time.current) }
@@ -25,7 +25,7 @@ class OidcAuthorizationCode < ApplicationRecord
# Compute HMAC for code lookup
def self.compute_code_hmac(plaintext_code)
OpenSSL::HMAC.hexdigest('SHA256', TokenHmac::KEY, plaintext_code)
OpenSSL::HMAC.hexdigest("SHA256", TokenHmac::KEY, plaintext_code)
end
def expired?

View File

@@ -29,7 +29,7 @@ class OidcRefreshToken < ApplicationRecord
# Compute HMAC for token lookup
def self.compute_token_hmac(plaintext_token)
OpenSSL::HMAC.hexdigest('SHA256', TokenHmac::KEY, plaintext_token)
OpenSSL::HMAC.hexdigest("SHA256", TokenHmac::KEY, plaintext_token)
end
def expired?

View File

@@ -3,19 +3,19 @@ class OidcUserConsent < ApplicationRecord
belongs_to :application
validates :user, :application, :scopes_granted, :granted_at, presence: true
validates :user_id, uniqueness: { scope: :application_id }
validates :user_id, uniqueness: {scope: :application_id}
before_validation :set_granted_at, on: :create
before_validation :set_sid, on: :create
# Parse scopes_granted into an array
def scopes
scopes_granted.split(' ')
scopes_granted.split(" ")
end
# Set scopes from an array
def scopes=(scope_array)
self.scopes_granted = Array(scope_array).uniq.join(' ')
self.scopes_granted = Array(scope_array).uniq.join(" ")
end
# Check if this consent covers the requested scopes
@@ -31,18 +31,18 @@ class OidcUserConsent < ApplicationRecord
def formatted_scopes
scopes.map do |scope|
case scope
when 'openid'
'Basic authentication'
when 'profile'
'Profile information'
when 'email'
'Email address'
when 'groups'
'Group membership'
when "openid"
"Basic authentication"
when "profile"
"Profile information"
when "email"
"Email address"
when "groups"
"Group membership"
else
scope.humanize
end
end.join(', ')
end.join(", ")
end
# Find consent by SID

View File

@@ -29,16 +29,16 @@ class User < ApplicationRecord
groups
].freeze
validates :email_address, presence: true, uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :username, uniqueness: { case_sensitive: false }, allow_nil: true,
format: { with: /\A[a-zA-Z0-9_-]+\z/, message: "can only contain letters, numbers, underscores, and hyphens" },
length: { minimum: 2, maximum: 30 }
validates :password, length: { minimum: 8 }, allow_nil: true
validates :email_address, presence: true, uniqueness: {case_sensitive: false},
format: {with: URI::MailTo::EMAIL_REGEXP}
validates :username, uniqueness: {case_sensitive: false}, allow_nil: true,
format: {with: /\A[a-zA-Z0-9_-]+\z/, message: "can only contain letters, numbers, underscores, and hyphens"},
length: {minimum: 2, maximum: 30}
validates :password, length: {minimum: 8}, allow_nil: true
validate :no_reserved_claim_names
# Enum - automatically creates scopes (User.active, User.disabled, etc.)
enum :status, { active: 0, disabled: 1, pending_invitation: 2 }
enum :status, {active: 0, disabled: 1, pending_invitation: 2}
# Scopes
scope :admins, -> { where(admin: true) }
@@ -122,12 +122,7 @@ class User < ApplicationRecord
cache_key = "backup_code_failed_attempts_#{id}"
attempts = Rails.cache.read(cache_key) || 0
if attempts >= 5 # Allow max 5 failed attempts per hour
true
else
# Don't increment here - increment only on failed attempts
false
end
attempts >= 5
end
# Increment failed attempt counter
@@ -231,7 +226,7 @@ class User < ApplicationRecord
reserved_used = parsed_custom_claims.keys.map(&:to_s) & RESERVED_CLAIMS
if reserved_used.any?
errors.add(:custom_claims, "cannot override reserved OIDC claims: #{reserved_used.join(', ')}")
errors.add(:custom_claims, "cannot override reserved OIDC claims: #{reserved_used.join(", ")}")
end
end

View File

@@ -2,5 +2,5 @@ class UserGroup < ApplicationRecord
belongs_to :user
belongs_to :group
validates :user_id, uniqueness: { scope: :group_id }
validates :user_id, uniqueness: {scope: :group_id}
end

View File

@@ -4,9 +4,9 @@ class WebauthnCredential < ApplicationRecord
# Validations
validates :external_id, presence: true, uniqueness: true
validates :public_key, presence: true
validates :sign_count, presence: true, numericality: { greater_than_or_equal_to: 0, only_integer: true }
validates :sign_count, presence: true, numericality: {greater_than_or_equal_to: 0, only_integer: true}
validates :nickname, presence: true
validates :authenticator_type, inclusion: { in: %w[platform cross-platform] }
validates :authenticator_type, inclusion: {in: %w[platform cross-platform]}
# Scopes for querying
scope :active, -> { where(nil) } # All credentials are active (we can add revoked_at later if needed)
@@ -84,13 +84,13 @@ class WebauthnCredential < ApplicationRecord
days = hours / 24
if days > 0
"#{days.floor} day#{'s' if days > 1} ago"
"#{days.floor} day#{"s" if days > 1} ago"
elsif hours > 0
"#{hours.floor} hour#{'s' if hours > 1} ago"
"#{hours.floor} hour#{"s" if hours > 1} ago"
elsif minutes > 0
"#{minutes.floor} minute#{'s' if minutes > 1} ago"
"#{minutes.floor} minute#{"s" if minutes > 1} ago"
else
"Just now"
end
end
end
end