OIDC app creation with encrypted secrets and application roles
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
module Admin
|
||||
class ApplicationsController < BaseController
|
||||
before_action :set_application, only: [:show, :edit, :update, :destroy, :regenerate_credentials]
|
||||
before_action :set_application, only: [:show, :edit, :update, :destroy, :regenerate_credentials, :roles, :create_role, :update_role, :assign_role, :remove_role]
|
||||
|
||||
def index
|
||||
@applications = Application.order(created_at: :desc)
|
||||
@@ -17,6 +17,7 @@ module Admin
|
||||
|
||||
def create
|
||||
@application = Application.new(application_params)
|
||||
@available_groups = Group.order(:name)
|
||||
|
||||
if @application.save
|
||||
# Handle group assignments
|
||||
@@ -25,9 +26,22 @@ module Admin
|
||||
@application.allowed_groups = Group.where(id: group_ids)
|
||||
end
|
||||
|
||||
redirect_to admin_application_path(@application), notice: "Application created successfully."
|
||||
# Get the plain text client secret to show one time
|
||||
client_secret = nil
|
||||
if @application.oidc?
|
||||
client_secret = @application.generate_new_client_secret!
|
||||
end
|
||||
|
||||
if @application.oidc? && client_secret
|
||||
flash[:notice] = "Application created successfully."
|
||||
flash[:client_id] = @application.client_id
|
||||
flash[:client_secret] = client_secret
|
||||
else
|
||||
flash[:notice] = "Application created successfully."
|
||||
end
|
||||
|
||||
redirect_to admin_application_path(@application)
|
||||
else
|
||||
@available_groups = Group.order(:name)
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -60,16 +74,69 @@ module Admin
|
||||
|
||||
def regenerate_credentials
|
||||
if @application.oidc?
|
||||
@application.update!(
|
||||
client_id: SecureRandom.urlsafe_base64(32),
|
||||
client_secret: SecureRandom.urlsafe_base64(48)
|
||||
)
|
||||
redirect_to admin_application_path(@application), notice: "Credentials regenerated successfully. Make sure to update your application configuration."
|
||||
# Generate new client ID and secret
|
||||
new_client_id = SecureRandom.urlsafe_base64(32)
|
||||
client_secret = @application.generate_new_client_secret!
|
||||
|
||||
@application.update!(client_id: new_client_id)
|
||||
|
||||
flash[:notice] = "Credentials regenerated successfully."
|
||||
flash[:client_id] = @application.client_id
|
||||
flash[:client_secret] = client_secret
|
||||
|
||||
redirect_to admin_application_path(@application)
|
||||
else
|
||||
redirect_to admin_application_path(@application), alert: "Only OIDC applications have credentials."
|
||||
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
|
||||
@@ -77,7 +144,14 @@ module Admin
|
||||
end
|
||||
|
||||
def application_params
|
||||
params.require(:application).permit(:name, :slug, :app_type, :active, :redirect_uris, :description, :metadata)
|
||||
params.require(:application).permit(
|
||||
:name, :slug, :app_type, :active, :redirect_uris, :description, :metadata,
|
||||
:role_mapping_mode, :role_prefix, :role_claim_name, managed_permissions: {}
|
||||
)
|
||||
end
|
||||
|
||||
def role_params
|
||||
params.require(:application_role).permit(:name, :display_name, :description, :active, permissions: {})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -161,7 +161,7 @@ class OidcController < ApplicationController
|
||||
|
||||
# Find and validate the application
|
||||
application = Application.find_by(client_id: client_id)
|
||||
unless application && application.client_secret == client_secret
|
||||
unless application && application.authenticate_client_secret(client_secret)
|
||||
render json: { error: "invalid_client" }, status: :unauthorized
|
||||
return
|
||||
end
|
||||
|
||||
51
app/javascript/controllers/role_management_controller.js
Normal file
51
app/javascript/controllers/role_management_controller.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["userSelect", "assignLink", "editForm"]
|
||||
|
||||
connect() {
|
||||
console.log("Role management controller connected")
|
||||
}
|
||||
|
||||
assignRole(event) {
|
||||
event.preventDefault()
|
||||
|
||||
const link = event.currentTarget
|
||||
const roleId = link.dataset.roleId
|
||||
const select = document.getElementById(`assign-user-${roleId}`)
|
||||
|
||||
if (!select.value) {
|
||||
alert("Please select a user")
|
||||
return
|
||||
}
|
||||
|
||||
// Update the href with the selected user ID
|
||||
const originalHref = link.href
|
||||
const newHref = originalHref.replace("PLACEHOLDER", select.value)
|
||||
|
||||
// Navigate to the updated URL
|
||||
window.location.href = newHref
|
||||
}
|
||||
|
||||
toggleEdit(event) {
|
||||
event.preventDefault()
|
||||
|
||||
const roleId = event.currentTarget.dataset.roleId
|
||||
const editForm = document.getElementById(`edit-role-${roleId}`)
|
||||
|
||||
if (editForm) {
|
||||
editForm.classList.toggle("hidden")
|
||||
}
|
||||
}
|
||||
|
||||
hideEdit(event) {
|
||||
event.preventDefault()
|
||||
|
||||
const roleId = event.currentTarget.dataset.roleId
|
||||
const editForm = document.getElementById(`edit-role-${roleId}`)
|
||||
|
||||
if (editForm) {
|
||||
editForm.classList.add("hidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
26
app/models/application_role.rb
Normal file
26
app/models/application_role.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
15
app/models/user_role_assignment.rb
Normal file
15
app/models/user_role_assignment.rb
Normal 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
|
||||
@@ -27,6 +27,11 @@ class OidcJwtService
|
||||
# Add admin claim if user is admin
|
||||
payload[:admin] = true if user.admin?
|
||||
|
||||
# Add role-based claims if role mapping is enabled
|
||||
if application.role_mapping_enabled?
|
||||
add_role_claims!(payload, user, application)
|
||||
end
|
||||
|
||||
JWT.encode(payload, private_key, "RS256", { kid: key_id, typ: "JWT" })
|
||||
end
|
||||
|
||||
@@ -88,5 +93,50 @@ class OidcJwtService
|
||||
def key_id
|
||||
@key_id ||= Digest::SHA256.hexdigest(public_key.to_pem)[0..15]
|
||||
end
|
||||
|
||||
# Add role-based claims to the JWT payload
|
||||
def add_role_claims!(payload, user, application)
|
||||
user_roles = application.user_roles(user)
|
||||
return if user_roles.empty?
|
||||
|
||||
role_names = user_roles.pluck(:name)
|
||||
|
||||
# Filter roles by prefix if configured
|
||||
if application.role_prefix.present?
|
||||
role_names = role_names.select { |role| role.start_with?(application.role_prefix) }
|
||||
end
|
||||
|
||||
return if role_names.empty?
|
||||
|
||||
# Add roles using the configured claim name
|
||||
claim_name = application.role_claim_name.presence || 'roles'
|
||||
payload[claim_name] = role_names
|
||||
|
||||
# Add role permissions if configured
|
||||
managed_permissions = application.parsed_managed_permissions
|
||||
if managed_permissions['include_permissions'] == true
|
||||
role_permissions = user_roles.map do |role|
|
||||
{
|
||||
name: role.name,
|
||||
display_name: role.display_name,
|
||||
permissions: role.permissions
|
||||
}
|
||||
end
|
||||
payload['role_permissions'] = role_permissions
|
||||
end
|
||||
|
||||
# Add role metadata if configured
|
||||
if managed_permissions['include_metadata'] == true
|
||||
role_metadata = user_roles.map do |role|
|
||||
assignment = role.user_role_assignments.find_by(user: user)
|
||||
{
|
||||
name: role.name,
|
||||
source: assignment&.source,
|
||||
assigned_at: assignment&.created_at
|
||||
}
|
||||
end
|
||||
payload['role_metadata'] = role_metadata
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
127
app/services/role_mapping_engine.rb
Normal file
127
app/services/role_mapping_engine.rb
Normal file
@@ -0,0 +1,127 @@
|
||||
class RoleMappingEngine
|
||||
class << self
|
||||
# Sync user roles from OIDC claims
|
||||
def sync_user_roles!(user, application, claims)
|
||||
return unless application.role_mapping_enabled?
|
||||
|
||||
# Extract roles from claims
|
||||
external_roles = extract_roles_from_claims(application, claims)
|
||||
|
||||
case application.role_mapping_mode
|
||||
when 'oidc_managed'
|
||||
sync_oidc_managed_roles!(user, application, external_roles)
|
||||
when 'hybrid'
|
||||
sync_hybrid_roles!(user, application, external_roles)
|
||||
end
|
||||
end
|
||||
|
||||
# Check if user is allowed based on roles
|
||||
def user_allowed_with_roles?(user, application, claims = nil)
|
||||
return application.user_allowed_with_roles?(user) unless claims
|
||||
|
||||
if application.oidc_managed_roles?
|
||||
external_roles = extract_roles_from_claims(application, claims)
|
||||
return false if external_roles.empty?
|
||||
|
||||
# Check if any external role matches configured application roles
|
||||
application.application_roles.active.exists?(name: external_roles)
|
||||
elsif application.hybrid_roles?
|
||||
# Allow access if either group-based or role-based access works
|
||||
application.user_allowed?(user) ||
|
||||
(external_roles.present? &&
|
||||
application.application_roles.active.exists?(name: external_roles))
|
||||
else
|
||||
application.user_allowed?(user)
|
||||
end
|
||||
end
|
||||
|
||||
# Get available roles for a user in an application
|
||||
def user_available_roles(user, application)
|
||||
return [] unless application.role_mapping_enabled?
|
||||
|
||||
application.application_roles.active
|
||||
end
|
||||
|
||||
# Map external roles to internal roles
|
||||
def map_external_to_internal_roles(application, external_roles)
|
||||
return [] if external_roles.empty?
|
||||
|
||||
configured_roles = application.application_roles.active.pluck(:name)
|
||||
|
||||
# Apply role prefix filtering
|
||||
if application.role_prefix.present?
|
||||
external_roles = external_roles.select { |role| role.start_with?(application.role_prefix) }
|
||||
end
|
||||
|
||||
# Find matching internal roles
|
||||
external_roles & configured_roles
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Extract roles from various claim sources
|
||||
def extract_roles_from_claims(application, claims)
|
||||
claim_name = application.role_claim_name.presence || 'roles'
|
||||
|
||||
# Try the configured claim name first
|
||||
roles = claims[claim_name]
|
||||
|
||||
# Fallback to common claim names if not found
|
||||
roles ||= claims['roles']
|
||||
roles ||= claims['groups']
|
||||
roles ||= claims['http://schemas.microsoft.com/ws/2008/06/identity/claims/role']
|
||||
|
||||
# Ensure roles is an array
|
||||
case roles
|
||||
when String
|
||||
[roles]
|
||||
when Array
|
||||
roles
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Sync roles for OIDC managed mode (replace existing roles)
|
||||
def sync_oidc_managed_roles!(user, application, external_roles)
|
||||
# Map external roles to internal roles
|
||||
internal_roles = map_external_to_internal_roles(application, external_roles)
|
||||
|
||||
# Get current OIDC-managed roles
|
||||
current_assignments = user.user_role_assignments
|
||||
.joins(:application_role)
|
||||
.where(application_role: { application: application })
|
||||
.oidc_managed
|
||||
.includes(:application_role)
|
||||
|
||||
current_role_names = current_assignments.map { |assignment| assignment.application_role.name }
|
||||
|
||||
# Remove roles that are no longer in external roles
|
||||
roles_to_remove = current_role_names - internal_roles
|
||||
roles_to_remove.each do |role_name|
|
||||
application.remove_role_from_user!(user, role_name)
|
||||
end
|
||||
|
||||
# Add new roles
|
||||
roles_to_add = internal_roles - current_role_names
|
||||
roles_to_add.each do |role_name|
|
||||
application.assign_role_to_user!(user, role_name, source: 'oidc',
|
||||
metadata: { synced_at: Time.current })
|
||||
end
|
||||
end
|
||||
|
||||
# Sync roles for hybrid mode (merge with existing roles)
|
||||
def sync_hybrid_roles!(user, application, external_roles)
|
||||
# Map external roles to internal roles
|
||||
internal_roles = map_external_to_internal_roles(application, external_roles)
|
||||
|
||||
# Only add new roles, don't remove manually assigned ones
|
||||
internal_roles.each do |role_name|
|
||||
next if application.user_has_role?(user, role_name)
|
||||
|
||||
application.assign_role_to_user!(user, role_name, source: 'oidc',
|
||||
metadata: { synced_at: Time.current })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -51,6 +51,52 @@
|
||||
<%= form.text_area :redirect_uris, rows: 4, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono", placeholder: "https://example.com/callback\nhttps://app.example.com/auth/callback" %>
|
||||
<p class="mt-1 text-sm text-gray-500">One URI per line. These are the allowed callback URLs for your application.</p>
|
||||
</div>
|
||||
|
||||
<!-- Role Mapping Configuration -->
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<h4 class="text-base font-semibold text-gray-900 mb-4">Role Mapping Configuration</h4>
|
||||
|
||||
<div>
|
||||
<%= form.label :role_mapping_mode, "Role Mapping Mode", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.select :role_mapping_mode,
|
||||
options_for_select([
|
||||
["Disabled", "disabled"],
|
||||
["OIDC Managed", "oidc_managed"],
|
||||
["Hybrid (Groups + Roles)", "hybrid"]
|
||||
], application.role_mapping_mode || "disabled"),
|
||||
{},
|
||||
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" } %>
|
||||
<p class="mt-1 text-sm text-gray-500">Controls how external roles are mapped and synchronized.</p>
|
||||
</div>
|
||||
|
||||
<div id="role-mapping-advanced" class="mt-4 space-y-4 border-t border-gray-200 pt-4" style="<%= 'display: none;' unless application.role_mapping_enabled? %>">
|
||||
<div>
|
||||
<%= form.label :role_claim_name, "Role Claim Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :role_claim_name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "roles" %>
|
||||
<p class="mt-1 text-sm text-gray-500">Name of the claim that contains role information (default: 'roles').</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :role_prefix, "Role Prefix (Optional)", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :role_prefix, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "app-" %>
|
||||
<p class="mt-1 text-sm text-gray-500">Only roles starting with this prefix will be mapped. Useful for multi-tenant scenarios.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm font-medium text-gray-700">Managed Permissions</label>
|
||||
|
||||
<div class="flex items-center">
|
||||
<%= form.check_box :managed_permissions, { multiple: true, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" }, "include_permissions", "" %>
|
||||
<%= form.label :managed_permissions_include_permissions, "Include role permissions in tokens", class: "ml-2 block text-sm text-gray-900" %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<%= form.check_box :managed_permissions, { multiple: true, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" }, "include_metadata", "" %>
|
||||
<%= form.label :managed_permissions_include_metadata, "Include role metadata in tokens", class: "ml-2 block text-sm text-gray-900" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -86,14 +132,30 @@
|
||||
// Show/hide OIDC fields based on app type selection
|
||||
const appTypeSelect = document.querySelector('#application_app_type');
|
||||
const oidcFields = document.querySelector('#oidc-fields');
|
||||
const roleMappingMode = document.querySelector('#application_role_mapping_mode');
|
||||
const roleMappingAdvanced = document.querySelector('#role-mapping-advanced');
|
||||
|
||||
function updateFieldVisibility() {
|
||||
const isOidc = appTypeSelect.value === 'oidc';
|
||||
const roleMappingEnabled = roleMappingMode && ['oidc_managed', 'hybrid'].includes(roleMappingMode.value);
|
||||
|
||||
if (oidcFields) {
|
||||
oidcFields.style.display = isOidc ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (roleMappingAdvanced) {
|
||||
roleMappingAdvanced.style.display = isOidc && roleMappingEnabled ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (appTypeSelect && oidcFields) {
|
||||
appTypeSelect.addEventListener('change', function() {
|
||||
if (this.value === 'oidc') {
|
||||
oidcFields.style.display = 'block';
|
||||
} else {
|
||||
oidcFields.style.display = 'none';
|
||||
}
|
||||
});
|
||||
appTypeSelect.addEventListener('change', updateFieldVisibility);
|
||||
}
|
||||
|
||||
if (roleMappingMode) {
|
||||
roleMappingMode.addEventListener('change', updateFieldVisibility);
|
||||
}
|
||||
|
||||
// Initialize visibility on page load
|
||||
updateFieldVisibility();
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-semibold text-gray-900">Applications</h1>
|
||||
<p class="mt-2 text-sm text-gray-700">Manage OIDC applications.</p>
|
||||
<p class="mt-2 text-sm text-gray-700">Manage OIDC Clients.</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<%= link_to "New Application", new_admin_application_path, class: "block rounded-md bg-blue-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
|
||||
125
app/views/admin/applications/roles.html.erb
Normal file
125
app/views/admin/applications/roles.html.erb
Normal file
@@ -0,0 +1,125 @@
|
||||
<% content_for :title, "Role Management - #{@application.name}" %>
|
||||
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900">
|
||||
Role Management for <%= @application.name %>
|
||||
</h3>
|
||||
<%= link_to "← Back to Application", admin_application_path(@application), class: "text-sm text-blue-600 hover:text-blue-500" %>
|
||||
</div>
|
||||
|
||||
<% if @application.role_mapping_enabled? %>
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-md p-4 mb-6">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-blue-800">Role Mapping Configuration</h3>
|
||||
<div class="mt-2 text-sm text-blue-700">
|
||||
<p>Mode: <strong><%= @application.role_mapping_mode.humanize %></strong></p>
|
||||
<% if @application.role_claim_name.present? %>
|
||||
<p>Role Claim: <strong><%= @application.role_claim_name %></strong></p>
|
||||
<% end %>
|
||||
<% if @application.role_prefix.present? %>
|
||||
<p>Role Prefix: <strong><%= @application.role_prefix %></strong></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4 mb-6">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-yellow-800">Role Mapping Disabled</h3>
|
||||
<div class="mt-2 text-sm text-yellow-700">
|
||||
<p>Role mapping is currently disabled for this application. Enable it in the application settings to manage roles.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Create New Role -->
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h4 class="text-md font-medium text-gray-900 mb-4">Create New Role</h4>
|
||||
<%= form_with(model: [:admin, @application, ApplicationRole.new], url: create_role_admin_application_path(@application), local: true, class: "space-y-4") do |form| %>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<%= form.label :name, "Role Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "admin" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :display_name, "Display Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :display_name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Administrator" %>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_area :description, rows: 2, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Description of this role's permissions" %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<%= form.check_box :active, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
||||
<%= form.label :active, "Active", class: "ml-2 block text-sm text-gray-900" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.submit "Create Role", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Existing Roles -->
|
||||
<div class="space-y-6">
|
||||
<h4 class="text-md font-medium text-gray-900">Existing Roles</h4>
|
||||
|
||||
<% if @application_roles.any? %>
|
||||
<div class="space-y-4">
|
||||
<% @application_roles.each do |role| %>
|
||||
<div class="border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-3">
|
||||
<h5 class="text-sm font-medium text-gray-900"><%= role.name %></h5>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
<%= role.display_name %>
|
||||
</span>
|
||||
<% unless role.active %>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
Inactive
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if role.description.present? %>
|
||||
<p class="mt-1 text-sm text-gray-500"><%= role.description %></p>
|
||||
<% end %>
|
||||
|
||||
<!-- Assigned Users -->
|
||||
<div class="mt-3">
|
||||
<p class="text-xs text-gray-500 mb-2">Assigned Users:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<% role.users.each do |user| %>
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<%= user.email_address %>
|
||||
<span class="ml-1 text-blue-600">(<%= role.user_role_assignments.find_by(user: user)&.source %>)</span>
|
||||
<%= link_to "×", remove_role_admin_application_path(@application, user_id: user.id, role_id: role.id),
|
||||
method: :post,
|
||||
data: { confirm: "Remove role from #{user.email_address}?" },
|
||||
class: "ml-1 text-blue-600 hover:text-blue-800" %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-12">
|
||||
<div class="text-gray-500 text-sm">
|
||||
No roles configured yet. Create your first role above to get started with role-based access control.
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
173
app/views/admin/applications/roles_backup.html.erb
Normal file
173
app/views/admin/applications/roles_backup.html.erb
Normal file
@@ -0,0 +1,173 @@
|
||||
<% content_for :title, "Role Management - #{@application.name}" %>
|
||||
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900">
|
||||
Role Management for <%= @application.name %>
|
||||
</h3>
|
||||
<%= link_to "← Back to Application", admin_application_path(@application), class: "text-sm text-blue-600 hover:text-blue-500" %>
|
||||
</div>
|
||||
|
||||
<% if @application.role_mapping_enabled? %>
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-md p-4 mb-6">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-blue-800">Role Mapping Configuration</h3>
|
||||
<div class="mt-2 text-sm text-blue-700">
|
||||
<p>Mode: <strong><%= @application.role_mapping_mode.humanize %></strong></p>
|
||||
<% if @application.role_claim_name.present? %>
|
||||
<p>Role Claim: <strong><%= @application.role_claim_name %></strong></p>
|
||||
<% end %>
|
||||
<% if @application.role_prefix.present? %>
|
||||
<p>Role Prefix: <strong><%= @application.role_prefix %></strong></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4 mb-6">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-yellow-800">Role Mapping Disabled</h3>
|
||||
<div class="mt-2 text-sm text-yellow-700">
|
||||
<p>Role mapping is currently disabled for this application. Enable it in the application settings to manage roles.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Create New Role -->
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h4 class="text-md font-medium text-gray-900 mb-4">Create New Role</h4>
|
||||
<%= form_with(model: [:admin, @application, ApplicationRole.new], url: create_role_admin_application_path(@application), local: true, class: "space-y-4") do |form| %>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<%= form.label :name, "Role Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "admin" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :display_name, "Display Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :display_name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Administrator" %>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_area :description, rows: 2, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Description of this role's permissions" %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<%= form.check_box :active, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
||||
<%= form.label :active, "Active", class: "ml-2 block text-sm text-gray-900" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.submit "Create Role", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Existing Roles -->
|
||||
<div class="space-y-6">
|
||||
<h4 class="text-md font-medium text-gray-900">Existing Roles</h4>
|
||||
|
||||
<% if @application_roles.any? %>
|
||||
<div class="space-y-4">
|
||||
<% @application_roles.each do |role| %>
|
||||
<div class="border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-3">
|
||||
<h5 class="text-sm font-medium text-gray-900"><%= role.name %></h5>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
<%= role.display_name %>
|
||||
</span>
|
||||
<% unless role.active %>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
Inactive
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if role.description.present? %>
|
||||
<p class="mt-1 text-sm text-gray-500"><%= role.description %></p>
|
||||
<% end %>
|
||||
|
||||
<!-- Assigned Users -->
|
||||
<div class="mt-3">
|
||||
<p class="text-xs text-gray-500 mb-2">Assigned Users:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<% role.users.each do |user| %>
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<%= user.email_address %>
|
||||
<span class="ml-1 text-blue-600">(<%= role.user_role_assignments.find_by(user: user)&.source %>)</span>
|
||||
<%= link_to "×", remove_role_admin_application_path(@application, user_id: user.id, role_id: role.id),
|
||||
method: :post,
|
||||
data: { confirm: "Remove role from #{user.email_address}?" },
|
||||
class: "ml-1 text-blue-600 hover:text-blue-800" %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="ml-4 flex-shrink-0">
|
||||
<div class="space-y-2">
|
||||
<!-- Assign Role to User -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<select id="assign-user-<%= role.id %>" class="text-xs rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Assign to user...</option>
|
||||
<% @available_users.each do |user| %>
|
||||
<% unless role.user_has_role?(user) %>
|
||||
<option value="<%= user.id %>"><%= user.email_address %></option>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</select>
|
||||
<%= link_to "Assign", assign_role_admin_application_path(@application, role_id: role.id, user_id: "REPLACE_USER_ID"),
|
||||
method: :post,
|
||||
class: "text-xs bg-blue-600 px-2 py-1 rounded text-white hover:bg-blue-500",
|
||||
onclick: "this.href = this.href.replace('REPLACE_USER_ID', document.getElementById('assign-user-<%= role.id %>').value); if (this.href.includes('undefined')) { alert('Please select a user'); return false; }" %>
|
||||
</div>
|
||||
|
||||
<!-- Edit Role -->
|
||||
<%= link_to "Edit", "#", class: "text-xs text-gray-600 hover:text-gray-800", onclick: "document.getElementById('edit-role-<%= role.id %>').classList.toggle('hidden'); return false;" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Role Form (Hidden by default) -->
|
||||
<div id="edit-role-<%= role.id %>" class="hidden mt-4 border-t pt-4">
|
||||
<%= form_with(model: [:admin, @application, role], url: update_role_admin_application_path(@application, role_id: role.id), local: true, method: :patch, class: "space-y-3") do |form| %>
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<%= form.label :display_name, "Display Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :display_name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
<div class="flex items-center pt-6">
|
||||
<%= form.check_box :active, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
||||
<%= form.label :active, "Active", class: "ml-2 block text-sm text-gray-900" %>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_area :description, rows: 2, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<%= form.submit "Update Role", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500" %>
|
||||
<%= link_to "Cancel", "#", class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50", onclick: "document.getElementById('edit-role-<%= role.id %>').classList.add('hidden'); return false;" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-12">
|
||||
<div class="text-gray-500 text-sm">
|
||||
No roles configured yet. Create your first role above to get started with role-based access control.
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
179
app/views/admin/applications/roles_broken.html.erb
Normal file
179
app/views/admin/applications/roles_broken.html.erb
Normal file
@@ -0,0 +1,179 @@
|
||||
<% content_for :title, "Role Management - #{@application.name}" %>
|
||||
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900">
|
||||
Role Management for <%= @application.name %>
|
||||
</h3>
|
||||
<%= link_to "← Back to Application", admin_application_path(@application), class: "text-sm text-blue-600 hover:text-blue-500" %>
|
||||
</div>
|
||||
|
||||
<% if @application.role_mapping_enabled? %>
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-md p-4 mb-6">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-blue-800">Role Mapping Configuration</h3>
|
||||
<div class="mt-2 text-sm text-blue-700">
|
||||
<p>Mode: <strong><%= @application.role_mapping_mode.humanize %></strong></p>
|
||||
<% if @application.role_claim_name.present? %>
|
||||
<p>Role Claim: <strong><%= @application.role_claim_name %></strong></p>
|
||||
<% end %>
|
||||
<% if @application.role_prefix.present? %>
|
||||
<p>Role Prefix: <strong><%= @application.role_prefix %></strong></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4 mb-6">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-yellow-800">Role Mapping Disabled</h3>
|
||||
<div class="mt-2 text-sm text-yellow-700">
|
||||
<p>Role mapping is currently disabled for this application. Enable it in the application settings to manage roles.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Create New Role -->
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h4 class="text-md font-medium text-gray-900 mb-4">Create New Role</h4>
|
||||
<%= form_with(model: [:admin, @application, ApplicationRole.new], url: create_role_admin_application_path(@application), local: true, class: "space-y-4") do |form| %>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<%= form.label :name, "Role Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "admin" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :display_name, "Display Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :display_name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Administrator" %>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_area :description, rows: 2, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Description of this role's permissions" %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<%= form.check_box :active, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
||||
<%= form.label :active, "Active", class: "ml-2 block text-sm text-gray-900" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.submit "Create Role", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Existing Roles -->
|
||||
<div class="space-y-6" data-controller="role-management">
|
||||
<h4 class="text-md font-medium text-gray-900">Existing Roles</h4>
|
||||
|
||||
<% if @application_roles.any? %>
|
||||
<div class="space-y-4">
|
||||
<% @application_roles.each do |role| %>
|
||||
<div class="border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-3">
|
||||
<h5 class="text-sm font-medium text-gray-900"><%= role.name %></h5>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
<%= role.display_name %>
|
||||
</span>
|
||||
<% unless role.active %>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
Inactive
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if role.description.present? %>
|
||||
<p class="mt-1 text-sm text-gray-500"><%= role.description %></p>
|
||||
<% end %>
|
||||
|
||||
<!-- Assigned Users -->
|
||||
<div class="mt-3">
|
||||
<p class="text-xs text-gray-500 mb-2">Assigned Users:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<% role.users.each do |user| %>
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<%= user.email_address %>
|
||||
<span class="ml-1 text-blue-600">(<%= role.user_role_assignments.find_by(user: user)&.source %>)</span>
|
||||
<%= link_to "×", remove_role_admin_application_path(@application, user_id: user.id, role_id: role.id),
|
||||
method: :post,
|
||||
data: { confirm: "Remove role from #{user.email_address}?" },
|
||||
class: "ml-1 text-blue-600 hover:text-blue-800" %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="ml-4 flex-shrink-0">
|
||||
<div class="space-y-2">
|
||||
<!-- Assign Role to User -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<select id="assign-user-<%= role.id %>" data-role-target="userSelect" data-role-id="<%= role.id %>" class="text-xs rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Assign to user...</option>
|
||||
<% @available_users.each do |user| %>
|
||||
<% unless role.user_has_role?(user) %>
|
||||
<option value="<%= user.id %>"><%= user.email_address %></option>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</select>
|
||||
<%= link_to "Assign", assign_role_admin_application_path(@application, role_id: role.id, user_id: "PLACEHOLDER"),
|
||||
method: :post,
|
||||
class: "text-xs bg-blue-600 px-2 py-1 rounded text-white hover:bg-blue-500",
|
||||
data: { role_target: "assignLink", action: "click->role-management#assignRole" } %>
|
||||
</div>
|
||||
|
||||
<!-- Edit Role -->
|
||||
<%= link_to "Edit", "#",
|
||||
class: "text-xs text-gray-600 hover:text-gray-800",
|
||||
data: { action: "click->role-management#toggleEdit" },
|
||||
data: { role_id: role.id } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Role Form (Hidden by default) -->
|
||||
<div id="edit-role-<%= role.id %>" class="hidden mt-4 border-t pt-4" data-role-target="editForm" data-role-id="<%= role.id %>">
|
||||
<%= form_with(model: [:admin, @application, role], url: update_role_admin_application_path(@application, role_id: role.id), local: true, method: :patch, class: "space-y-3") do |form| %>
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<%= form.label :display_name, "Display Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :display_name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
<div class="flex items-center pt-6">
|
||||
<%= form.check_box :active, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
||||
<%= form.label :active, "Active", class: "ml-2 block text-sm text-gray-900" %>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_area :description, rows: 2, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<%= form.submit "Update Role", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500" %>
|
||||
<%= link_to "Cancel", "#",
|
||||
class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50",
|
||||
data: { action: "click->role-management#hideEdit" },
|
||||
data: { role_id: role.id } %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-12">
|
||||
<div class="text-gray-500 text-sm">
|
||||
No roles configured yet. Create your first role above to get started with role-based access control.
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
173
app/views/admin/applications/roles_complex.html.erb
Normal file
173
app/views/admin/applications/roles_complex.html.erb
Normal file
@@ -0,0 +1,173 @@
|
||||
<% content_for :title, "Role Management - #{@application.name}" %>
|
||||
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900">
|
||||
Role Management for <%= @application.name %>
|
||||
</h3>
|
||||
<%= link_to "← Back to Application", admin_application_path(@application), class: "text-sm text-blue-600 hover:text-blue-500" %>
|
||||
</div>
|
||||
|
||||
<% if @application.role_mapping_enabled? %>
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-md p-4 mb-6">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-blue-800">Role Mapping Configuration</h3>
|
||||
<div class="mt-2 text-sm text-blue-700">
|
||||
<p>Mode: <strong><%= @application.role_mapping_mode.humanize %></strong></p>
|
||||
<% if @application.role_claim_name.present? %>
|
||||
<p>Role Claim: <strong><%= @application.role_claim_name %></strong></p>
|
||||
<% end %>
|
||||
<% if @application.role_prefix.present? %>
|
||||
<p>Role Prefix: <strong><%= @application.role_prefix %></strong></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4 mb-6">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-yellow-800">Role Mapping Disabled</h3>
|
||||
<div class="mt-2 text-sm text-yellow-700">
|
||||
<p>Role mapping is currently disabled for this application. Enable it in the application settings to manage roles.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Create New Role -->
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h4 class="text-md font-medium text-gray-900 mb-4">Create New Role</h4>
|
||||
<%= form_with(model: [:admin, @application, ApplicationRole.new], url: create_role_admin_application_path(@application), local: true, class: "space-y-4") do |form| %>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<%= form.label :name, "Role Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "admin" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :display_name, "Display Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :display_name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Administrator" %>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_area :description, rows: 2, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Description of this role's permissions" %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<%= form.check_box :active, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
||||
<%= form.label :active, "Active", class: "ml-2 block text-sm text-gray-900" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.submit "Create Role", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Existing Roles -->
|
||||
<div class="space-y-6">
|
||||
<h4 class="text-md font-medium text-gray-900">Existing Roles</h4>
|
||||
|
||||
<% if @application_roles.any? %>
|
||||
<div class="space-y-4">
|
||||
<% @application_roles.each do |role| %>
|
||||
<div class="border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-3">
|
||||
<h5 class="text-sm font-medium text-gray-900"><%= role.name %></h5>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
<%= role.display_name %>
|
||||
</span>
|
||||
<% unless role.active %>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
Inactive
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if role.description.present? %>
|
||||
<p class="mt-1 text-sm text-gray-500"><%= role.description %></p>
|
||||
<% end %>
|
||||
|
||||
<!-- Assigned Users -->
|
||||
<div class="mt-3">
|
||||
<p class="text-xs text-gray-500 mb-2">Assigned Users:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<% role.users.each do |user| %>
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<%= user.email_address %>
|
||||
<span class="ml-1 text-blue-600">(<%= role.user_role_assignments.find_by(user: user)&.source %>)</span>
|
||||
<%= link_to "×", remove_role_admin_application_path(@application, user_id: user.id, role_id: role.id),
|
||||
method: :post,
|
||||
data: { confirm: "Remove role from #{user.email_address}?" },
|
||||
class: "ml-1 text-blue-600 hover:text-blue-800" %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="ml-4 flex-shrink-0">
|
||||
<div class="space-y-2">
|
||||
<!-- Assign Role to User -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<select id="assign-user-<%= role.id %>" class="text-xs rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Assign to user...</option>
|
||||
<% @available_users.each do |user| %>
|
||||
<% unless role.user_has_role?(user) %>
|
||||
<option value="<%= user.id %>"><%= user.email_address %></option>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</select>
|
||||
<%= link_to "Assign", assign_role_admin_application_path(@application, role_id: role.id, user_id: "PLACEHOLDER"),
|
||||
method: :post,
|
||||
class: "text-xs bg-blue-600 px-2 py-1 rounded text-white hover:bg-blue-500",
|
||||
onclick: "var select = document.getElementById('assign-user-<%= role.id %>'); var userId = select.value; if (!userId) { alert('Please select a user'); return false; } this.href = this.href.replace('PLACEHOLDER', userId);" %>
|
||||
</div>
|
||||
|
||||
<!-- Edit Role -->
|
||||
<%= link_to "Edit", "#", class: "text-xs text-gray-600 hover:text-gray-800", onclick: "document.getElementById('edit-role-<%= role.id %>').classList.toggle('hidden'); return false;" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Role Form (Hidden by default) -->
|
||||
<div id="edit-role-<%= role.id %>" class="hidden mt-4 border-t pt-4">
|
||||
<%= form_with(model: [:admin, @application, role], url: update_role_admin_application_path(@application, role_id: role.id), local: true, method: :patch, class: "space-y-3") do |form| %>
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<%= form.label :display_name, "Display Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :display_name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
<div class="flex items-center pt-6">
|
||||
<%= form.check_box :active, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
||||
<%= form.label :active, "Active", class: "ml-2 block text-sm text-gray-900" %>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_area :description, rows: 2, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<%= form.submit "Update Role", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500" %>
|
||||
<%= link_to "Cancel", "#", class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50", onclick: "document.getElementById('edit-role-<%= role.id %>').classList.add('hidden'); return false;" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-12">
|
||||
<div class="text-gray-500 text-sm">
|
||||
No roles configured yet. Create your first role above to get started with role-based access control.
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +1,21 @@
|
||||
<div class="mb-6">
|
||||
<% if flash[:client_id] && flash[:client_secret] %>
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4 mb-6">
|
||||
<h4 class="text-sm font-medium text-yellow-800 mb-2">🔐 OIDC Client Credentials</h4>
|
||||
<p class="text-xs text-yellow-700 mb-3">Copy these credentials now. The client secret will not be shown again.</p>
|
||||
<div class="space-y-2">
|
||||
<div>
|
||||
<span class="text-xs font-medium text-yellow-700">Client ID:</span>
|
||||
</div>
|
||||
<code class="block bg-yellow-100 px-3 py-2 rounded font-mono text-xs break-all"><%= flash[:client_id] %></code>
|
||||
<div class="mt-3">
|
||||
<span class="text-xs font-medium text-yellow-700">Client Secret:</span>
|
||||
</div>
|
||||
<code class="block bg-yellow-100 px-3 py-2 rounded font-mono text-xs break-all"><%= flash[:client_secret] %></code>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900"><%= @application.name %></h1>
|
||||
@@ -6,6 +23,9 @@
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 flex gap-3">
|
||||
<%= link_to "Edit", edit_admin_application_path(@application), class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" %>
|
||||
<% if @application.oidc? %>
|
||||
<%= link_to "Manage Roles", roles_admin_application_path(@application), class: "rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500" %>
|
||||
<% end %>
|
||||
<%= button_to "Delete", admin_application_path(@application), method: :delete, data: { turbo_confirm: "Are you sure?" }, class: "rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500" %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,7 +84,12 @@
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Client Secret</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<code class="block bg-gray-100 px-3 py-2 rounded font-mono text-xs break-all"><%= @application.client_secret %></code>
|
||||
<div class="bg-gray-100 px-3 py-2 rounded text-xs text-gray-500 italic">
|
||||
🔒 Client secret is stored securely and cannot be displayed
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
To get a new client secret, use the "Regenerate Credentials" button above.
|
||||
</p>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user