mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 23:28:31 +00:00
Extract things as service instead
This commit is contained in:
Vendored
+2
@@ -105,6 +105,7 @@ import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-c
|
|||||||
import { TPkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
|
import { TPkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
|
||||||
import { TPkiSyncServiceFactory } from "@app/services/pki-sync/pki-sync-service";
|
import { TPkiSyncServiceFactory } from "@app/services/pki-sync/pki-sync-service";
|
||||||
import { TPkiTemplatesServiceFactory } from "@app/services/pki-templates/pki-templates-service";
|
import { TPkiTemplatesServiceFactory } from "@app/services/pki-templates/pki-templates-service";
|
||||||
|
import { TPkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-types";
|
||||||
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||||
@@ -294,6 +295,7 @@ declare module "fastify" {
|
|||||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||||
certificateEst: TCertificateEstServiceFactory;
|
certificateEst: TCertificateEstServiceFactory;
|
||||||
|
pkiAcme: TPkiAcmeServiceFactory;
|
||||||
certificateEstV3: TCertificateEstV3ServiceFactory;
|
certificateEstV3: TCertificateEstV3ServiceFactory;
|
||||||
pkiCollection: TPkiCollectionServiceFactory;
|
pkiCollection: TPkiCollectionServiceFactory;
|
||||||
pkiSubscriber: TPkiSubscriberServiceFactory;
|
pkiSubscriber: TPkiSubscriberServiceFactory;
|
||||||
|
|||||||
@@ -3,6 +3,28 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import {
|
||||||
|
CreateAcmeAccountResponseSchema,
|
||||||
|
CreateAcmeAccountSchema,
|
||||||
|
CreateAcmeOrderResponseSchema,
|
||||||
|
CreateAcmeOrderSchema,
|
||||||
|
DeactivateAcmeAccountResponseSchema,
|
||||||
|
DeactivateAcmeAccountSchema,
|
||||||
|
DownloadAcmeCertificateSchema,
|
||||||
|
FinalizeAcmeOrderResponseSchema,
|
||||||
|
FinalizeAcmeOrderSchema,
|
||||||
|
GetAcmeAuthorizationResponseSchema,
|
||||||
|
GetAcmeAuthorizationSchema,
|
||||||
|
GetAcmeDirectoryResponseSchema,
|
||||||
|
GetAcmeDirectorySchema,
|
||||||
|
GetAcmeNewNonceSchema,
|
||||||
|
GetAcmeOrderResponseSchema,
|
||||||
|
GetAcmeOrderSchema,
|
||||||
|
ListAcmeOrdersResponseSchema,
|
||||||
|
ListAcmeOrdersSchema,
|
||||||
|
RespondToAcmeChallengeResponseSchema,
|
||||||
|
RespondToAcmeChallengeSchema
|
||||||
|
} from "@app/ee/services/pki-acme/pki-acme-schemas";
|
||||||
|
|
||||||
export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
||||||
// GET /api/v1/pki/acme/profiles/<profile_id>/directory
|
// GET /api/v1/pki/acme/profiles/<profile_id>/directory
|
||||||
@@ -17,27 +39,14 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiAcme],
|
tags: [ApiDocsTags.PkiAcme],
|
||||||
description: "ACME Directory - provides URLs for the client to make API calls to",
|
description: "ACME Directory - provides URLs for the client to make API calls to",
|
||||||
params: z.object({
|
...GetAcmeDirectorySchema.shape,
|
||||||
profileId: z.string().uuid()
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: GetAcmeDirectoryResponseSchema
|
||||||
newNonce: z.string(),
|
|
||||||
newAccount: z.string(),
|
|
||||||
newOrder: z.string(),
|
|
||||||
revokeCert: z.string()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
// FIXME: Implement ACME directory endpoint
|
const directory = await server.services.pkiAcme.getAcmeDirectory(req.params.profileId);
|
||||||
// This endpoint should return the base URLs for ACME operations
|
return directory;
|
||||||
return {
|
|
||||||
newNonce: `/api/v1/pki/acme/profiles/${req.params.profileId}/new-nonce`,
|
|
||||||
newAccount: `/api/v1/pki/acme/profiles/${req.params.profileId}/new-account`,
|
|
||||||
newOrder: `/api/v1/pki/acme/profiles/${req.params.profileId}/new-order`,
|
|
||||||
revokeCert: `/api/v1/pki/acme/profiles/${req.params.profileId}/revoke-cert`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,17 +63,13 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiAcme],
|
tags: [ApiDocsTags.PkiAcme],
|
||||||
description: "ACME New Nonce - generate a new nonce and return in Replay-Nonce header",
|
description: "ACME New Nonce - generate a new nonce and return in Replay-Nonce header",
|
||||||
params: z.object({
|
...GetAcmeNewNonceSchema.shape,
|
||||||
profileId: z.string().uuid()
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({})
|
200: z.object({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req, res) => {
|
handler: async (req, res) => {
|
||||||
// FIXME: Implement ACME new nonce generation
|
const nonce = await server.services.pkiAcme.getAcmeNewNonce(req.params.profileId);
|
||||||
// Generate a new nonce, store it, and return it in the Replay-Nonce header
|
|
||||||
const nonce = "FIXME-generate-nonce";
|
|
||||||
res.header("Replay-Nonce", nonce);
|
res.header("Replay-Nonce", nonce);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -82,40 +87,15 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiAcme],
|
tags: [ApiDocsTags.PkiAcme],
|
||||||
description: "ACME New Account - register a new account or find existing one",
|
description: "ACME New Account - register a new account or find existing one",
|
||||||
params: z.object({
|
...CreateAcmeAccountSchema.shape,
|
||||||
profileId: z.string().uuid()
|
|
||||||
}),
|
|
||||||
body: z.object({
|
|
||||||
contact: z.array(z.string()).optional(),
|
|
||||||
termsOfServiceAgreed: z.boolean().optional(),
|
|
||||||
onlyReturnExisting: z.boolean().optional(),
|
|
||||||
externalAccountBinding: z
|
|
||||||
.object({
|
|
||||||
protected: z.string(),
|
|
||||||
payload: z.string(),
|
|
||||||
signature: z.string()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
201: z.object({
|
201: CreateAcmeAccountResponseSchema
|
||||||
status: z.string(),
|
|
||||||
contact: z.array(z.string()).optional(),
|
|
||||||
orders: z.string().optional(),
|
|
||||||
accountUrl: z.string()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req, res) => {
|
||||||
// FIXME: Implement ACME new account registration
|
const account = await server.services.pkiAcme.createAcmeAccount(req.params.profileId, req.body);
|
||||||
// Use EAB authentication to find corresponding Infisical machine identity
|
res.code(201);
|
||||||
// Check permissions and return account information
|
return account;
|
||||||
return {
|
|
||||||
status: "valid",
|
|
||||||
accountUrl: `/api/v1/pki/acme/profiles/${req.params.profileId}/accounts/FIXME-account-id`,
|
|
||||||
contact: req.body.contact,
|
|
||||||
orders: `/api/v1/pki/acme/profiles/${req.params.profileId}/accounts/FIXME-account-id/orders`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,47 +111,15 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiAcme],
|
tags: [ApiDocsTags.PkiAcme],
|
||||||
description: "ACME New Order - apply for a new certificate",
|
description: "ACME New Order - apply for a new certificate",
|
||||||
params: z.object({
|
...CreateAcmeOrderSchema.shape,
|
||||||
profileId: z.string().uuid()
|
|
||||||
}),
|
|
||||||
body: z.object({
|
|
||||||
identifiers: z.array(
|
|
||||||
z.object({
|
|
||||||
type: z.string(),
|
|
||||||
value: z.string()
|
|
||||||
})
|
|
||||||
),
|
|
||||||
notBefore: z.string().optional(),
|
|
||||||
notAfter: z.string().optional()
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
201: z.object({
|
201: CreateAcmeOrderResponseSchema
|
||||||
status: z.string(),
|
|
||||||
expires: z.string(),
|
|
||||||
identifiers: z.array(
|
|
||||||
z.object({
|
|
||||||
type: z.string(),
|
|
||||||
value: z.string()
|
|
||||||
})
|
|
||||||
),
|
|
||||||
authorizations: z.array(z.string()),
|
|
||||||
finalize: z.string(),
|
|
||||||
certificate: z.string().optional()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req, res) => {
|
||||||
// FIXME: Implement ACME new order creation
|
const order = await server.services.pkiAcme.createAcmeOrder(req.params.profileId, req.body);
|
||||||
const orderId = "FIXME-order-id";
|
res.code(201);
|
||||||
return {
|
return order;
|
||||||
status: "pending",
|
|
||||||
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
identifiers: req.body.identifiers,
|
|
||||||
authorizations: req.body.identifiers.map(
|
|
||||||
(id) => `/api/v1/pki/acme/profiles/${req.params.profileId}/authorizations/FIXME-authz-${id.value}`
|
|
||||||
),
|
|
||||||
finalize: `/api/v1/pki/acme/profiles/${req.params.profileId}/orders/${orderId}/finalize`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -187,24 +135,14 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiAcme],
|
tags: [ApiDocsTags.PkiAcme],
|
||||||
description: "ACME Account Deactivation",
|
description: "ACME Account Deactivation",
|
||||||
params: z.object({
|
...DeactivateAcmeAccountSchema.shape,
|
||||||
profileId: z.string().uuid(),
|
|
||||||
accountId: z.string()
|
|
||||||
}),
|
|
||||||
body: z.object({
|
|
||||||
status: z.literal("deactivated")
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: DeactivateAcmeAccountResponseSchema
|
||||||
status: z.string()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
// FIXME: Implement ACME account deactivation
|
const result = await server.services.pkiAcme.deactivateAcmeAccount(req.params.profileId, req.params.accountId);
|
||||||
return {
|
return result;
|
||||||
status: "deactivated"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -220,21 +158,14 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiAcme],
|
tags: [ApiDocsTags.PkiAcme],
|
||||||
description: "ACME List Orders - get existing orders from current account",
|
description: "ACME List Orders - get existing orders from current account",
|
||||||
params: z.object({
|
...ListAcmeOrdersSchema.shape,
|
||||||
profileId: z.string().uuid(),
|
|
||||||
accountId: z.string()
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: ListAcmeOrdersResponseSchema
|
||||||
orders: z.array(z.string())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
// FIXME: Implement ACME list orders
|
const orders = await server.services.pkiAcme.listAcmeOrders(req.params.profileId, req.params.accountId);
|
||||||
return {
|
return orders;
|
||||||
orders: []
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,35 +181,14 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiAcme],
|
tags: [ApiDocsTags.PkiAcme],
|
||||||
description: "ACME Get Order - return status and details of the order",
|
description: "ACME Get Order - return status and details of the order",
|
||||||
params: z.object({
|
...GetAcmeOrderSchema.shape,
|
||||||
profileId: z.string().uuid(),
|
|
||||||
orderId: z.string()
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: GetAcmeOrderResponseSchema
|
||||||
status: z.string(),
|
|
||||||
expires: z.string().optional(),
|
|
||||||
identifiers: z.array(
|
|
||||||
z.object({
|
|
||||||
type: z.string(),
|
|
||||||
value: z.string()
|
|
||||||
})
|
|
||||||
),
|
|
||||||
authorizations: z.array(z.string()),
|
|
||||||
finalize: z.string(),
|
|
||||||
certificate: z.string().optional()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
// FIXME: Implement ACME get order
|
const order = await server.services.pkiAcme.getAcmeOrder(req.params.profileId, req.params.orderId);
|
||||||
return {
|
return order;
|
||||||
status: "pending",
|
|
||||||
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
identifiers: [],
|
|
||||||
authorizations: [],
|
|
||||||
finalize: `/api/v1/pki/acme/profiles/${req.params.profileId}/orders/${req.params.orderId}/finalize`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -294,39 +204,18 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiAcme],
|
tags: [ApiDocsTags.PkiAcme],
|
||||||
description: "ACME Finalize Order - finalize cert order by providing CSR",
|
description: "ACME Finalize Order - finalize cert order by providing CSR",
|
||||||
params: z.object({
|
...FinalizeAcmeOrderSchema.shape,
|
||||||
profileId: z.string().uuid(),
|
|
||||||
orderId: z.string()
|
|
||||||
}),
|
|
||||||
body: z.object({
|
|
||||||
csr: z.string()
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: FinalizeAcmeOrderResponseSchema
|
||||||
status: z.string(),
|
|
||||||
expires: z.string().optional(),
|
|
||||||
identifiers: z.array(
|
|
||||||
z.object({
|
|
||||||
type: z.string(),
|
|
||||||
value: z.string()
|
|
||||||
})
|
|
||||||
),
|
|
||||||
authorizations: z.array(z.string()),
|
|
||||||
finalize: z.string(),
|
|
||||||
certificate: z.string().optional()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
// FIXME: Implement ACME finalize order
|
const order = await server.services.pkiAcme.finalizeAcmeOrder(
|
||||||
return {
|
req.params.profileId,
|
||||||
status: "processing",
|
req.params.orderId,
|
||||||
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
req.body.csr
|
||||||
identifiers: [],
|
);
|
||||||
authorizations: [],
|
return order;
|
||||||
finalize: `/api/v1/pki/acme/profiles/${req.params.profileId}/orders/${req.params.orderId}/finalize`,
|
|
||||||
certificate: `/api/v1/pki/acme/profiles/${req.params.profileId}/orders/${req.params.orderId}/certificate`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -342,18 +231,16 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiAcme],
|
tags: [ApiDocsTags.PkiAcme],
|
||||||
description: "ACME Download Certificate - download certificate when ready",
|
description: "ACME Download Certificate - download certificate when ready",
|
||||||
params: z.object({
|
...DownloadAcmeCertificateSchema.shape,
|
||||||
profileId: z.string().uuid(),
|
|
||||||
orderId: z.string()
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.string()
|
200: z.string()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req, res) => {
|
handler: async (req, res) => {
|
||||||
// FIXME: Implement ACME certificate download
|
const certificate = await server.services.pkiAcme.downloadAcmeCertificate(
|
||||||
// Return the certificate in PEM format
|
req.params.profileId,
|
||||||
const certificate = "FIXME-certificate-pem";
|
req.params.orderId
|
||||||
|
);
|
||||||
res.header("Content-Type", "application/pem-certificate-chain");
|
res.header("Content-Type", "application/pem-certificate-chain");
|
||||||
return certificate;
|
return certificate;
|
||||||
}
|
}
|
||||||
@@ -371,48 +258,14 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiAcme],
|
tags: [ApiDocsTags.PkiAcme],
|
||||||
description: "ACME Identifier Authorization - get authorization info (challenges)",
|
description: "ACME Identifier Authorization - get authorization info (challenges)",
|
||||||
params: z.object({
|
...GetAcmeAuthorizationSchema.shape,
|
||||||
profileId: z.string().uuid(),
|
|
||||||
authzId: z.string()
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: GetAcmeAuthorizationResponseSchema
|
||||||
status: z.string(),
|
|
||||||
expires: z.string().optional(),
|
|
||||||
identifier: z.object({
|
|
||||||
type: z.string(),
|
|
||||||
value: z.string()
|
|
||||||
}),
|
|
||||||
challenges: z.array(
|
|
||||||
z.object({
|
|
||||||
type: z.string(),
|
|
||||||
url: z.string(),
|
|
||||||
status: z.string(),
|
|
||||||
token: z.string(),
|
|
||||||
validated: z.string().optional()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
// FIXME: Implement ACME authorization retrieval
|
const authz = await server.services.pkiAcme.getAcmeAuthorization(req.params.profileId, req.params.authzId);
|
||||||
return {
|
return authz;
|
||||||
status: "pending",
|
|
||||||
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
identifier: {
|
|
||||||
type: "dns",
|
|
||||||
value: "FIXME-domain-name"
|
|
||||||
},
|
|
||||||
challenges: [
|
|
||||||
{
|
|
||||||
type: "http-01",
|
|
||||||
url: `/api/v1/pki/acme/profiles/${req.params.profileId}/authorizations/${req.params.authzId}/challenges/http-01`,
|
|
||||||
status: "pending",
|
|
||||||
token: "FIXME-challenge-token"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -428,36 +281,14 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiAcme],
|
tags: [ApiDocsTags.PkiAcme],
|
||||||
description: "ACME Respond to Challenge - let ACME server know challenge is ready",
|
description: "ACME Respond to Challenge - let ACME server know challenge is ready",
|
||||||
params: z.object({
|
...RespondToAcmeChallengeSchema.shape,
|
||||||
profileId: z.string().uuid(),
|
|
||||||
authzId: z.string()
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: RespondToAcmeChallengeResponseSchema
|
||||||
type: z.string(),
|
|
||||||
url: z.string(),
|
|
||||||
status: z.string(),
|
|
||||||
token: z.string(),
|
|
||||||
validated: z.string().optional(),
|
|
||||||
error: z
|
|
||||||
.object({
|
|
||||||
type: z.string(),
|
|
||||||
detail: z.string(),
|
|
||||||
status: z.number()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
// FIXME: Implement ACME challenge response
|
const challenge = await server.services.pkiAcme.respondToAcmeChallenge(req.params.profileId, req.params.authzId);
|
||||||
// Trigger verification process
|
return challenge;
|
||||||
return {
|
|
||||||
type: "http-01",
|
|
||||||
url: `/api/v1/pki/acme/profiles/${req.params.profileId}/authorizations/${req.params.authzId}/challenges/http-01`,
|
|
||||||
status: "pending",
|
|
||||||
token: "FIXME-challenge-token"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Directory endpoint
|
||||||
|
export const GetAcmeDirectorySchema = z.object({
|
||||||
|
params: z.object({
|
||||||
|
profileId: z.string().uuid()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GetAcmeDirectoryResponseSchema = z.object({
|
||||||
|
newNonce: z.string(),
|
||||||
|
newAccount: z.string(),
|
||||||
|
newOrder: z.string(),
|
||||||
|
revokeCert: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
// New Nonce endpoint
|
||||||
|
export const GetAcmeNewNonceSchema = z.object({
|
||||||
|
params: z.object({
|
||||||
|
profileId: z.string().uuid()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// New Account endpoint
|
||||||
|
export const CreateAcmeAccountSchema = z.object({
|
||||||
|
params: z.object({
|
||||||
|
profileId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
contact: z.array(z.string()).optional(),
|
||||||
|
termsOfServiceAgreed: z.boolean().optional(),
|
||||||
|
onlyReturnExisting: z.boolean().optional(),
|
||||||
|
externalAccountBinding: z
|
||||||
|
.object({
|
||||||
|
protected: z.string(),
|
||||||
|
payload: z.string(),
|
||||||
|
signature: z.string()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateAcmeAccountResponseSchema = z.object({
|
||||||
|
status: z.string(),
|
||||||
|
contact: z.array(z.string()).optional(),
|
||||||
|
orders: z.string().optional(),
|
||||||
|
accountUrl: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
// New Order endpoint
|
||||||
|
export const CreateAcmeOrderSchema = z.object({
|
||||||
|
params: z.object({
|
||||||
|
profileId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
identifiers: z.array(
|
||||||
|
z.object({
|
||||||
|
type: z.string(),
|
||||||
|
value: z.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
notBefore: z.string().optional(),
|
||||||
|
notAfter: z.string().optional()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateAcmeOrderResponseSchema = z.object({
|
||||||
|
status: z.string(),
|
||||||
|
expires: z.string(),
|
||||||
|
identifiers: z.array(
|
||||||
|
z.object({
|
||||||
|
type: z.string(),
|
||||||
|
value: z.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
authorizations: z.array(z.string()),
|
||||||
|
finalize: z.string(),
|
||||||
|
certificate: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Account Deactivation endpoint
|
||||||
|
export const DeactivateAcmeAccountSchema = z.object({
|
||||||
|
params: z.object({
|
||||||
|
profileId: z.string().uuid(),
|
||||||
|
accountId: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
status: z.literal("deactivated")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DeactivateAcmeAccountResponseSchema = z.object({
|
||||||
|
status: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
// List Orders endpoint
|
||||||
|
export const ListAcmeOrdersSchema = z.object({
|
||||||
|
params: z.object({
|
||||||
|
profileId: z.string().uuid(),
|
||||||
|
accountId: z.string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ListAcmeOrdersResponseSchema = z.object({
|
||||||
|
orders: z.array(z.string())
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get Order endpoint
|
||||||
|
export const GetAcmeOrderSchema = z.object({
|
||||||
|
params: z.object({
|
||||||
|
profileId: z.string().uuid(),
|
||||||
|
orderId: z.string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GetAcmeOrderResponseSchema = z.object({
|
||||||
|
status: z.string(),
|
||||||
|
expires: z.string().optional(),
|
||||||
|
identifiers: z.array(
|
||||||
|
z.object({
|
||||||
|
type: z.string(),
|
||||||
|
value: z.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
authorizations: z.array(z.string()),
|
||||||
|
finalize: z.string(),
|
||||||
|
certificate: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Finalize Order endpoint
|
||||||
|
export const FinalizeAcmeOrderSchema = z.object({
|
||||||
|
params: z.object({
|
||||||
|
profileId: z.string().uuid(),
|
||||||
|
orderId: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
csr: z.string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const FinalizeAcmeOrderResponseSchema = z.object({
|
||||||
|
status: z.string(),
|
||||||
|
expires: z.string().optional(),
|
||||||
|
identifiers: z.array(
|
||||||
|
z.object({
|
||||||
|
type: z.string(),
|
||||||
|
value: z.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
authorizations: z.array(z.string()),
|
||||||
|
finalize: z.string(),
|
||||||
|
certificate: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download Certificate endpoint
|
||||||
|
export const DownloadAcmeCertificateSchema = z.object({
|
||||||
|
params: z.object({
|
||||||
|
profileId: z.string().uuid(),
|
||||||
|
orderId: z.string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get Authorization endpoint
|
||||||
|
export const GetAcmeAuthorizationSchema = z.object({
|
||||||
|
params: z.object({
|
||||||
|
profileId: z.string().uuid(),
|
||||||
|
authzId: z.string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GetAcmeAuthorizationResponseSchema = z.object({
|
||||||
|
status: z.string(),
|
||||||
|
expires: z.string().optional(),
|
||||||
|
identifier: z.object({
|
||||||
|
type: z.string(),
|
||||||
|
value: z.string()
|
||||||
|
}),
|
||||||
|
challenges: z.array(
|
||||||
|
z.object({
|
||||||
|
type: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
token: z.string(),
|
||||||
|
validated: z.string().optional()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Respond to Challenge endpoint
|
||||||
|
export const RespondToAcmeChallengeSchema = z.object({
|
||||||
|
params: z.object({
|
||||||
|
profileId: z.string().uuid(),
|
||||||
|
authzId: z.string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RespondToAcmeChallengeResponseSchema = z.object({
|
||||||
|
type: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
token: z.string(),
|
||||||
|
validated: z.string().optional(),
|
||||||
|
error: z
|
||||||
|
.object({
|
||||||
|
type: z.string(),
|
||||||
|
detail: z.string(),
|
||||||
|
status: z.number()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TCreateAcmeAccountResponse,
|
||||||
|
TCreateAcmeOrderResponse,
|
||||||
|
TDeactivateAcmeAccountResponse,
|
||||||
|
TDownloadAcmeCertificateDTO,
|
||||||
|
TFinalizeAcmeOrderResponse,
|
||||||
|
TGetAcmeAuthorizationResponse,
|
||||||
|
TGetAcmeDirectoryResponse,
|
||||||
|
TGetAcmeOrderResponse,
|
||||||
|
TListAcmeOrdersResponse,
|
||||||
|
TPkiAcmeServiceFactory,
|
||||||
|
TRespondToAcmeChallengeResponse
|
||||||
|
} from "./pki-acme-types";
|
||||||
|
|
||||||
|
type TPkiAcmeServiceFactoryDep = {
|
||||||
|
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => {
|
||||||
|
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
|
||||||
|
// FIXME: Implement ACME directory endpoint
|
||||||
|
// Validate profile exists and is for ACME enrollment
|
||||||
|
const profile = await certificateProfileDAL.findById(profileId);
|
||||||
|
if (!profile) {
|
||||||
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Validate profile is configured for ACME enrollment
|
||||||
|
return {
|
||||||
|
newNonce: `/api/v1/pki/acme/profiles/${profileId}/new-nonce`,
|
||||||
|
newAccount: `/api/v1/pki/acme/profiles/${profileId}/new-account`,
|
||||||
|
newOrder: `/api/v1/pki/acme/profiles/${profileId}/new-order`,
|
||||||
|
revokeCert: `/api/v1/pki/acme/profiles/${profileId}/revoke-cert`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAcmeNewNonce = async (profileId: string): Promise<string> => {
|
||||||
|
// FIXME: Implement ACME new nonce generation
|
||||||
|
// Generate a new nonce, store it, and return it
|
||||||
|
return "FIXME-generate-nonce";
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAcmeAccount = async (profileId: string, body: unknown): Promise<TCreateAcmeAccountResponse> => {
|
||||||
|
// FIXME: Implement ACME new account registration
|
||||||
|
// Use EAB authentication to find corresponding Infisical machine identity
|
||||||
|
// Check permissions and return account information
|
||||||
|
return {
|
||||||
|
status: "valid",
|
||||||
|
accountUrl: `/api/v1/pki/acme/profiles/${profileId}/accounts/FIXME-account-id`,
|
||||||
|
contact: [],
|
||||||
|
orders: `/api/v1/pki/acme/profiles/${profileId}/accounts/FIXME-account-id/orders`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAcmeOrder = async (profileId: string, body: unknown): Promise<TCreateAcmeOrderResponse> => {
|
||||||
|
// FIXME: Implement ACME new order creation
|
||||||
|
const orderId = "FIXME-order-id";
|
||||||
|
return {
|
||||||
|
status: "pending",
|
||||||
|
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
identifiers: [],
|
||||||
|
authorizations: [],
|
||||||
|
finalize: `/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const deactivateAcmeAccount = async (
|
||||||
|
profileId: string,
|
||||||
|
accountId: string
|
||||||
|
): Promise<TDeactivateAcmeAccountResponse> => {
|
||||||
|
// FIXME: Implement ACME account deactivation
|
||||||
|
return {
|
||||||
|
status: "deactivated"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const listAcmeOrders = async (profileId: string, accountId: string): Promise<TListAcmeOrdersResponse> => {
|
||||||
|
// FIXME: Implement ACME list orders
|
||||||
|
return {
|
||||||
|
orders: []
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAcmeOrder = async (profileId: string, orderId: string): Promise<TGetAcmeOrderResponse> => {
|
||||||
|
// FIXME: Implement ACME get order
|
||||||
|
return {
|
||||||
|
status: "pending",
|
||||||
|
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
identifiers: [],
|
||||||
|
authorizations: [],
|
||||||
|
finalize: `/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalizeAcmeOrder = async (
|
||||||
|
profileId: string,
|
||||||
|
orderId: string,
|
||||||
|
csr: string
|
||||||
|
): Promise<TFinalizeAcmeOrderResponse> => {
|
||||||
|
// FIXME: Implement ACME finalize order
|
||||||
|
return {
|
||||||
|
status: "processing",
|
||||||
|
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
identifiers: [],
|
||||||
|
authorizations: [],
|
||||||
|
finalize: `/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`,
|
||||||
|
certificate: `/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadAcmeCertificate = async (profileId: string, orderId: string): Promise<string> => {
|
||||||
|
// FIXME: Implement ACME certificate download
|
||||||
|
// Return the certificate in PEM format
|
||||||
|
return "FIXME-certificate-pem";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAcmeAuthorization = async (profileId: string, authzId: string): Promise<TGetAcmeAuthorizationResponse> => {
|
||||||
|
// FIXME: Implement ACME authorization retrieval
|
||||||
|
return {
|
||||||
|
status: "pending",
|
||||||
|
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
identifier: {
|
||||||
|
type: "dns",
|
||||||
|
value: "FIXME-domain-name"
|
||||||
|
},
|
||||||
|
challenges: [
|
||||||
|
{
|
||||||
|
type: "http-01",
|
||||||
|
url: `/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`,
|
||||||
|
status: "pending",
|
||||||
|
token: "FIXME-challenge-token"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const respondToAcmeChallenge = async (
|
||||||
|
profileId: string,
|
||||||
|
authzId: string
|
||||||
|
): Promise<TRespondToAcmeChallengeResponse> => {
|
||||||
|
// FIXME: Implement ACME challenge response
|
||||||
|
// Trigger verification process
|
||||||
|
return {
|
||||||
|
type: "http-01",
|
||||||
|
url: `/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`,
|
||||||
|
status: "pending",
|
||||||
|
token: "FIXME-challenge-token"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getAcmeDirectory,
|
||||||
|
getAcmeNewNonce,
|
||||||
|
createAcmeAccount,
|
||||||
|
createAcmeOrder,
|
||||||
|
deactivateAcmeAccount,
|
||||||
|
listAcmeOrders,
|
||||||
|
getAcmeOrder,
|
||||||
|
finalizeAcmeOrder,
|
||||||
|
downloadAcmeCertificate,
|
||||||
|
getAcmeAuthorization,
|
||||||
|
respondToAcmeChallenge
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateAcmeAccountResponseSchema,
|
||||||
|
CreateAcmeOrderResponseSchema,
|
||||||
|
DeactivateAcmeAccountResponseSchema,
|
||||||
|
FinalizeAcmeOrderResponseSchema,
|
||||||
|
GetAcmeAuthorizationResponseSchema,
|
||||||
|
GetAcmeDirectoryResponseSchema,
|
||||||
|
GetAcmeOrderResponseSchema,
|
||||||
|
ListAcmeOrdersResponseSchema,
|
||||||
|
RespondToAcmeChallengeResponseSchema
|
||||||
|
} from "./pki-acme-schemas";
|
||||||
|
|
||||||
|
export type TGetAcmeDirectoryResponse = z.infer<typeof GetAcmeDirectoryResponseSchema>;
|
||||||
|
export type TCreateAcmeAccountResponse = z.infer<typeof CreateAcmeAccountResponseSchema>;
|
||||||
|
export type TCreateAcmeOrderResponse = z.infer<typeof CreateAcmeOrderResponseSchema>;
|
||||||
|
export type TDeactivateAcmeAccountResponse = z.infer<typeof DeactivateAcmeAccountResponseSchema>;
|
||||||
|
export type TListAcmeOrdersResponse = z.infer<typeof ListAcmeOrdersResponseSchema>;
|
||||||
|
export type TGetAcmeOrderResponse = z.infer<typeof GetAcmeOrderResponseSchema>;
|
||||||
|
export type TFinalizeAcmeOrderResponse = z.infer<typeof FinalizeAcmeOrderResponseSchema>;
|
||||||
|
export type TDownloadAcmeCertificateDTO = string;
|
||||||
|
export type TGetAcmeAuthorizationResponse = z.infer<typeof GetAcmeAuthorizationResponseSchema>;
|
||||||
|
export type TRespondToAcmeChallengeResponse = z.infer<typeof RespondToAcmeChallengeResponseSchema>;
|
||||||
|
|
||||||
|
export type TPkiAcmeServiceFactory = {
|
||||||
|
getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>;
|
||||||
|
getAcmeNewNonce: (profileId: string) => Promise<string>;
|
||||||
|
createAcmeAccount: (profileId: string, body: unknown) => Promise<TCreateAcmeAccountResponse>;
|
||||||
|
createAcmeOrder: (profileId: string, body: unknown) => Promise<TCreateAcmeOrderResponse>;
|
||||||
|
deactivateAcmeAccount: (profileId: string, accountId: string) => Promise<TDeactivateAcmeAccountResponse>;
|
||||||
|
listAcmeOrders: (profileId: string, accountId: string) => Promise<TListAcmeOrdersResponse>;
|
||||||
|
getAcmeOrder: (profileId: string, orderId: string) => Promise<TGetAcmeOrderResponse>;
|
||||||
|
finalizeAcmeOrder: (profileId: string, orderId: string, csr: string) => Promise<TFinalizeAcmeOrderResponse>;
|
||||||
|
downloadAcmeCertificate: (profileId: string, orderId: string) => Promise<string>;
|
||||||
|
getAcmeAuthorization: (profileId: string, authzId: string) => Promise<TGetAcmeAuthorizationResponse>;
|
||||||
|
respondToAcmeChallenge: (profileId: string, authzId: string) => Promise<TRespondToAcmeChallengeResponse>;
|
||||||
|
};
|
||||||
@@ -25,6 +25,7 @@ import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/
|
|||||||
import { certificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { certificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
import { certificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
|
import { certificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
|
||||||
import { certificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
|
import { certificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
|
||||||
|
import { pkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-service";
|
||||||
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
|
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
|
||||||
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||||
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
|
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
|
||||||
@@ -1161,6 +1162,10 @@ export const registerRoutes = async (
|
|||||||
projectDAL
|
projectDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pkiAcmeService = pkiAcmeServiceFactory({
|
||||||
|
certificateProfileDAL
|
||||||
|
});
|
||||||
|
|
||||||
const pkiAlertService = pkiAlertServiceFactory({
|
const pkiAlertService = pkiAlertServiceFactory({
|
||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
@@ -2436,6 +2441,7 @@ export const registerRoutes = async (
|
|||||||
certificateProfile: certificateProfileService,
|
certificateProfile: certificateProfileService,
|
||||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||||
certificateEst: certificateEstService,
|
certificateEst: certificateEstService,
|
||||||
|
pkiAcme: pkiAcmeService,
|
||||||
pit: pitService,
|
pit: pitService,
|
||||||
pkiAlert: pkiAlertService,
|
pkiAlert: pkiAlertService,
|
||||||
pkiCollection: pkiCollectionService,
|
pkiCollection: pkiCollectionService,
|
||||||
|
|||||||
Reference in New Issue
Block a user