src/platform/config-portability/config-bundle-apply.service.ts

Relationships

Depends on

No results matching.

Index

Methods

Constructor

constructor(bundleService: ConfigBundleService, migrationService: ConfigMigrationService, ownershipService: ConfigOwnershipService, filesService: FilesService, keyChainService: KeyChainService, kmsReferenceService: ConfigKmsReferenceService, resourceRegistry: ConfigResourceRegistry, kmsTenantConfigService: KmsTenantConfigService, registrarConfigService: RegistrarConfigService, issuanceService: IssuanceService, credentialConfigService: CredentialConfigService, presentationsService: PresentationsService, attributeProviderService: AttributeProviderService, webhookEndpointService: WebhookEndpointService, trustListService: TrustListService, statusListService: StatusListService, clientsProvider: ClientsProvider, tenants: Repository<TenantEntity>, clients: Repository<ClientEntity>, keyChains: Repository<KeyChainEntity>, registrarConfigs: Repository<RegistrarConfigEntity>, issuanceConfigs: Repository<IssuanceConfig>, credentialConfigs: Repository<CredentialConfig>, presentationConfigs: Repository<PresentationConfig>, attributeProviders: Repository<AttributeProviderEntity>, webhookEndpoints: Repository<WebhookEndpointEntity>, trustLists: Repository<TrustList>, statusLists: Repository<StatusListEntity>)
Parameters :
Name Type Optional
bundleService ConfigBundleService No
migrationService ConfigMigrationService No
ownershipService ConfigOwnershipService No
filesService FilesService No
keyChainService KeyChainService No
kmsReferenceService ConfigKmsReferenceService No
resourceRegistry ConfigResourceRegistry No
kmsTenantConfigService KmsTenantConfigService No
registrarConfigService RegistrarConfigService No
issuanceService IssuanceService No
credentialConfigService CredentialConfigService No
presentationsService PresentationsService No
attributeProviderService AttributeProviderService No
webhookEndpointService WebhookEndpointService No
trustListService TrustListService No
statusListService StatusListService No
clientsProvider ClientsProvider No
tenants Repository<TenantEntity> No
clients Repository<ClientEntity> No
keyChains Repository<KeyChainEntity> No
registrarConfigs Repository<RegistrarConfigEntity> No
issuanceConfigs Repository<IssuanceConfig> No
credentialConfigs Repository<CredentialConfig> No
presentationConfigs Repository<PresentationConfig> No
attributeProviders Repository<AttributeProviderEntity> No
webhookEndpoints Repository<WebhookEndpointEntity> No
trustLists Repository<TrustList> No
statusLists Repository<StatusListEntity> No

Methods

