feat: Implement customer invitation infrastructure in Authentik

- Added IInvitationSetupService and InvitationSetupService to orchestrate the setup of invitation infrastructure for customers.
- Introduced methods for creating groups, enrollment flows, invitation stages, roles, and policies in Authentik.
- Updated PostInstanceInitService to call the new invitation setup methods during post-initialization.
- Enhanced InstanceService to pass customer name during SAML configuration deployment.
- Updated App.axaml.cs to register the new IInvitationSetupService.
- Modified settings-custom.php.template to include documentation for SAML authentication configuration with group-based admin assignment.
- Added logic to exclude specific groups from being synced to Xibo during group synchronization.
This commit is contained in:
Matt Batchelder
2026-03-04 21:58:59 -05:00
parent 9493bdb9df
commit 150549a20d
8 changed files with 1305 additions and 8 deletions

View File

@@ -351,7 +351,8 @@ public class PostInstanceInitService
string abbrev,
string instanceUrl,
SettingsService settings,
CancellationToken ct)
string? customerName = null,
CancellationToken ct = default)
{
try
{
@@ -465,6 +466,11 @@ public class PostInstanceInitService
// Pre-create Authentik groups as Xibo user groups so they're available
// immediately (before any user logs in via SSO).
await SyncGroupsFromAuthentikAsync(abbrev, instanceUrl, settings, ct);
// ── 6. Set up customer invitation infrastructure in Authentik ─────
// Creates a group, enrollment flow, invitation stage, role, and
// scoping policies so the customer admin can invite users directly.
await SetupCustomerInvitationInfrastructureAsync(abbrev, customerName, ct);
}
catch (Exception ex)
{
@@ -474,9 +480,57 @@ public class PostInstanceInitService
}
}
/// <summary>
/// Sets up the customer invitation infrastructure in Authentik (group, enrollment flow,
/// stages, role, and policies). Errors are logged but do not block other operations.
/// </summary>
private async Task SetupCustomerInvitationInfrastructureAsync(
string abbrev, string? customerName, CancellationToken ct)
{
try
{
using var scope = _services.CreateScope();
var invitationSetup = scope.ServiceProvider.GetRequiredService<IInvitationSetupService>();
var displayName = !string.IsNullOrWhiteSpace(customerName)
? customerName
: abbrev.ToUpperInvariant();
_logger.LogInformation("[PostInit] Setting up invitation infrastructure for {Abbrev}", abbrev);
var result = await invitationSetup.SetupCustomerInvitationAsync(abbrev, displayName, ct);
if (result.Success)
{
_logger.LogInformation(
"[PostInit] Invitation infrastructure ready for {Abbrev}: group={Group}, flow={Flow}, role={Role}",
abbrev, result.GroupName, result.EnrollmentFlowSlug, result.RoleName);
if (!string.IsNullOrWhiteSpace(result.InvitationManagementUrl))
{
_logger.LogInformation(
"[PostInit] Customer admin invitation URL: {Url}", result.InvitationManagementUrl);
}
}
else
{
_logger.LogWarning("[PostInit] Invitation setup reported failure for {Abbrev}: {Message}",
abbrev, result.Message);
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"[PostInit] Invitation infrastructure setup failed for {Abbrev}: {Message}. " +
"Customer invitations can be configured manually in Authentik.", abbrev, ex.Message);
// Don't rethrow — invitation setup failure should not block post-init
}
}
/// <summary>
/// Fetches all groups from Authentik and creates matching user groups in the
/// specified Xibo instance. Groups that already exist in Xibo are skipped.
/// specified Xibo instance, excluding any groups listed in the
/// "SamlGroupSyncExcludedGroups" setting (comma-separated group names).
/// Groups that already exist in Xibo are skipped.
/// This ensures that groups are available in Xibo for permission assignment
/// before any user logs in via SAML SSO.
/// </summary>
@@ -505,6 +559,22 @@ public class PostInstanceInitService
_logger.LogInformation("[GroupSync] Found {Count} Authentik group(s) to sync", authentikGroups.Count);
// ── 1b. Read excluded groups from settings ────────────────────────
var excludedGroupsSetting = await settings.GetAsync("SamlGroupSyncExcludedGroups");
var excludedGroups = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(excludedGroupsSetting))
{
var excluded = excludedGroupsSetting
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(g => g.Trim())
.Where(g => !string.IsNullOrWhiteSpace(g));
foreach (var g in excluded)
{
excludedGroups.Add(g);
}
_logger.LogInformation("[GroupSync] Excluded groups: {Groups}", string.Join(", ", excludedGroups));
}
// ── 2. Authenticate to Xibo ───────────────────────────────────────
var oauthClientId = await settings.GetAsync(SettingsService.InstanceOAuthClientId(abbrev));
var oauthSecretId = await settings.GetAsync(SettingsService.InstanceOAuthSecretId(abbrev));
@@ -525,9 +595,16 @@ public class PostInstanceInitService
existingGroups.Select(g => g.Group),
StringComparer.OrdinalIgnoreCase);
// ── 4. Create missing groups in Xibo ──────────────────────────────
// ── 4. Create missing groups in Xibo (excluding specified ones) ────
foreach (var group in authentikGroups)
{
// Skip excluded groups
if (excludedGroups.Contains(group.Name))
{
_logger.LogInformation("[GroupSync] Skipping excluded group '{Name}'", group.Name);
continue;
}
if (existingNames.Contains(group.Name))
{
_logger.LogDebug("[GroupSync] Group '{Name}' already exists in Xibo", group.Name);