OPC # 0001: Extract OPC into standalone repo
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
using ControlPlane.Core.Config;
|
||||
using ControlPlane.Core.Interfaces;
|
||||
using ControlPlane.Core.Models;
|
||||
using ControlPlane.Worker.Services;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ControlPlane.Worker.Steps;
|
||||
|
||||
public class KeycloakStep(
|
||||
KeycloakAdminClient adminClient,
|
||||
IConfiguration config,
|
||||
IOptions<ClarityInfraOptions> infraOptions,
|
||||
ILogger<KeycloakStep> logger) : ISagaStep
|
||||
{
|
||||
public string StepName => "Identity Bootstrapping (Keycloak)";
|
||||
|
||||
public async Task ExecuteAsync(SagaContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var realmId = RealmId(context);
|
||||
|
||||
logger.LogInformation("[{JobId}] Creating Keycloak realm {Realm}.", context.Job.Id, realmId);
|
||||
await adminClient.CreateRealmAsync(realmId, context.Job.ClientName, cancellationToken);
|
||||
|
||||
logger.LogInformation("[{JobId}] Creating AgencyAdmin role.", context.Job.Id);
|
||||
await adminClient.CreateRealmRoleAsync(realmId, "AgencyAdmin", "Day-zero administrator for this Clarity tenant.", cancellationToken);
|
||||
|
||||
// Derive the tenant's public-facing origin from ClarityInfraOptions.
|
||||
var tenantOrigin = infraOptions.Value.TenantPublicUrl(context.Job.Subdomain);
|
||||
|
||||
logger.LogInformation("[{JobId}] Creating Keycloak clients for realm {Realm} (origin: {Origin}).",
|
||||
context.Job.Id, realmId, tenantOrigin);
|
||||
|
||||
// clarity-rest-api: bearer-only resource server — just registers the audience so JWT validation passes.
|
||||
await adminClient.CreateClientAsync(realmId, new
|
||||
{
|
||||
clientId = "clarity-rest-api",
|
||||
name = "Clarity REST API",
|
||||
enabled = true,
|
||||
bearerOnly = true,
|
||||
publicClient = false,
|
||||
}, cancellationToken);
|
||||
|
||||
// clarity-web-app: public OIDC client used by the React frontend.
|
||||
await adminClient.CreateClientAsync(realmId, new
|
||||
{
|
||||
clientId = "clarity-web-app",
|
||||
name = "Clarity Web App",
|
||||
enabled = true,
|
||||
publicClient = true,
|
||||
standardFlowEnabled = true,
|
||||
directAccessGrantsEnabled = false,
|
||||
rootUrl = tenantOrigin,
|
||||
baseUrl = "/",
|
||||
redirectUris = new[] { $"{tenantOrigin}/*" },
|
||||
webOrigins = new[] { tenantOrigin },
|
||||
}, cancellationToken);
|
||||
|
||||
// Ensure tokens issued by clarity-web-app include "clarity-rest-api" in the `aud` claim
|
||||
// so that Clarity.Server JWT bearer validation (Audience = "clarity-rest-api") passes.
|
||||
logger.LogInformation("[{JobId}] Adding audience mapper for clarity-rest-api on clarity-web-app.", context.Job.Id);
|
||||
var webAppUuid = await adminClient.GetClientUuidAsync(realmId, "clarity-web-app", cancellationToken);
|
||||
await adminClient.AddAudienceMapperAsync(realmId, webAppUuid, "clarity-rest-api", cancellationToken);
|
||||
|
||||
logger.LogInformation("[{JobId}] Creating day-zero user {Email}.", context.Job.Id, context.Job.AdminEmail);
|
||||
var userId = await adminClient.CreateUserAsync(realmId, context.Job.AdminEmail, context.Job.ClientName, cancellationToken);
|
||||
|
||||
logger.LogInformation("[{JobId}] Assigning AgencyAdmin role.", context.Job.Id);
|
||||
await adminClient.AssignRealmRoleAsync(realmId, userId, "AgencyAdmin", cancellationToken);
|
||||
|
||||
// TODO No SMTP right now
|
||||
//logger.LogInformation("[{JobId}] Sending required actions email to {Email}.", context.Job.Id, context.Job.AdminEmail);
|
||||
//await adminClient.SendRequiredActionsEmailAsync(realmId, userId, ["UPDATE_PASSWORD", "VERIFY_EMAIL"], cancellationToken);
|
||||
|
||||
context.DayZeroUserSubjectId = userId;
|
||||
//context.MagicLink = $"Action email sent to {context.Job.AdminEmail} for realm '{realmId}'.";
|
||||
context.Job.CompletedSteps |= CompletedSteps.KeycloakProvisioned;
|
||||
|
||||
logger.LogInformation("[{JobId}] Keycloak provisioning complete for realm {Realm}.", context.Job.Id, realmId);
|
||||
}
|
||||
|
||||
public async Task CompensateAsync(SagaContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var realmId = RealmId(context);
|
||||
logger.LogWarning("[{JobId}] Compensating Keycloak - deleting realm {Realm}.", context.Job.Id, realmId);
|
||||
await adminClient.DeleteRealmAsync(realmId, cancellationToken);
|
||||
}
|
||||
|
||||
private static string RealmId(SagaContext context) =>
|
||||
$"clarity-{context.Job.Subdomain.ToLowerInvariant()}";
|
||||
}
|
||||
Reference in New Issue
Block a user