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

@@ -36,7 +36,7 @@
<div>
<%= form.label :app_type, "Application Type", class: "block text-sm font-medium text-gray-700" %>
<%= form.select :app_type, [["OpenID Connect (OIDC)", "oidc"], ["SAML (Coming Soon)", "saml", { disabled: 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", disabled: application.persisted? %>
<%= form.select :app_type, [["OpenID Connect (OIDC)", "oidc"], ["Forward Auth (Reverse Proxy)", "forward_auth"]], {}, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", disabled: application.persisted? %>
<% if application.persisted? %>
<p class="mt-1 text-sm text-gray-500">Application type cannot be changed after creation.</p>
<% end %>
@@ -51,51 +51,22 @@
<%= 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>
</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>
<!-- Forward Auth-specific fields -->
<div id="forward-auth-fields" class="space-y-6 border-t border-gray-200 pt-6" style="<%= 'display: none;' unless application.forward_auth? %>">
<h3 class="text-base font-semibold text-gray-900">Forward Auth Configuration</h3>
<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>
<%= form.label :domain_pattern, "Domain Pattern", class: "block text-sm font-medium text-gray-700" %>
<%= form.text_field :domain_pattern, 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: "*.example.com or app.example.com" %>
<p class="mt-1 text-sm text-gray-500">Domain pattern to match. Use * for wildcard subdomains (e.g., *.example.com matches app.example.com, api.example.com, etc.)</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>
<%= form.label :headers_config, "Custom Headers Configuration (JSON)", class: "block text-sm font-medium text-gray-700" %>
<%= form.text_area :headers_config, rows: 8, 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: '{"user": "X-Remote-User", "email": "X-Remote-Email"}' %>
<p class="mt-1 text-sm text-gray-500">Optional: Override default headers. Leave empty to use defaults: X-Remote-User, X-Remote-Email, X-Remote-Name, X-Remote-Groups, X-Remote-Admin</p>
</div>
</div>
@@ -129,33 +100,29 @@
<% end %>
<script>
// Show/hide OIDC fields based on app type selection
// Show/hide type-specific 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');
const forwardAuthFields = document.querySelector('#forward-auth-fields');
function updateFieldVisibility() {
const isOidc = appTypeSelect.value === 'oidc';
const roleMappingEnabled = roleMappingMode && ['oidc_managed', 'hybrid'].includes(roleMappingMode.value);
if (!appTypeSelect) return;
const appType = appTypeSelect.value;
if (oidcFields) {
oidcFields.style.display = isOidc ? 'block' : 'none';
oidcFields.style.display = appType === 'oidc' ? 'block' : 'none';
}
if (roleMappingAdvanced) {
roleMappingAdvanced.style.display = isOidc && roleMappingEnabled ? 'block' : 'none';
if (forwardAuthFields) {
forwardAuthFields.style.display = appType === 'forward_auth' ? 'block' : 'none';
}
}
if (appTypeSelect && oidcFields) {
if (appTypeSelect) {
appTypeSelect.addEventListener('change', updateFieldVisibility);
}
if (roleMappingMode) {
roleMappingMode.addEventListener('change', updateFieldVisibility);
}
// Initialize visibility on page load
updateFieldVisibility();
</script>

View File

@@ -1,125 +0,0 @@
<% 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>

View File

@@ -1,173 +0,0 @@
<% 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>

View File

@@ -1,179 +0,0 @@
<% 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>

View File

@@ -1,173 +0,0 @@
<% 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>

View File

@@ -23,9 +23,6 @@
</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>
@@ -47,8 +44,8 @@
<% case @application.app_type %>
<% when "oidc" %>
<span class="inline-flex items-center rounded-full bg-purple-100 px-2 py-1 text-xs font-medium text-purple-700">OIDC</span>
<% when "saml" %>
<span class="inline-flex items-center rounded-full bg-orange-100 px-2 py-1 text-xs font-medium text-orange-700">SAML</span>
<% when "forward_auth" %>
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700">Forward Auth</span>
<% end %>
</dd>
</div>
@@ -109,6 +106,35 @@
</div>
<% end %>
<!-- Forward Auth Configuration (only for Forward Auth apps) -->
<% if @application.forward_auth? %>
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4">Forward Auth Configuration</h3>
<dl class="space-y-4">
<div>
<dt class="text-sm font-medium text-gray-500">Domain Pattern</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"><%= @application.domain_pattern %></code>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Headers Configuration</dt>
<dd class="mt-1 text-sm text-gray-900">
<% if @application.headers_config.present? && @application.headers_config.any? %>
<code class="block bg-gray-100 px-3 py-2 rounded font-mono text-xs whitespace-pre-wrap"><%= JSON.pretty_generate(@application.headers_config) %></code>
<% else %>
<div class="bg-gray-100 px-3 py-2 rounded text-xs text-gray-500">
Using default headers: X-Remote-User, X-Remote-Email, X-Remote-Name, X-Remote-Groups, X-Remote-Admin
</div>
<% end %>
</dd>
</div>
</dl>
</div>
</div>
<% end %>
<!-- Group Access Control -->
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">

View File

@@ -1,126 +0,0 @@
<% content_for :title, "Edit Forward Auth Rule" %>
<div class="md:flex md:items-center md:justify-between">
<div class="min-w-0 flex-1">
<h2 class="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
Edit Forward Auth Rule
</h2>
</div>
</div>
<div class="mt-8">
<%= form_with(model: [:admin, @forward_auth_rule], local: true, class: "space-y-6") do |form| %>
<%= render "shared/form_errors", form: form %>
<div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2">
<div class="px-4 py-6 sm:p-8">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-4">
<%= form.label :domain_pattern, class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= form.text_field :domain_pattern, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6", placeholder: "*.example.com" %>
</div>
<p class="mt-3 text-sm leading-6 text-gray-600">
Use patterns like "*.example.com" or "api.example.com". Wildcards (*) are supported.
</p>
</div>
<div class="sm:col-span-4">
<%= form.label :active, class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= form.select :active, options_for_select([["Active", true], ["Inactive", false]], @forward_auth_rule.active), { prompt: "Select status" }, { class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:max-w-xs sm:text-sm sm:leading-6" } %>
</div>
</div>
<div class="col-span-full">
<div class="block text-sm font-medium leading-6 text-gray-900 mb-4">
Groups
</div>
<div class="mt-2 space-y-2">
<%= form.collection_select :group_ids, @available_groups, :id, :name,
{ selected: @forward_auth_rule.allowed_groups.map(&:id), prompt: "Select groups (leave empty for bypass)" },
{ multiple: true, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6" } %>
</div>
<p class="mt-3 text-sm leading-6 text-gray-600">
Select groups that are allowed to access this domain. If no groups are selected, all authenticated users will be allowed access (bypass).
</p>
</div>
<div class="col-span-full">
<div class="block text-sm font-medium leading-6 text-gray-900 mb-4">
HTTP Headers Configuration
</div>
<div class="mt-2 space-y-4">
<div class="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-x-4">
<div>
<%= label_tag "headers_config[user]", "User Header", class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= text_field_tag "headers_config[user]", @forward_auth_rule.headers_config&.dig(:user) || ForwardAuthRule::DEFAULT_HEADERS[:user],
class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6",
placeholder: "Remote-User" %>
</div>
<p class="mt-1 text-xs text-gray-500">Header name for user identity</p>
</div>
<div>
<%= label_tag "headers_config[email]", "Email Header", class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= text_field_tag "headers_config[email]", @forward_auth_rule.headers_config&.dig(:email) || ForwardAuthRule::DEFAULT_HEADERS[:email],
class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6",
placeholder: "Remote-Email" %>
</div>
<p class="mt-1 text-xs text-gray-500">Header name for user email</p>
</div>
<div>
<%= label_tag "headers_config[name]", "Name Header", class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= text_field_tag "headers_config[name]", @forward_auth_rule.headers_config&.dig(:name) || ForwardAuthRule::DEFAULT_HEADERS[:name],
class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6",
placeholder: "Remote-Name" %>
</div>
<p class="mt-1 text-xs text-gray-500">Header name for user display name</p>
</div>
<div>
<%= label_tag "headers_config[groups]", "Groups Header", class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= text_field_tag "headers_config[groups]", @forward_auth_rule.headers_config&.dig(:groups) || ForwardAuthRule::DEFAULT_HEADERS[:groups],
class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6",
placeholder: "Remote-Groups" %>
</div>
<p class="mt-1 text-xs text-gray-500">Header name for user groups (comma-separated)</p>
</div>
<div>
<%= label_tag "headers_config[admin]", "Admin Header", class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= text_field_tag "headers_config[admin]", @forward_auth_rule.headers_config&.dig(:admin) || ForwardAuthRule::DEFAULT_HEADERS[:admin],
class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6",
placeholder: "Remote-Admin" %>
</div>
<p class="mt-1 text-xs text-gray-500">Header name for admin status (true/false)</p>
</div>
</div>
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
<h4 class="text-sm font-medium text-blue-900 mb-2">Header Configuration Options:</h4>
<ul class="text-sm text-blue-700 space-y-1">
<li>• <strong>Default headers:</strong> Use standard headers like Remote-User, Remote-Email</li>
<li>• <strong>X- prefixed:</strong> Use X-Remote-User, X-Remote-Email, etc.</li>
<li>• <strong>Custom:</strong> Use application-specific headers</li>
<li>• <strong>No headers:</strong> Leave fields empty for access-only (like Metube)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-6 flex items-center justify-end gap-x-6">
<%= link_to "Cancel", admin_forward_auth_rule_path(@forward_auth_rule), class: "text-sm font-semibold leading-6 text-gray-900 hover:text-gray-700" %>
<%= form.submit "Update Rule", 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>

View File

@@ -1,68 +0,0 @@
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-2xl font-semibold text-gray-900">Forward Auth Rules</h1>
<p class="mt-2 text-sm text-gray-700">Manage forward authentication rules for domain-based access control.</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<%= link_to "New Rule", new_admin_forward_auth_rule_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" %>
</div>
</div>
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<table class="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">Domain Pattern</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Headers</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Groups</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Status</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<% @forward_auth_rules.each do |rule| %>
<tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
<%= link_to rule.domain_pattern, admin_forward_auth_rule_path(rule), class: "text-blue-600 hover:text-blue-900" %>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<% if rule.headers_config.blank? %>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">Default</span>
<% elsif rule.headers_config.values.all?(&:blank?) %>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">None</span>
<% else %>
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700">Custom</span>
<% end %>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<% if rule.allowed_groups.empty? %>
<span class="text-gray-400">All users</span>
<% else %>
<%= rule.allowed_groups.count %> groups
<% end %>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<% if rule.active? %>
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700">Active</span>
<% else %>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">Inactive</span>
<% end %>
</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
<div class="flex justify-end space-x-3">
<%= link_to "View", admin_forward_auth_rule_path(rule), class: "text-blue-600 hover:text-blue-900 whitespace-nowrap" %>
<%= link_to "Edit", edit_admin_forward_auth_rule_path(rule), class: "text-blue-600 hover:text-blue-900 whitespace-nowrap" %>
<%= button_to "Delete", admin_forward_auth_rule_path(rule), method: :delete, data: { turbo_confirm: "Are you sure you want to delete this forward auth rule?" }, class: "text-red-600 hover:text-red-900 whitespace-nowrap" %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -1,126 +0,0 @@
<% content_for :title, "New Forward Auth Rule" %>
<div class="md:flex md:items-center md:justify-between">
<div class="min-w-0 flex-1">
<h2 class="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
New Forward Auth Rule
</h2>
</div>
</div>
<div class="mt-8">
<%= form_with(model: [:admin, @forward_auth_rule], local: true, class: "space-y-6") do |form| %>
<%= render "shared/form_errors", form: form %>
<div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2">
<div class="px-4 py-6 sm:p-8">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-4">
<%= form.label :domain_pattern, class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= form.text_field :domain_pattern, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6", placeholder: "*.example.com" %>
</div>
<p class="mt-3 text-sm leading-6 text-gray-600">
Use patterns like "*.example.com" or "api.example.com". Wildcards (*) are supported.
</p>
</div>
<div class="sm:col-span-4">
<%= form.label :active, class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= form.select :active, options_for_select([["Active", true], ["Inactive", false]], @forward_auth_rule.active), { prompt: "Select status" }, { class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:max-w-xs sm:text-sm sm:leading-6" } %>
</div>
</div>
<div class="col-span-full">
<div class="block text-sm font-medium leading-6 text-gray-900 mb-4">
Groups
</div>
<div class="mt-2 space-y-2">
<%= form.collection_select :group_ids, @available_groups, :id, :name,
{ prompt: "Select groups (leave empty for bypass)" },
{ multiple: true, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6" } %>
</div>
<p class="mt-3 text-sm leading-6 text-gray-600">
Select groups that are allowed to access this domain. If no groups are selected, all authenticated users will be allowed access (bypass).
</p>
</div>
<div class="col-span-full">
<div class="block text-sm font-medium leading-6 text-gray-900 mb-4">
HTTP Headers Configuration
</div>
<div class="mt-2 space-y-4">
<div class="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-x-4">
<div>
<%= label_tag "headers_config[user]", "User Header", class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= text_field_tag "headers_config[user]", @forward_auth_rule.headers_config&.dig(:user) || ForwardAuthRule::DEFAULT_HEADERS[:user],
class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6",
placeholder: "Remote-User" %>
</div>
<p class="mt-1 text-xs text-gray-500">Header name for user identity</p>
</div>
<div>
<%= label_tag "headers_config[email]", "Email Header", class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= text_field_tag "headers_config[email]", @forward_auth_rule.headers_config&.dig(:email) || ForwardAuthRule::DEFAULT_HEADERS[:email],
class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6",
placeholder: "Remote-Email" %>
</div>
<p class="mt-1 text-xs text-gray-500">Header name for user email</p>
</div>
<div>
<%= label_tag "headers_config[name]", "Name Header", class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= text_field_tag "headers_config[name]", @forward_auth_rule.headers_config&.dig(:name) || ForwardAuthRule::DEFAULT_HEADERS[:name],
class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6",
placeholder: "Remote-Name" %>
</div>
<p class="mt-1 text-xs text-gray-500">Header name for user display name</p>
</div>
<div>
<%= label_tag "headers_config[groups]", "Groups Header", class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= text_field_tag "headers_config[groups]", @forward_auth_rule.headers_config&.dig(:groups) || ForwardAuthRule::DEFAULT_HEADERS[:groups],
class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6",
placeholder: "Remote-Groups" %>
</div>
<p class="mt-1 text-xs text-gray-500">Header name for user groups (comma-separated)</p>
</div>
<div>
<%= label_tag "headers_config[admin]", "Admin Header", class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= text_field_tag "headers_config[admin]", @forward_auth_rule.headers_config&.dig(:admin) || ForwardAuthRule::DEFAULT_HEADERS[:admin],
class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6",
placeholder: "Remote-Admin" %>
</div>
<p class="mt-1 text-xs text-gray-500">Header name for admin status (true/false)</p>
</div>
</div>
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
<h4 class="text-sm font-medium text-blue-900 mb-2">Header Configuration Options:</h4>
<ul class="text-sm text-blue-700 space-y-1">
<li>• <strong>Default headers:</strong> Use standard headers like Remote-User, Remote-Email</li>
<li>• <strong>X- prefixed:</strong> Use X-Remote-User, X-Remote-Email, etc.</li>
<li>• <strong>Custom:</strong> Use application-specific headers</li>
<li>• <strong>No headers:</strong> Leave fields empty for access-only (like Metube)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-6 flex items-center justify-end gap-x-6">
<%= link_to "Cancel", admin_forward_auth_rules_path, class: "text-sm font-semibold leading-6 text-gray-900 hover:text-gray-700" %>
<%= form.submit "Create Rule", 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>

View File

@@ -1,116 +0,0 @@
<div class="mb-6">
<div class="sm:flex sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold text-gray-900"><%= @forward_auth_rule.domain_pattern %></h1>
<p class="mt-1 text-sm text-gray-500">Forward authentication rule for domain-based access control</p>
</div>
<div class="mt-4 sm:mt-0 flex gap-3">
<%= link_to "Edit", edit_admin_forward_auth_rule_path(@forward_auth_rule), 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" %>
<%= button_to "Delete", admin_forward_auth_rule_path(@forward_auth_rule), 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>
</div>
<div class="space-y-6">
<!-- Basic Information -->
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4">Basic Information</h3>
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-gray-500">Domain Pattern</dt>
<dd class="mt-1 text-sm text-gray-900"><code class="bg-gray-100 px-2 py-1 rounded"><%= @forward_auth_rule.domain_pattern %></code></dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Status</dt>
<dd class="mt-1 text-sm text-gray-900">
<% if @forward_auth_rule.active? %>
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700">Active</span>
<% else %>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">Inactive</span>
<% end %>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Headers Configuration</dt>
<dd class="mt-1 text-sm text-gray-900">
<% if @forward_auth_rule.headers_config.blank? %>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">Default</span>
<% elsif @forward_auth_rule.headers_config.values.all?(&:blank?) %>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">None</span>
<% else %>
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700">Custom</span>
<% end %>
</dd>
</div>
</dl>
</div>
</div>
<!-- Header Configuration -->
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4">Header Configuration</h3>
<div class="space-y-4">
<% effective_headers = @forward_auth_rule.effective_headers %>
<% if effective_headers.empty? %>
<div class="rounded-md bg-gray-50 p-4">
<div class="flex">
<div class="ml-3">
<p class="text-sm text-gray-700">
No headers configured - access control only.
</p>
</div>
</div>
</div>
<% else %>
<dl class="space-y-4">
<% effective_headers.each do |key, header_name| %>
<div>
<dt class="text-sm font-medium text-gray-500"><%= key.to_s.capitalize %></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"><%= header_name %></code>
</dd>
</div>
<% end %>
</dl>
<% end %>
</div>
</div>
</div>
<!-- Group Access Control -->
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4">Access Control</h3>
<div>
<dt class="text-sm font-medium text-gray-500 mb-2">Allowed Groups</dt>
<dd class="mt-1 text-sm text-gray-900">
<% if @allowed_groups.empty? %>
<div class="rounded-md bg-blue-50 p-4">
<div class="flex">
<div class="ml-3">
<p class="text-sm text-blue-700">
No groups assigned - all active users can access this domain.
</p>
</div>
</div>
</div>
<% else %>
<ul class="divide-y divide-gray-200 border border-gray-200 rounded-md">
<% @allowed_groups.each do |group| %>
<li class="px-4 py-3 flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-900"><%= group.name %></p>
<p class="text-xs text-gray-500"><%= pluralize(group.users.count, "member") %></p>
</div>
</li>
<% end %>
</ul>
<% end %>
</dd>
</div>
</div>
</div>
</div>

View File

@@ -49,6 +49,12 @@
<p class="mt-1 text-sm text-gray-500">Select which users should be members of this group.</p>
</div>
<div>
<%= form.label :custom_claims, "Custom Claims (JSON)", class: "block text-sm font-medium text-gray-700" %>
<%= form.text_area :custom_claims, value: (group.custom_claims.present? ? JSON.pretty_generate(group.custom_claims) : ""), rows: 8, 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: '{"roles": ["admin", "editor"]}' %>
<p class="mt-1 text-sm text-gray-500">Optional: Custom claims to add to OIDC tokens for all members. These will be merged with user-level claims.</p>
</div>
<div class="flex gap-3">
<%= form.submit group.persisted? ? "Update Group" : "Create Group", 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" %>
<%= link_to "Cancel", admin_groups_path, 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" %>

View File

@@ -46,6 +46,12 @@
<% end %>
</div>
<div>
<%= form.label :custom_claims, "Custom Claims (JSON)", class: "block text-sm font-medium text-gray-700" %>
<%= form.text_area :custom_claims, value: (user.custom_claims.present? ? JSON.pretty_generate(user.custom_claims) : ""), rows: 8, 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: '{"department": "engineering", "level": "senior"}' %>
<p class="mt-1 text-sm text-gray-500">Optional: User-specific custom claims to add to OIDC tokens. These override group-level claims.</p>
</div>
<div class="flex gap-3">
<%= form.submit user.persisted? ? "Update User" : "Create User", 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" %>
<%= link_to "Cancel", admin_users_path, 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" %>

View File

@@ -57,16 +57,6 @@
<% end %>
</li>
<!-- Admin: Forward Auth Rules -->
<li>
<%= link_to admin_forward_auth_rules_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/forward_auth_rules') ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }" do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
Forward Auth Rules
<% end %>
</li>
<!-- Admin: Groups -->
<li>
<%= link_to admin_groups_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/groups') ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }" do %>
@@ -170,14 +160,6 @@
Groups
<% end %>
</li>
<li>
<%= link_to admin_forward_auth_rules_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:text-blue-600 hover:bg-gray-50" do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
Forward Auth Rules
<% end %>
</li>
<% end %>
<li>
<%= link_to profile_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:text-blue-600 hover:bg-gray-50" do %>