Refactor SAML configuration deployment and enhance Authentik integration

- Removed SAML configuration deployment calls from PostInstanceInitService.
- Updated DeploySamlConfigurationAsync to improve template fetching logic from Git and local directories.
- Added Authentik flow and keypair models for better representation in the UI.
- Enhanced SettingsViewModel to include Authentik settings with save and test functionality.
- Updated UI to support Authentik configuration, including dropdowns for flows and keypairs.
- Changed default CMS server name template to "app.ots-signs.com" across various files.
- Improved password handling in SshDockerCliService for secure shell command execution.
- Added new template file for settings-custom.php in the project structure.
This commit is contained in:
Matt Batchelder
2026-02-27 22:15:24 -05:00
parent 2aaa0442b2
commit 56d48b6062
22 changed files with 1245 additions and 172 deletions

View File

@@ -107,9 +107,6 @@ public class PostInstanceInitService
_logger.LogInformation("[PostInit] Setting Xibo theme to 'otssigns'");
await xibo.SetThemeAsync(instanceUrl, accessToken, "otssigns");
// ── 6a. Deploy SAML configuration ─────────────────────────────────
await DeploySamlConfigurationAsync(abbrev, instanceUrl, settings, ct);
// ── 7. Store credentials in Bitwarden ─────────────────────────────
_logger.LogInformation("[PostInit] Storing credentials in Bitwarden");
@@ -200,9 +197,6 @@ public class PostInstanceInitService
_logger.LogInformation("[PostInit] Setting theme to 'otssigns'");
await xibo.SetThemeAsync(instanceUrl, accessToken, "otssigns");
// ── 5a. Deploy SAML configuration ─────────────────────────────────
await DeploySamlConfigurationAsync(abbrev, instanceUrl, settings, ct);
// ── 6. Store admin password in Bitwarden ──────────────────────────
_logger.LogInformation("[PostInit] Storing credentials in Bitwarden");
var adminSecretId = await bws.CreateInstanceSecretAsync(
@@ -349,9 +343,11 @@ public class PostInstanceInitService
/// <summary>
/// Provisions a SAML application in Authentik, renders the settings-custom.php template,
/// and writes the rendered file to the instance's NFS-backed cms-custom volume.
/// Errors are logged but do not fail the overall post-init process.
/// The template is resolved from (a) the git repo cache, or (b) the local bundled
/// <c>templates/</c> directory shipped with the application.
/// Errors are logged but do not fail the overall deployment.
/// </summary>
private async Task DeploySamlConfigurationAsync(
public async Task DeploySamlConfigurationAsync(
string abbrev,
string instanceUrl,
SettingsService settings,
@@ -366,36 +362,80 @@ public class PostInstanceInitService
var git = scope.ServiceProvider.GetRequiredService<GitTemplateService>();
var docker = scope.ServiceProvider.GetRequiredService<IDockerCliService>();
// ── 1. Fetch template from git repo ───────────────────────────────
// ── 1. Locate settings-custom.php.template ────────────────────────
string? templateContent = null;
// 1a. Try git repo cache first
var repoUrl = await settings.GetAsync(SettingsService.GitRepoUrl);
var repoPat = await settings.GetAsync(SettingsService.GitRepoPat);
if (string.IsNullOrWhiteSpace(repoUrl))
throw new InvalidOperationException("Git repository URL is not configured.");
if (!string.IsNullOrWhiteSpace(repoUrl))
{
try
{
var templateConfig = await git.FetchAsync(repoUrl, repoPat);
var gitPath = Path.Combine(templateConfig.CacheDir, "settings-custom.php.template");
if (File.Exists(gitPath))
{
templateContent = await File.ReadAllTextAsync(gitPath, ct);
_logger.LogInformation("[PostInit] Using template from git repo cache: {Path}", gitPath);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[PostInit] Could not fetch template from git — trying local fallback");
}
}
var templateConfig = await git.FetchAsync(repoUrl, repoPat);
var templatePath = Path.Combine(templateConfig.CacheDir, "settings-custom.php.template");
// 1b. Fall back to local templates/ directory (bundled with app)
if (templateContent == null)
{
var candidates = new[]
{
Path.Combine(AppContext.BaseDirectory, "templates", "settings-custom.php.template"),
Path.Combine(Directory.GetCurrentDirectory(), "templates", "settings-custom.php.template"),
};
if (!File.Exists(templatePath))
foreach (var candidate in candidates)
{
if (File.Exists(candidate))
{
templateContent = await File.ReadAllTextAsync(candidate, ct);
_logger.LogInformation("[PostInit] Using local template: {Path}", candidate);
break;
}
}
}
if (templateContent == null)
{
_logger.LogWarning(
"[PostInit] settings-custom.php.template not found in git repo — skipping SAML deployment");
"[PostInit] settings-custom.php.template not found in git repo or local templates/ — skipping SAML deployment");
return;
}
var templateContent = await File.ReadAllTextAsync(templatePath, ct);
// ── 2. Provision Authentik SAML application ───────────────────────
var samlBaseUrl = instanceUrl.TrimEnd('/') + "/saml";
var samlConfig = await authentik.ProvisionSamlAsync(abbrev, instanceUrl, ct);
Models.DTOs.AuthentikSamlConfig? samlConfig = null;
try
{
samlConfig = await authentik.ProvisionSamlAsync(abbrev, instanceUrl, ct);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"[PostInit] Authentik provisioning failed for {Abbrev} — skipping SAML config deployment to avoid broken Xibo instance",
abbrev);
return; // Do NOT write a settings-custom.php with empty IdP values — it will crash Xibo
}
// ── 3. Render template ────────────────────────────────────────────
var rendered = templateContent
.Replace("{{SAML_BASE_URL}}", samlBaseUrl)
.Replace("{{SAML_SP_ENTITY_ID}}", $"{samlBaseUrl}/metadata")
.Replace("{{AUTHENTIK_IDP_ENTITY_ID}}", samlConfig.IdpEntityId)
.Replace("{{AUTHENTIK_SSO_URL}}", samlConfig.SsoUrlRedirect)
.Replace("{{AUTHENTIK_SLO_URL}}", samlConfig.SloUrlRedirect)
.Replace("{{AUTHENTIK_IDP_X509_CERT}}", samlConfig.IdpX509Cert);
.Replace("{{AUTHENTIK_IDP_ENTITY_ID}}", samlConfig?.IdpEntityId ?? "")
.Replace("{{AUTHENTIK_SSO_URL}}", samlConfig?.SsoUrlRedirect ?? "")
.Replace("{{AUTHENTIK_SLO_URL}}", samlConfig?.SloUrlRedirect ?? "")
.Replace("{{AUTHENTIK_IDP_X509_CERT}}", samlConfig?.IdpX509Cert ?? "");
// ── 4. Write rendered file to NFS volume ──────────────────────────
var nfsServer = await settings.GetAsync(SettingsService.NfsServer);
@@ -415,8 +455,11 @@ public class PostInstanceInitService
throw new InvalidOperationException($"Failed to write settings-custom.php to NFS: {error}");
_logger.LogInformation(
"[PostInit] SAML configuration deployed for {Abbrev} (Authentik provider={ProviderId})",
abbrev, samlConfig.ProviderId);
"[PostInit] SAML configuration deployed for {Abbrev}{ProviderInfo}",
abbrev,
samlConfig != null
? $" (Authentik provider={samlConfig.ProviderId})"
: " (without Authentik — needs manual IdP config)");
}
catch (Exception ex)
{