Async apply
apply(tenantId: string, bundle: ConfigBundle, mode: ConfigImportMode, ownershipSource: unknown)
Parameters :
Name Type Optional Default value
tenantId string No
bundle ConfigBundle No
mode ConfigImportMode No
ownershipSource unknown No `bundle:${bundle.manifest.tenant}`
Private Async applyDocument
applyDocument(tenantId: string, document: ConfigDocument)
Parameters :
Name Type Optional
tenantId string No
document ConfigDocument No
Returns : Promise<literal type | undefined>
Private Async applyKeyChain
applyKeyChain(tenantId: string, document: ConfigDocument)
Parameters :
Name Type Optional
tenantId string No
document ConfigDocument No
Returns : Promise<void>
Private Async deleteDocument
deleteDocument(tenantId: string, kind: ConfigResourceKind, id: string)
Parameters :
Name Type Optional
tenantId string No
kind ConfigResourceKind No
id string No
Returns : Promise<void>
Private order
order(kind: ConfigResourceKind)
Parameters :
Name Type Optional
kind ConfigResourceKind No
Returns : number
Private Async restoreAssets
restoreAssets(tenantId: string, bundle: ConfigBundle, overwrite: boolean)
Parameters :
Name Type Optional
tenantId string No
bundle ConfigBundle No
overwrite boolean No
Returns : Promise<void>
import { createHash, randomBytes } from "node:crypto";
import { BadRequestException, Inject, Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import {
    CLIENTS_PROVIDER,
    ClientsProvider,
} from "../../auth/client/client.provider";
import { ClientEntity } from "../../auth/client/entities/client.entity";
import { TenantEntity } from "../../auth/tenant/entities/tenant.entity";
import { KeyChainType } from "../../crypto/key/dto/key-chain-create.dto";
import {
    KeyChainEntity,
    KeyUsage,
} from "../../crypto/key/entities/key-chain.entity";
import { KeyChainService } from "../../crypto/key/key-chain.service";
import { KmsTenantConfigService } from "../../crypto/key/kms/kms-tenant-config.service";
import { AttributeProviderService } from "../../issuer/configuration/attribute-provider/attribute-provider.service";
import { AttributeProviderEntity } from "../../issuer/configuration/attribute-provider/entities/attribute-provider.entity";
import { CredentialConfigService } from "../../issuer/configuration/credentials/credential-config/credential-config.service";
import { CredentialConfig } from "../../issuer/configuration/credentials/entities/credential.entity";
import { IssuanceConfig } from "../../issuer/configuration/issuance/entities/issuance-config.entity";
import { IssuanceService } from "../../issuer/configuration/issuance/issuance.service";
import { WebhookEndpointEntity } from "../../issuer/configuration/webhook-endpoint/entities/webhook-endpoint.entity";
import { WebhookEndpointService } from "../../issuer/configuration/webhook-endpoint/webhook-endpoint.service";
import { StatusListEntity } from "../../issuer/status-list/entities/status-list.entity";
import { StatusListService } from "../../issuer/status-list/status-list.service";
import { TrustList } from "../../issuer/trust-list/entities/trust-list.entity";
import { TrustListService } from "../../issuer/trust-list/trustlist.service";
import { RegistrarConfigEntity } from "../../registrar/entities/registrar-config.entity";
import { RegistrarConfigService } from "../../registrar/registrar-config.service";
import { FilesService } from "../../storage/files.service";
import { PresentationConfig } from "../../verifier/presentations/entities/presentation-config.entity";
import { PresentationsService } from "../../verifier/presentations/presentations.service";
import { ConfigBundleService } from "./config-bundle.service";
import { ConfigKmsReferenceService } from "./config-kms-reference.service";
import { ConfigMigrationService } from "./config-migration.service";
import { ConfigOwnershipService } from "./config-ownership.service";
import { ConfigResourceRegistry } from "./config-resource.registry";
import type {
    ConfigBundle,
    ConfigDocument,
    ConfigImportMode,
    ConfigImportPlan,
    ConfigResourceKind,
} from "./config-resource.types";

@Injectable()
export class ConfigBundleApplyService {
    constructor(
        private readonly bundleService: ConfigBundleService,
        private readonly migrationService: ConfigMigrationService,
        private readonly ownershipService: ConfigOwnershipService,
        private readonly filesService: FilesService,
        private readonly keyChainService: KeyChainService,
        private readonly kmsReferenceService: ConfigKmsReferenceService,
        private readonly resourceRegistry: ConfigResourceRegistry,
        private readonly kmsTenantConfigService: KmsTenantConfigService,
        private readonly registrarConfigService: RegistrarConfigService,
        private readonly issuanceService: IssuanceService,
        private readonly credentialConfigService: CredentialConfigService,
        private readonly presentationsService: PresentationsService,
        private readonly attributeProviderService: AttributeProviderService,
        private readonly webhookEndpointService: WebhookEndpointService,
        private readonly trustListService: TrustListService,
        private readonly statusListService: StatusListService,
        @Inject(CLIENTS_PROVIDER)
        private readonly clientsProvider: ClientsProvider,
        @InjectRepository(TenantEntity)
        private readonly tenants: Repository<TenantEntity>,
        @InjectRepository(ClientEntity)
        private readonly clients: Repository<ClientEntity>,
        @InjectRepository(KeyChainEntity)
        private readonly keyChains: Repository<KeyChainEntity>,
        @InjectRepository(RegistrarConfigEntity)
        private readonly registrarConfigs: Repository<RegistrarConfigEntity>,
        @InjectRepository(IssuanceConfig)
        private readonly issuanceConfigs: Repository<IssuanceConfig>,
        @InjectRepository(CredentialConfig)
        private readonly credentialConfigs: Repository<CredentialConfig>,
        @InjectRepository(PresentationConfig)
        private readonly presentationConfigs: Repository<PresentationConfig>,
        @InjectRepository(AttributeProviderEntity)
        private readonly attributeProviders: Repository<AttributeProviderEntity>,
        @InjectRepository(WebhookEndpointEntity)
        private readonly webhookEndpoints: Repository<WebhookEndpointEntity>,
        @InjectRepository(TrustList)
        private readonly trustLists: Repository<TrustList>,
        @InjectRepository(StatusListEntity)
        private readonly statusLists: Repository<StatusListEntity>,
    ) {}

    async apply(
        tenantId: string,
        bundle: ConfigBundle,
        mode: ConfigImportMode,
        ownershipSource = `bundle:${bundle.manifest.tenant}`,
    ): Promise<ConfigImportPlan> {
        const plan = await this.bundleService.plan(
            tenantId,
            bundle,
            mode,
            ownershipSource,
        );
        if (!plan.applicable) {
            throw new BadRequestException({
                message: "Configuration bundle has blocking issues",
                plan,
            });
        }
        // Resource importers resolve image filenames to stored public URLs, so
        // bundle assets must exist before the dependent resources are applied.
        await this.restoreAssets(tenantId, bundle, mode !== "create");
        const applicable = new Set(
            plan.items
                .filter(
                    (item) =>
                        item.action === "create" || item.action === "update",
                )
                .map((item) => `${item.kind}/${item.id}`),
        );
        const ordered = [...bundle.documents]
            .sort(
                (left, right) => this.order(left.kind) - this.order(right.kind),
            )
            .filter((document) =>
                applicable.has(`${document.kind}/${document.metadata.id}`),
            );
        const generatedSecrets: NonNullable<
            ConfigImportPlan["generatedSecrets"]
        > = [];
        for (const input of ordered) {
            const { document } = this.migrationService.upgrade(input);
            // A startup-folder KMS document is already the live backing file.
            // Record it as managed without rewriting an envelope into a bare spec.
            const generatedSecret =
                document.kind === "KmsConfig" &&
                ownershipSource.startsWith("folder:")
                    ? undefined
                    : await this.applyDocument(tenantId, document);
            if (generatedSecret) generatedSecrets.push(generatedSecret);
            await this.ownershipService.markApplied({
                tenantId,
                kind: document.kind,
                resourceId: document.metadata.id,
                ownership: "file-managed",
                generation: document.metadata.generation ?? 1,
                source: ownershipSource,
                sourceHash: createHash("sha256")
                    .update(JSON.stringify(document))
                    .digest("hex"),
            });
        }
        const deletions = plan.items
            .filter((item) => item.action === "delete")
            .sort(
                (left, right) => this.order(right.kind) - this.order(left.kind),
            );
        for (const item of deletions) {
            await this.deleteDocument(tenantId, item.kind, item.id);
            await this.ownershipService.remove(tenantId, item.kind, item.id);
        }
        return generatedSecrets.length ? { ...plan, generatedSecrets } : plan;
    }

    private async restoreAssets(
        tenantId: string,
        bundle: ConfigBundle,
        overwrite: boolean,
    ): Promise<void> {
        for (const asset of bundle.assets ?? []) {
            const data = Buffer.from(asset.data, "base64");
            if (
                createHash("sha256").update(data).digest("hex") !== asset.sha256
            ) {
                throw new BadRequestException(
                    `Asset checksum mismatch: ${asset.path}`,
                );
            }
            await this.filesService.saveImportedAsset(
                tenantId,
                asset.path.replace(/^images\//, ""),
                data,
                asset.contentType,
                overwrite,
            );
        }
    }

    private async deleteDocument(
        tenantId: string,
        kind: ConfigResourceKind,
        id: string,
    ): Promise<void> {
        switch (kind) {
            case "Tenant":
                throw new BadRequestException(
                    "Replace mode cannot delete a tenant",
                );
            case "Client":
                await this.clientsProvider.removeClient(tenantId, id);
                return;
            case "KmsConfig":
                this.kmsTenantConfigService.deleteTenantConfig(tenantId);
                return;
            case "KeyChain":
                await this.keyChainService.delete(tenantId, id);
                return;
            case "RegistrarConfig":
                await this.registrarConfigService.deleteConfig(tenantId);
                return;
            case "IssuanceConfig":
                await this.issuanceConfigs.delete({ tenantId });
                return;
            case "CredentialConfig":
                await this.credentialConfigService.delete(tenantId, id);
                return;
            case "PresentationConfig":
                await this.presentationsService.deletePresentationConfig(
                    id,
                    tenantId,
                );
                return;
            case "AttributeProvider":
                await this.attributeProviderService.delete(tenantId, id);
                return;
            case "WebhookEndpoint":
                await this.webhookEndpointService.delete(tenantId, id);
                return;
            case "TrustList":
                await this.trustListService.remove(tenantId, id);
                return;
            case "StatusList":
                await this.statusListService.deleteList(tenantId, id);
        }
    }

    private async applyDocument(
        tenantId: string,
        document: ConfigDocument,
    ): Promise<
        | {
              kind: "Client";
              id: string;
              path: "/spec/secret";
              value: string;
          }
        | undefined
    > {
        const spec = structuredClone(document.spec) as Record<string, any>;
        switch (document.kind) {
            case "Tenant":
                await this.tenants.update({ id: tenantId }, spec);
                return;
            case "Client": {
                const exists =
                    (await this.clients.countBy({
                        tenantId,
                        clientId: document.metadata.id,
                    })) > 0;
                const generated = spec.secret === "!generate";
                const secret = generated
                    ? exists
                        ? undefined
                        : randomBytes(32).toString("base64url")
                    : typeof spec.secret === "string"
                      ? spec.secret
                      : undefined;
                delete spec.secret;
                const client = {
                    ...spec,
                    clientId: document.metadata.id,
                } as any;
                if (exists) {
                    await this.clientsProvider.updateClient(
                        tenantId,
                        document.metadata.id,
                        client,
                    );
                    if (generated) {
                        const value =
                            await this.clientsProvider.rotateClientSecret(
                                tenantId,
                                document.metadata.id,
                            );
                        return {
                            kind: "Client",
                            id: document.metadata.id,
                            path: "/spec/secret",
                            value,
                        };
                    }
                    if (secret) {
                        await this.clientsProvider.setClientSecret(
                            tenantId,
                            document.metadata.id,
                            secret,
                        );
                    }
                } else {
                    await this.clientsProvider.addClient(tenantId, {
                        ...client,
                        secret,
                    });
                }
                return generated
                    ? {
                          kind: "Client",
                          id: document.metadata.id,
                          path: "/spec/secret",
                          value: secret!,
                      }
                    : undefined;
            }
            case "KmsConfig":
                this.kmsTenantConfigService.saveTenantConfig(
                    tenantId,
                    spec as any,
                );
                return;
            case "KeyChain":
                await this.applyKeyChain(tenantId, document);
                return;
            case "RegistrarConfig":
                await this.registrarConfigService.saveConfig(
                    tenantId,
                    spec as any,
                );
                return;
            case "IssuanceConfig":
                await this.issuanceService.storeIssuanceConfiguration(
                    tenantId,
                    spec as any,
                );
                return;
            case "CredentialConfig":
                await this.credentialConfigService.store(
                    tenantId,
                    { ...spec, id: document.metadata.id } as any,
                    true,
                );
                return;
            case "PresentationConfig":
                await this.presentationsService.storePresentationConfig(
                    tenantId,
                    { ...spec, id: document.metadata.id } as any,
                );
                return;
            case "AttributeProvider":
                if (
                    await this.attributeProviders.countBy({
                        tenantId,
                        id: document.metadata.id,
                    })
                ) {
                    await this.attributeProviderService.update(
                        tenantId,
                        document.metadata.id,
                        spec as any,
                    );
                } else {
                    await this.attributeProviderService.create(tenantId, {
                        ...spec,
                        id: document.metadata.id,
                    } as any);
                }
                return;
            case "WebhookEndpoint":
                if (
                    await this.webhookEndpoints.countBy({
                        tenantId,
                        id: document.metadata.id,
                    })
                ) {
                    await this.webhookEndpointService.update(
                        tenantId,
                        document.metadata.id,
                        spec as any,
                    );
                } else {
                    await this.webhookEndpointService.create(tenantId, {
                        ...spec,
                        id: document.metadata.id,
                    } as any);
                }
                return;
            case "TrustList": {
                const tenant = await this.tenants.findOneByOrFail({
                    id: tenantId,
                });
                if (
                    await this.trustLists.countBy({
                        tenantId,
                        id: document.metadata.id,
                    })
                ) {
                    await this.trustListService.update(
                        tenantId,
                        document.metadata.id,
                        { ...spec, id: document.metadata.id } as any,
                    );
                } else {
                    await this.trustListService.create(
                        { ...spec, id: document.metadata.id } as any,
                        tenant,
                    );
                }
                return;
            }
            case "StatusList":
                await this.statusLists.delete({
                    tenantId,
                    id: document.metadata.id,
                });
                await this.statusListService.processStatusListConfig(tenantId, {
                    ...spec,
                    id: document.metadata.id,
                } as any);
        }
    }

    private async applyKeyChain(
        tenantId: string,
        document: ConfigDocument,
    ): Promise<void> {
        const spec = document.spec as any;
        if (spec.keySource?.type === "regenerate") {
            await this.keyChainService.regenerate(
                tenantId,
                document.metadata.id,
                {
                    usageType: spec.usageType,
                    type:
                        spec.keySource.keyChainType ??
                        (spec.activeCertificate
                            ? KeyChainType.InternalChain
                            : KeyChainType.Standalone),
                    description: spec.description,
                    kmsProvider:
                        spec.keySource.provider ?? spec.kmsProvider ?? "db",
                    rotationPolicy: spec.rotationPolicy,
                },
            );
            return;
        }
        if (spec.keySource?.type === "private-jwk") {
            await this.keyChains.delete({ tenantId, id: document.metadata.id });
            if (spec.keySource.activeJwk) {
                await this.keyChains.save({
                    id: document.metadata.id,
                    tenantId,
                    description: spec.description,
                    usageType: spec.usageType,
                    usage: KeyUsage.Sign,
                    kmsProvider: "db",
                    rootJwk: spec.keySource.jwk,
                    rootCertificate: spec.crt?.at(-1),
                    activeJwk: spec.keySource.activeJwk,
                    activeCertificate: spec.activeCertificate,
                    rotationEnabled: spec.rotationPolicy?.enabled ?? true,
                    rotationIntervalDays: spec.rotationPolicy?.intervalDays,
                    certValidityDays: spec.rotationPolicy?.certValidityDays,
                });
                return;
            }
            await this.keyChainService.importKeyChain(
                tenantId,
                this.migrationService.unwrapForLegacyImporter(document) as any,
            );
            return;
        }
        if (spec.keySource?.type !== "external-reference") {
            throw new BadRequestException(
                `KeyChain '${document.metadata.id}' has no importable key source`,
            );
        }
        await this.kmsReferenceService.verify(tenantId, spec.keySource);
        await this.keyChains.save({
            id: document.metadata.id,
            tenantId,
            description: spec.description,
            usageType: spec.usageType,
            usage: KeyUsage.Sign,
            kmsProvider: spec.keySource.provider,
            rootJwk: spec.keySource.activeExternalKeyId
                ? spec.keySource.publicJwk
                : undefined,
            rootExternalKeyId: spec.keySource.activeExternalKeyId
                ? spec.keySource.externalKeyId
                : undefined,
            rootCertificate: spec.keySource.activeExternalKeyId
                ? spec.crt?.at(-1)
                : undefined,
            activeJwk:
                spec.keySource.activePublicJwk ?? spec.keySource.publicJwk,
            externalKeyId:
                spec.keySource.activeExternalKeyId ??
                spec.keySource.externalKeyId,
            activeCertificate:
                spec.activeCertificate ?? spec.crt?.join("\n") ?? "",
            rotationEnabled: spec.rotationPolicy?.enabled ?? false,
            rotationIntervalDays: spec.rotationPolicy?.intervalDays,
            certValidityDays: spec.rotationPolicy?.certValidityDays,
        });
    }

    private order(kind: ConfigResourceKind): number {
        return this.resourceRegistry.get(kind).importPhase;
    }
}

results matching ""

    No results matching ""