OIDC app creation with encrypted secrets and application roles
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-24 14:47:24 +11:00
parent 831bd083c2
commit 12e0ef66ed
32 changed files with 1983 additions and 72 deletions

View File

@@ -1,8 +1,12 @@
class Application < ApplicationRecord
has_secure_password :client_secret
has_many :application_groups, dependent: :destroy
has_many :allowed_groups, through: :application_groups, source: :group
has_many :oidc_authorization_codes, dependent: :destroy
has_many :oidc_access_tokens, dependent: :destroy
has_many :application_roles, dependent: :destroy
has_many :user_role_assignments, through: :application_roles
validates :name, presence: true
validates :slug, presence: true, uniqueness: { case_sensitive: false },
@@ -10,6 +14,7 @@ class Application < ApplicationRecord
validates :app_type, presence: true,
inclusion: { in: %w[oidc saml] }
validates :client_id, uniqueness: { allow_nil: true }
validates :role_mapping_mode, inclusion: { in: %w[disabled oidc_managed hybrid] }, allow_blank: true
normalizes :slug, with: ->(slug) { slug.strip.downcase }
@@ -19,6 +24,8 @@ class Application < ApplicationRecord
scope :active, -> { where(active: true) }
scope :oidc, -> { where(app_type: "oidc") }
scope :saml, -> { where(app_type: "saml") }
scope :oidc_managed_roles, -> { where(role_mapping_mode: "oidc_managed") }
scope :hybrid_roles, -> { where(role_mapping_mode: "hybrid") }
# Type checks
def oidc?
@@ -29,6 +36,19 @@ class Application < ApplicationRecord
app_type == "saml"
end
# Role mapping checks
def role_mapping_enabled?
role_mapping_mode.in?(['oidc_managed', 'hybrid'])
end
def oidc_managed_roles?
role_mapping_mode == 'oidc_managed'
end
def hybrid_roles?
role_mapping_mode == 'hybrid'
end
# Access control
def user_allowed?(user)
return false unless active?
@@ -56,10 +76,67 @@ class Application < ApplicationRecord
{}
end
def parsed_managed_permissions
return {} unless managed_permissions.present?
managed_permissions.is_a?(Hash) ? managed_permissions : JSON.parse(managed_permissions)
rescue JSON::ParserError
{}
end
# Role management methods
def user_roles(user)
application_roles.joins(:user_role_assignments)
.where(user_role_assignments: { user: user })
.active
end
def user_has_role?(user, role_name)
user_roles(user).exists?(name: role_name)
end
def assign_role_to_user!(user, role_name, source: 'manual', metadata: {})
role = application_roles.active.find_by!(name: role_name)
role.assign_to_user!(user, source: source, metadata: metadata)
end
def remove_role_from_user!(user, role_name)
role = application_roles.find_by!(name: role_name)
role.remove_from_user!(user)
end
# Enhanced access control with roles
def user_allowed_with_roles?(user)
return user_allowed?(user) unless role_mapping_enabled?
# For OIDC managed roles, check if user has any roles assigned
if oidc_managed_roles?
return user_roles(user).exists?
end
# For hybrid mode, either group-based access or role-based access works
if hybrid_roles?
return user_allowed?(user) || user_roles(user).exists?
end
user_allowed?(user)
end
# Generate and return a new client secret
def generate_new_client_secret!
secret = SecureRandom.urlsafe_base64(48)
self.client_secret = secret
self.save!
secret
end
private
def generate_client_credentials
self.client_id ||= SecureRandom.urlsafe_base64(32)
self.client_secret ||= SecureRandom.urlsafe_base64(48)
# Generate and hash the client secret
if new_record? && client_secret.blank?
secret = SecureRandom.urlsafe_base64(48)
self.client_secret = secret
end
end
end

View File

@@ -0,0 +1,26 @@
class ApplicationRole < ApplicationRecord
belongs_to :application
has_many :user_role_assignments, dependent: :destroy
has_many :users, through: :user_role_assignments
validates :name, presence: true, uniqueness: { scope: :application_id }
validates :display_name, presence: true
scope :active, -> { where(active: true) }
def user_has_role?(user)
user_role_assignments.exists?(user: user)
end
def assign_to_user!(user, source: 'oidc', metadata: {})
user_role_assignments.find_or_create_by!(user: user) do |assignment|
assignment.source = source
assignment.metadata = metadata
end
end
def remove_from_user!(user)
assignment = user_role_assignments.find_by(user: user)
assignment&.destroy
end
end

View File

@@ -3,6 +3,8 @@ class User < ApplicationRecord
has_many :sessions, dependent: :destroy
has_many :user_groups, dependent: :destroy
has_many :groups, through: :user_groups
has_many :user_role_assignments, dependent: :destroy
has_many :application_roles, through: :user_role_assignments
# Token generation for passwordless flows
generates_token_for :invitation, expires_in: 7.days

View File

@@ -0,0 +1,15 @@
class UserRoleAssignment < ApplicationRecord
belongs_to :user
belongs_to :application_role
validates :user, uniqueness: { scope: :application_role }
validates :source, inclusion: { in: %w[oidc manual group_sync] }
scope :oidc_managed, -> { where(source: 'oidc') }
scope :manually_assigned, -> { where(source: 'manual') }
scope :group_synced, -> { where(source: 'group_sync') }
def sync_from_oidc?
source == 'oidc'
end
end