feat: Implement provisioning pipelines for subscription management

- Add ReactivatePipeline to handle subscription reactivation, including scaling Docker services, health verification, status updates, audit logging, and broadcasting status changes.
- Introduce RotateCredentialsPipeline for OAuth2 credential rotation, managing the deletion of old apps, creation of new ones, credential storage, access verification, and audit logging.
- Create StepRunner to manage job step execution, including lifecycle management and progress broadcasting via SignalR.
- Implement SuspendPipeline for subscription suspension, scaling down services, updating statuses, logging audits, and broadcasting changes.
- Add UpdateScreenLimitPipeline to update Xibo CMS screen limits and record snapshots.
- Introduce XiboFeatureManifests for hardcoded feature ACLs per role.
- Add docker-compose.dev.yml for local development with PostgreSQL setup.
This commit is contained in:
Matt Batchelder
2026-03-18 10:27:26 -04:00
parent c2e03de8bb
commit c6d46098dd
77 changed files with 9412 additions and 29 deletions

View File

@@ -0,0 +1,146 @@
using Refit;
namespace OTSSignsOrchestrator.Server.Clients;
// ── Configuration ───────────────────────────────────────────────────────────
public sealed class AuthentikOptions
{
public const string Section = "Authentik";
public string BaseUrl { get; set; } = string.Empty;
public string ApiToken { get; set; } = string.Empty;
/// <summary>UUID of the OTS signing certificate-key pair used for all SAML sources.</summary>
public string OtsSigningKpId { get; set; } = string.Empty;
/// <summary>Authentik pre-authentication flow slug for SAML sources (e.g. "default-source-pre-authentication").</summary>
public string SourcePreAuthFlowSlug { get; set; } = "default-source-pre-authentication";
/// <summary>Authentik authentication flow slug for SAML sources (e.g. "default-source-authentication").</summary>
public string SourceAuthFlowSlug { get; set; } = "default-source-authentication";
}
// ── Request DTOs ────────────────────────────────────────────────────────────
public record CreateSamlProviderRequest(
string Name,
string AuthorizationFlow,
string AcsUrl,
string Issuer,
string SpBinding,
string Audience,
string? SigningKp);
public record CreateAuthentikApplicationRequest(
string Name,
string Slug,
string Provider,
string? MetaLaunchUrl);
public record CreateAuthentikGroupRequest(
string Name,
bool? IsSuperuser,
string? Parent);
public record CreateFlowRequest(
string Name,
bool? SingleUse,
DateTimeOffset? Expires);
public record CreateAuthentikUserRequest(
string Username,
string Name,
string Email,
string[] Groups);
public record ImportCertRequest(
string Name,
string CertificateData,
string? KeyData);
public record CreateSamlSourceRequest(
string Name,
string Slug,
string SsoUrl,
string? SloUrl,
string Issuer,
string? SigningKp,
string? VerificationKp,
string BindingType,
string NameIdPolicy,
string PreAuthenticationFlow,
string AuthenticationFlow,
bool AllowIdpInitiated);
// ── Response DTOs ───────────────────────────────────────────────────────────
/// <summary>Authentik paginated list response. Results contain dictionaries with entity fields.</summary>
public record AuthentikPagedResult(
List<Dictionary<string, object>> Results);
// ── Authentik Refit Interface ───────────────────────────────────────────────
// One global Authentik instance serves all tenants.
[Headers("Authorization: Bearer")]
public interface IAuthentikClient
{
// ── SAML Providers ──────────────────────────────────────────────────────
[Post("/api/v3/providers/saml/")]
Task<ApiResponse<Dictionary<string, object>>> CreateSamlProviderAsync(
[Body] CreateSamlProviderRequest body);
[Get("/api/v3/providers/saml/{id}/")]
Task<ApiResponse<Dictionary<string, object>>> GetSamlProviderAsync(int id);
[Delete("/api/v3/providers/saml/{id}/")]
Task DeleteSamlProviderAsync(int id);
// ── Applications ────────────────────────────────────────────────────────
[Post("/api/v3/core/applications/")]
Task<ApiResponse<Dictionary<string, object>>> CreateApplicationAsync(
[Body] CreateAuthentikApplicationRequest body);
[Delete("/api/v3/core/applications/{slug}/")]
Task DeleteApplicationAsync(string slug);
// ── Groups ──────────────────────────────────────────────────────────────
[Get("/api/v3/core/groups/")]
Task<ApiResponse<AuthentikPagedResult>> ListGroupsAsync([AliasAs("search")] string? search = null);
[Post("/api/v3/core/groups/")]
Task<ApiResponse<Dictionary<string, object>>> CreateGroupAsync(
[Body] CreateAuthentikGroupRequest body);
[Delete("/api/v3/core/groups/{id}/")]
Task DeleteGroupAsync(string id);
// ── Invitations ─────────────────────────────────────────────────────────
[Post("/api/v3/stages/invitation/invitations/")]
Task<ApiResponse<Dictionary<string, object>>> CreateInvitationAsync(
[Body] CreateFlowRequest body);
// ── Users ───────────────────────────────────────────────────────────────
[Post("/api/v3/core/users/")]
Task<ApiResponse<Dictionary<string, object>>> CreateUserAsync(
[Body] CreateAuthentikUserRequest body);
// ── Health ──────────────────────────────────────────────────────────────
[Get("/api/v3/-/health/ready/")]
Task<ApiResponse<object>> CheckHealthAsync();
// ── Certificates ────────────────────────────────────────────────────────
[Get("/api/v3/crypto/certificatekeypairs/{kpId}/")]
Task<ApiResponse<Dictionary<string, object>>> GetCertificateKeyPairAsync(string kpId);
[Post("/api/v3/crypto/certificatekeypairs/")]
Task<ApiResponse<Dictionary<string, object>>> ImportCertificateAsync(
[Body] ImportCertRequest body);
// ── SAML Sources ────────────────────────────────────────────────────────
[Post("/api/v3/sources/saml/")]
Task<ApiResponse<Dictionary<string, object>>> CreateSamlSourceAsync(
[Body] CreateSamlSourceRequest body);
[Get("/api/v3/sources/saml/{slug}/metadata/")]
Task<ApiResponse<string>> GetSamlSourceMetadataAsync(string slug);
[Delete("/api/v3/sources/saml/{slug}/")]
Task DeleteSamlSourceAsync(string slug);
}