mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 22:27:22 +00:00
Merge pull request #3514 from Infisical/ENG-2685
feat(pki): Store Secret Key Alongside Certificate + Endpoints to Fetch PK / Cert Bundle
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.CertificateBody)) {
|
||||||
|
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
|
||||||
|
t.binary("encryptedCertificateChain").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateSecret))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateSecret, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("certId").notNullable().unique();
|
||||||
|
t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedPrivateKey").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.CertificateSecret)) {
|
||||||
|
await knex.schema.dropTable(TableName.CertificateSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.CertificateBody)) {
|
||||||
|
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
|
||||||
|
t.dropColumn("encryptedCertificateChain");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,8 @@ export const CertificateBodiesSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
certId: z.string().uuid(),
|
certId: z.string().uuid(),
|
||||||
encryptedCertificate: zodBuffer
|
encryptedCertificate: zodBuffer,
|
||||||
|
encryptedCertificateChain: zodBuffer.nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCertificateBodies = z.infer<typeof CertificateBodiesSchema>;
|
export type TCertificateBodies = z.infer<typeof CertificateBodiesSchema>;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const CertificateSecretsSchema = z.object({
|
export const CertificateSecretsSchema = z.object({
|
||||||
@@ -12,8 +14,7 @@ export const CertificateSecretsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
certId: z.string().uuid(),
|
certId: z.string().uuid(),
|
||||||
pk: z.string(),
|
encryptedPrivateKey: zodBuffer
|
||||||
sk: z.string()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCertificateSecrets = z.infer<typeof CertificateSecretsSchema>;
|
export type TCertificateSecrets = z.infer<typeof CertificateSecretsSchema>;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const ProjectsSchema = z.object({
|
|||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
enforceCapitalization: z.boolean().default(false),
|
enforceCapitalization: z.boolean().default(false),
|
||||||
hasDeleteProtection: z.boolean().default(true).nullable().optional()
|
hasDeleteProtection: z.boolean().default(false).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ export enum EventType {
|
|||||||
DELETE_CERT = "delete-cert",
|
DELETE_CERT = "delete-cert",
|
||||||
REVOKE_CERT = "revoke-cert",
|
REVOKE_CERT = "revoke-cert",
|
||||||
GET_CERT_BODY = "get-cert-body",
|
GET_CERT_BODY = "get-cert-body",
|
||||||
|
GET_CERT_PRIVATE_KEY = "get-cert-private-key",
|
||||||
|
GET_CERT_BUNDLE = "get-cert-bundle",
|
||||||
CREATE_PKI_ALERT = "create-pki-alert",
|
CREATE_PKI_ALERT = "create-pki-alert",
|
||||||
GET_PKI_ALERT = "get-pki-alert",
|
GET_PKI_ALERT = "get-pki-alert",
|
||||||
UPDATE_PKI_ALERT = "update-pki-alert",
|
UPDATE_PKI_ALERT = "update-pki-alert",
|
||||||
@@ -1790,6 +1792,24 @@ interface GetCertBody {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetCertPrivateKey {
|
||||||
|
type: EventType.GET_CERT_PRIVATE_KEY;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCertBundle {
|
||||||
|
type: EventType.GET_CERT_BUNDLE;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreatePkiAlert {
|
interface CreatePkiAlert {
|
||||||
type: EventType.CREATE_PKI_ALERT;
|
type: EventType.CREATE_PKI_ALERT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2824,6 +2844,8 @@ export type Event =
|
|||||||
| DeleteCert
|
| DeleteCert
|
||||||
| RevokeCert
|
| RevokeCert
|
||||||
| GetCertBody
|
| GetCertBody
|
||||||
|
| GetCertPrivateKey
|
||||||
|
| GetCertBundle
|
||||||
| CreatePkiAlert
|
| CreatePkiAlert
|
||||||
| GetPkiAlert
|
| GetPkiAlert
|
||||||
| UpdatePkiAlert
|
| UpdatePkiAlert
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ export enum ProjectPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionCertificateActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete",
|
||||||
|
ReadPrivateKey = "read-private-key"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSecretActions {
|
export enum ProjectPermissionSecretActions {
|
||||||
DescribeAndReadValue = "read",
|
DescribeAndReadValue = "read",
|
||||||
DescribeSecret = "describeSecret",
|
DescribeSecret = "describeSecret",
|
||||||
@@ -232,7 +240,7 @@ export type ProjectPermissionSet =
|
|||||||
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
||||||
]
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||||
@@ -478,7 +486,7 @@ const GeneralPermissionSchema = [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."),
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
@@ -688,7 +696,6 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionSub.AuditLogs,
|
ProjectPermissionSub.AuditLogs,
|
||||||
ProjectPermissionSub.IpAllowList,
|
ProjectPermissionSub.IpAllowList,
|
||||||
ProjectPermissionSub.CertificateAuthorities,
|
ProjectPermissionSub.CertificateAuthorities,
|
||||||
ProjectPermissionSub.Certificates,
|
|
||||||
ProjectPermissionSub.CertificateTemplates,
|
ProjectPermissionSub.CertificateTemplates,
|
||||||
ProjectPermissionSub.PkiAlerts,
|
ProjectPermissionSub.PkiAlerts,
|
||||||
ProjectPermissionSub.PkiCollections,
|
ProjectPermissionSub.PkiCollections,
|
||||||
@@ -708,6 +715,17 @@ const buildAdminPermissionRules = () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionCertificateActions.Read,
|
||||||
|
ProjectPermissionCertificateActions.Edit,
|
||||||
|
ProjectPermissionCertificateActions.Create,
|
||||||
|
ProjectPermissionCertificateActions.Delete,
|
||||||
|
ProjectPermissionCertificateActions.ReadPrivateKey
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionSshHostActions.Edit,
|
ProjectPermissionSshHostActions.Edit,
|
||||||
@@ -965,10 +983,10 @@ const buildMemberPermissionRules = () => {
|
|||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionCertificateActions.Read,
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionCertificateActions.Edit,
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionCertificateActions.Create,
|
||||||
ProjectPermissionActions.Delete
|
ProjectPermissionCertificateActions.Delete
|
||||||
],
|
],
|
||||||
ProjectPermissionSub.Certificates
|
ProjectPermissionSub.Certificates
|
||||||
);
|
);
|
||||||
@@ -1041,7 +1059,7 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
|
||||||
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||||
|
|||||||
@@ -1619,7 +1619,8 @@ export const CERTIFICATES = {
|
|||||||
serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.",
|
serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.",
|
||||||
certificate: "The certificate body of the certificate.",
|
certificate: "The certificate body of the certificate.",
|
||||||
certificateChain: "The certificate chain of the certificate.",
|
certificateChain: "The certificate chain of the certificate.",
|
||||||
serialNumberRes: "The serial number of the certificate."
|
serialNumberRes: "The serial number of the certificate.",
|
||||||
|
privateKey: "The private key of the certificate."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { FastifyReply } from "fastify";
|
||||||
|
|
||||||
|
export const addNoCacheHeaders = (reply: FastifyReply) => {
|
||||||
|
void reply.header("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
|
||||||
|
void reply.header("Pragma", "no-cache");
|
||||||
|
void reply.header("Expires", "0");
|
||||||
|
void reply.header("Surrogate-Control", "no-store");
|
||||||
|
};
|
||||||
@@ -126,6 +126,7 @@ import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
|||||||
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { certificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { certificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
import { certificateServiceFactory } from "@app/services/certificate/certificate-service";
|
import { certificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||||
import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
@@ -812,6 +813,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const certificateDAL = certificateDALFactory(db);
|
const certificateDAL = certificateDALFactory(db);
|
||||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||||
|
const certificateSecretDAL = certificateSecretDALFactory(db);
|
||||||
|
|
||||||
const pkiAlertDAL = pkiAlertDALFactory(db);
|
const pkiAlertDAL = pkiAlertDALFactory(db);
|
||||||
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
||||||
@@ -820,6 +822,7 @@ export const registerRoutes = async (
|
|||||||
const certificateService = certificateServiceFactory({
|
const certificateService = certificateServiceFactory({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
certificateAuthorityCrlDAL,
|
certificateAuthorityCrlDAL,
|
||||||
@@ -891,6 +894,7 @@ export const registerRoutes = async (
|
|||||||
certificateAuthorityQueue,
|
certificateAuthorityQueue,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
pkiCollectionItemDAL,
|
pkiCollectionItemDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { CertificatesSchema } from "@app/db/schemas";
|
import { CertificatesSchema } from "@app/db/schemas";
|
||||||
@@ -5,6 +6,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
|||||||
import { ApiDocsTags, CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
|
import { ApiDocsTags, CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { addNoCacheHeaders } from "@app/server/lib/caching";
|
||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@@ -64,6 +66,111 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best.
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:serialNumber/private-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
description: "Get certificate private key",
|
||||||
|
params: z.object({
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.string().trim()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, reply) => {
|
||||||
|
const { ca, cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CERT_PRIVATE_KEY,
|
||||||
|
metadata: {
|
||||||
|
certId: cert.id,
|
||||||
|
cn: cert.commonName,
|
||||||
|
serialNumber: cert.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addNoCacheHeaders(reply);
|
||||||
|
|
||||||
|
return certPrivateKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best.
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:serialNumber/bundle",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
description: "Get certificate bundle including the certificate, chain, and private key.",
|
||||||
|
params: z.object({
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
||||||
|
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||||
|
privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey),
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, reply) => {
|
||||||
|
const { certificate, certificateChain, serialNumber, cert, ca, privateKey } =
|
||||||
|
await server.services.certificate.getCertBundle({
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CERT_BUNDLE,
|
||||||
|
metadata: {
|
||||||
|
certId: cert.id,
|
||||||
|
cn: cert.commonName,
|
||||||
|
serialNumber: cert.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addNoCacheHeaders(reply);
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
serialNumber,
|
||||||
|
privateKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/issue-certificate",
|
url: "/issue-certificate",
|
||||||
@@ -411,7 +518,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
||||||
certificateChain: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain),
|
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||||
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -429,7 +536,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: ca.projectId,
|
projectId: ca.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.DELETE_CERT,
|
type: EventType.GET_CERT_BODY,
|
||||||
metadata: {
|
metadata: {
|
||||||
certId: cert.id,
|
certId: cert.id,
|
||||||
cn: cert.commonName,
|
cn: cert.commonName,
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
@@ -21,6 +25,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
|||||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
|
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||||
import {
|
import {
|
||||||
CertExtendedKeyUsage,
|
CertExtendedKeyUsage,
|
||||||
CertExtendedKeyUsageOIDToName,
|
CertExtendedKeyUsageOIDToName,
|
||||||
@@ -75,6 +80,7 @@ type TCertificateAuthorityServiceFactoryDep = {
|
|||||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById" | "find">;
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById" | "find">;
|
||||||
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||||
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
|
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
|
||||||
@@ -96,6 +102,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
certificateTemplateDAL,
|
certificateTemplateDAL,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
pkiCollectionItemDAL,
|
pkiCollectionItemDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -1157,7 +1164,10 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Create,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||||
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||||
@@ -1373,6 +1383,23 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||||
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||||
});
|
});
|
||||||
|
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(skLeaf)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||||
|
caCertId: caCert.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(certificateChainPem)
|
||||||
|
});
|
||||||
|
|
||||||
await certificateDAL.transaction(async (tx) => {
|
await certificateDAL.transaction(async (tx) => {
|
||||||
const cert = await certificateDAL.create(
|
const cert = await certificateDAL.create(
|
||||||
@@ -1396,7 +1423,16 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
await certificateBodyDAL.create(
|
await certificateBodyDAL.create(
|
||||||
{
|
{
|
||||||
certId: cert.id,
|
certId: cert.id,
|
||||||
encryptedCertificate
|
encryptedCertificate,
|
||||||
|
encryptedCertificateChain
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateSecretDAL.create(
|
||||||
|
{
|
||||||
|
certId: cert.id,
|
||||||
|
encryptedPrivateKey
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -1414,17 +1450,9 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
return cert;
|
return cert;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
|
||||||
caCertId: caCert.id,
|
|
||||||
certificateAuthorityDAL,
|
|
||||||
certificateAuthorityCertDAL,
|
|
||||||
projectDAL,
|
|
||||||
kmsService
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate: leafCert.toString("pem"),
|
certificate: leafCert.toString("pem"),
|
||||||
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
certificateChain: certificateChainPem,
|
||||||
issuingCaCertificate,
|
issuingCaCertificate,
|
||||||
privateKey: skLeaf,
|
privateKey: skLeaf,
|
||||||
serialNumber,
|
serialNumber,
|
||||||
@@ -1487,7 +1515,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionCertificateActions.Create,
|
||||||
ProjectPermissionSub.Certificates
|
ProjectPermissionSub.Certificates
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
import { CrlReason } from "./certificate-types";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
|
||||||
|
import { CrlReason, TBuildCertificateChainDTO, TGetCertificateCredentialsDTO } from "./certificate-types";
|
||||||
|
|
||||||
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
||||||
switch (crlReason) {
|
switch (crlReason) {
|
||||||
@@ -46,3 +51,73 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
|
|||||||
.map((cert) => cert.toString("pem"))
|
.map((cert) => cert.toString("pem"))
|
||||||
.join("\n")
|
.join("\n")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the public and private key of certificate
|
||||||
|
* Note: credentials are returned as PEM strings
|
||||||
|
*/
|
||||||
|
export const getCertificateCredentials = async ({
|
||||||
|
certId,
|
||||||
|
projectId,
|
||||||
|
certificateSecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
}: TGetCertificateCredentialsDTO) => {
|
||||||
|
const certificateSecret = await certificateSecretDAL.findOne({ certId });
|
||||||
|
if (!certificateSecret)
|
||||||
|
throw new NotFoundError({ message: `Certificate secret for certificate with ID '${certId}' not found` });
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
|
kmsId: keyId
|
||||||
|
});
|
||||||
|
const decryptedPrivateKey = await kmsDecryptor({
|
||||||
|
cipherTextBlob: certificateSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "pem", type: "pkcs8" });
|
||||||
|
const certPrivateKey = skObj.export({ format: "pem", type: "pkcs8" }).toString();
|
||||||
|
|
||||||
|
const pkObj = crypto.createPublicKey(skObj);
|
||||||
|
const certPublicKey = pkObj.export({ format: "pem", type: "spki" }).toString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificateSecret,
|
||||||
|
certPrivateKey,
|
||||||
|
certPublicKey
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body
|
||||||
|
// Otherwise we'll fallback to manually building the chain
|
||||||
|
export const buildCertificateChain = async ({
|
||||||
|
caCert,
|
||||||
|
caCertChain,
|
||||||
|
encryptedCertificateChain,
|
||||||
|
kmsService,
|
||||||
|
kmsId
|
||||||
|
}: TBuildCertificateChainDTO) => {
|
||||||
|
if (!encryptedCertificateChain && (!caCert || !caCertChain)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let certificateChain = `${caCert}\n${caCertChain}`.trim();
|
||||||
|
|
||||||
|
if (encryptedCertificateChain) {
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId });
|
||||||
|
const decryptedCertChain = await kmsDecryptor({
|
||||||
|
cipherTextBlob: encryptedCertificateChain
|
||||||
|
});
|
||||||
|
certificateChain = decryptedCertChain.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificateChain;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateSecretDALFactory = ReturnType<typeof certificateSecretDALFactory>;
|
||||||
|
|
||||||
|
export const certificateSecretDALFactory = (db: TDbClient) => {
|
||||||
|
const certSecretOrm = ormify(db, TableName.CertificateSecret);
|
||||||
|
return certSecretOrm;
|
||||||
|
};
|
||||||
@@ -4,7 +4,10 @@ import * as x509 from "@peculiar/x509";
|
|||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import {
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
@@ -15,11 +18,21 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
|||||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
|
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
|
||||||
import { revocationReasonToCrlCode } from "./certificate-fns";
|
import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns";
|
||||||
import { CertStatus, TDeleteCertDTO, TGetCertBodyDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types";
|
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
||||||
|
import {
|
||||||
|
CertStatus,
|
||||||
|
TDeleteCertDTO,
|
||||||
|
TGetCertBodyDTO,
|
||||||
|
TGetCertBundleDTO,
|
||||||
|
TGetCertDTO,
|
||||||
|
TGetCertPrivateKeyDTO,
|
||||||
|
TRevokeCertDTO
|
||||||
|
} from "./certificate-types";
|
||||||
|
|
||||||
type TCertificateServiceFactoryDep = {
|
type TCertificateServiceFactoryDep = {
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
|
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||||
@@ -34,6 +47,7 @@ export type TCertificateServiceFactory = ReturnType<typeof certificateServiceFac
|
|||||||
|
|
||||||
export const certificateServiceFactory = ({
|
export const certificateServiceFactory = ({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
@@ -59,7 +73,10 @@ export const certificateServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Read,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cert,
|
cert,
|
||||||
@@ -67,6 +84,48 @@ export const certificateServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get certificate private key.
|
||||||
|
*/
|
||||||
|
const getCertPrivateKey = async ({
|
||||||
|
serialNumber,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TGetCertPrivateKeyDTO) => {
|
||||||
|
const cert = await certificateDAL.findOne({ serialNumber });
|
||||||
|
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
|
const { certPrivateKey } = await getCertificateCredentials({
|
||||||
|
certId: cert.id,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
certificateSecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca,
|
||||||
|
cert,
|
||||||
|
certPrivateKey
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete certificate with serial number [serialNumber]
|
* Delete certificate with serial number [serialNumber]
|
||||||
*/
|
*/
|
||||||
@@ -83,7 +142,10 @@ export const certificateServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Delete,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
const deletedCert = await certificateDAL.deleteById(cert.id);
|
const deletedCert = await certificateDAL.deleteById(cert.id);
|
||||||
|
|
||||||
@@ -118,7 +180,10 @@ export const certificateServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Delete,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked");
|
if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked");
|
||||||
|
|
||||||
@@ -165,7 +230,10 @@ export const certificateServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Read,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||||
|
|
||||||
@@ -192,19 +260,107 @@ export const certificateServiceFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const certificateChain = await buildCertificateChain({
|
||||||
|
caCert,
|
||||||
|
caCertChain,
|
||||||
|
kmsId: certificateManagerKeyId,
|
||||||
|
kmsService,
|
||||||
|
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate: certObj.toString("pem"),
|
certificate: certObj.toString("pem"),
|
||||||
certificateChain: `${caCert}\n${caCertChain}`.trim(),
|
certificateChain,
|
||||||
serialNumber: certObj.serialNumber,
|
serialNumber: certObj.serialNumber,
|
||||||
cert,
|
cert,
|
||||||
ca
|
ca
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return certificate body and certificate chain for certificate with
|
||||||
|
* serial number [serialNumber]
|
||||||
|
*/
|
||||||
|
const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => {
|
||||||
|
const cert = await certificateDAL.findOne({ serialNumber });
|
||||||
|
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Read,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
|
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||||
|
|
||||||
|
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKeyId
|
||||||
|
});
|
||||||
|
const decryptedCert = await kmsDecryptor({
|
||||||
|
cipherTextBlob: certBody.encryptedCertificate
|
||||||
|
});
|
||||||
|
|
||||||
|
const certObj = new x509.X509Certificate(decryptedCert);
|
||||||
|
const certificate = certObj.toString("pem");
|
||||||
|
|
||||||
|
const { caCert, caCertChain } = await getCaCertChain({
|
||||||
|
caCertId: cert.caCertId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateChain = await buildCertificateChain({
|
||||||
|
caCert,
|
||||||
|
caCertChain,
|
||||||
|
kmsId: certificateManagerKeyId,
|
||||||
|
kmsService,
|
||||||
|
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const { certPrivateKey } = await getCertificateCredentials({
|
||||||
|
certId: cert.id,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
certificateSecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
privateKey: certPrivateKey,
|
||||||
|
serialNumber,
|
||||||
|
cert,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getCert,
|
getCert,
|
||||||
|
getCertPrivateKey,
|
||||||
deleteCert,
|
deleteCert,
|
||||||
revokeCert,
|
revokeCert,
|
||||||
getCertBody
|
getCertBody,
|
||||||
|
getCertBundle
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import * as x509 from "@peculiar/x509";
|
|||||||
|
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
||||||
|
|
||||||
export enum CertStatus {
|
export enum CertStatus {
|
||||||
ACTIVE = "active",
|
ACTIVE = "active",
|
||||||
REVOKED = "revoked"
|
REVOKED = "revoked"
|
||||||
@@ -73,3 +77,27 @@ export type TRevokeCertDTO = {
|
|||||||
export type TGetCertBodyDTO = {
|
export type TGetCertBodyDTO = {
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCertPrivateKeyDTO = {
|
||||||
|
serialNumber: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCertBundleDTO = {
|
||||||
|
serialNumber: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCertificateCredentialsDTO = {
|
||||||
|
certId: string;
|
||||||
|
projectId: string;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBuildCertificateChainDTO = {
|
||||||
|
caCert?: string;
|
||||||
|
caCertChain?: string;
|
||||||
|
encryptedCertificateChain?: Buffer;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey">;
|
||||||
|
kmsId: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/servi
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSshHostActions,
|
ProjectPermissionSshHostActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
@@ -948,7 +949,10 @@ export const projectServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Read,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
const cas = await certificateAuthorityDAL.find({ projectId });
|
const cas = await certificateAuthorityDAL.find({ projectId });
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: "Get Certificate Bundle"
|
||||||
|
openapi: "GET /api/v2/workspace/{slug}/bundle"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
You must have the certificate `read-private-key` permission in order to call this endpoint.
|
||||||
|
</Note>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Certificate Private Key"
|
||||||
|
openapi: "GET /api/v2/workspace/{slug}/private-key"
|
||||||
|
---
|
||||||
@@ -252,11 +252,12 @@ Supports conditions and permission inversion
|
|||||||
|
|
||||||
#### Subject: `certificates`
|
#### Subject: `certificates`
|
||||||
|
|
||||||
| Action | Description |
|
| Action | Description |
|
||||||
| -------- | ----------------------------- |
|
| -------------------- | ----------------------------- |
|
||||||
| `read` | View certificates |
|
| `read` | View certificates |
|
||||||
| `create` | Issue new certificates |
|
| `read-private-key` | Read certificate private key |
|
||||||
| `delete` | Revoke or remove certificates |
|
| `create` | Issue new certificates |
|
||||||
|
| `delete` | Revoke or remove certificates |
|
||||||
|
|
||||||
#### Subject: `certificate-templates`
|
#### Subject: `certificate-templates`
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { useProjectPermission } from "./ProjectPermissionContext";
|
|||||||
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
||||||
export {
|
export {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
ProjectPermissionGroupActions,
|
ProjectPermissionGroupActions,
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ export enum ProjectPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionCertificateActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete",
|
||||||
|
ReadPrivateKey = "read-private-key"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSecretActions {
|
export enum ProjectPermissionSecretActions {
|
||||||
DescribeAndReadValue = "read",
|
DescribeAndReadValue = "read",
|
||||||
DescribeSecret = "describeSecret",
|
DescribeSecret = "describeSecret",
|
||||||
@@ -268,7 +276,7 @@ export type ProjectPermissionSet =
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export {
|
|||||||
export type { TProjectPermission } from "./ProjectPermissionContext";
|
export type { TProjectPermission } from "./ProjectPermissionContext";
|
||||||
export {
|
export {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
ProjectPermissionGroupActions,
|
ProjectPermissionGroupActions,
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
[EventType.DELETE_CERT]: "Delete certificate",
|
[EventType.DELETE_CERT]: "Delete certificate",
|
||||||
[EventType.REVOKE_CERT]: "Revoke certificate",
|
[EventType.REVOKE_CERT]: "Revoke certificate",
|
||||||
[EventType.GET_CERT_BODY]: "Get certificate body",
|
[EventType.GET_CERT_BODY]: "Get certificate body",
|
||||||
|
[EventType.GET_CERT_PRIVATE_KEY]: "Get certificate private key",
|
||||||
|
[EventType.GET_CERT_BUNDLE]: "Get certificate bundle",
|
||||||
[EventType.CREATE_PKI_ALERT]: "Create PKI alert",
|
[EventType.CREATE_PKI_ALERT]: "Create PKI alert",
|
||||||
[EventType.GET_PKI_ALERT]: "Get PKI alert",
|
[EventType.GET_PKI_ALERT]: "Get PKI alert",
|
||||||
[EventType.UPDATE_PKI_ALERT]: "Update PKI alert",
|
[EventType.UPDATE_PKI_ALERT]: "Update PKI alert",
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ export enum EventType {
|
|||||||
DELETE_CERT = "delete-cert",
|
DELETE_CERT = "delete-cert",
|
||||||
REVOKE_CERT = "revoke-cert",
|
REVOKE_CERT = "revoke-cert",
|
||||||
GET_CERT_BODY = "get-cert-body",
|
GET_CERT_BODY = "get-cert-body",
|
||||||
|
GET_CERT_PRIVATE_KEY = "get-cert-private-key",
|
||||||
|
GET_CERT_BUNDLE = "get-cert-bundle",
|
||||||
CREATE_PKI_ALERT = "create-pki-alert",
|
CREATE_PKI_ALERT = "create-pki-alert",
|
||||||
GET_PKI_ALERT = "get-pki-alert",
|
GET_PKI_ALERT = "get-pki-alert",
|
||||||
UPDATE_PKI_ALERT = "update-pki-alert",
|
UPDATE_PKI_ALERT = "update-pki-alert",
|
||||||
|
|||||||
@@ -620,6 +620,24 @@ interface GetCertBody {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetCertPrivateKey {
|
||||||
|
type: EventType.GET_CERT_PRIVATE_KEY;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCertBundle {
|
||||||
|
type: EventType.GET_CERT_BUNDLE;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreatePkiAlert {
|
interface CreatePkiAlert {
|
||||||
type: EventType.CREATE_PKI_ALERT;
|
type: EventType.CREATE_PKI_ALERT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -881,6 +899,8 @@ export type Event =
|
|||||||
| DeleteCert
|
| DeleteCert
|
||||||
| RevokeCert
|
| RevokeCert
|
||||||
| GetCertBody
|
| GetCertBody
|
||||||
|
| GetCertPrivateKey
|
||||||
|
| GetCertBundle
|
||||||
| CreatePkiAlert
|
| CreatePkiAlert
|
||||||
| GetPkiAlert
|
| GetPkiAlert
|
||||||
| UpdatePkiAlert
|
| UpdatePkiAlert
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { TCertificate } from "./types";
|
|||||||
|
|
||||||
export const certKeys = {
|
export const certKeys = {
|
||||||
getCertById: (serialNumber: string) => [{ serialNumber }, "cert"],
|
getCertById: (serialNumber: string) => [{ serialNumber }, "cert"],
|
||||||
getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"]
|
getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"],
|
||||||
|
getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetCert = (serialNumber: string) => {
|
export const useGetCert = (serialNumber: string) => {
|
||||||
@@ -38,3 +39,19 @@ export const useGetCertBody = (serialNumber: string) => {
|
|||||||
enabled: Boolean(serialNumber)
|
enabled: Boolean(serialNumber)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetCertBundle = (serialNumber: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: certKeys.getCertBundle(serialNumber),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
certificate: string;
|
||||||
|
certificateChain: string;
|
||||||
|
serialNumber: string;
|
||||||
|
privateKey: string;
|
||||||
|
}>(`/api/v1/pki/certificates/${serialNumber}/bundle`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: Boolean(serialNumber)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { PageHeader } from "@app/components/v2";
|
import { PageHeader } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission
|
||||||
|
} from "@app/context";
|
||||||
|
|
||||||
import { PkiCollectionSection } from "../AlertingPage/components";
|
import { PkiCollectionSection } from "../AlertingPage/components";
|
||||||
import { CertificatesSection } from "./components";
|
import { CertificatesSection } from "./components";
|
||||||
@@ -17,7 +22,7 @@ export const CertificatesPage = () => {
|
|||||||
ProjectPermissionSub.PkiCollections
|
ProjectPermissionSub.PkiCollections
|
||||||
);
|
);
|
||||||
const canAccessCerts = permission.can(
|
const canAccessCerts = permission.can(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionCertificateActions.Read,
|
||||||
ProjectPermissionSub.Certificates
|
ProjectPermissionSub.Certificates
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -40,7 +45,7 @@ export const CertificatesPage = () => {
|
|||||||
)}
|
)}
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
renderGuardBanner
|
renderGuardBanner
|
||||||
I={ProjectPermissionActions.Read}
|
I={ProjectPermissionCertificateActions.Read}
|
||||||
a={ProjectPermissionSub.Certificates}
|
a={ProjectPermissionSub.Certificates}
|
||||||
>
|
>
|
||||||
<CertificatesSection />
|
<CertificatesSection />
|
||||||
|
|||||||
+28
-2
@@ -1,5 +1,11 @@
|
|||||||
import { Modal, ModalContent } from "@app/components/v2";
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission
|
||||||
|
} from "@app/context";
|
||||||
import { useGetCertBody } from "@app/hooks/api";
|
import { useGetCertBody } from "@app/hooks/api";
|
||||||
|
import { useGetCertBundle } from "@app/hooks/api/certificates/queries";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { CertificateContent } from "./CertificateContent";
|
import { CertificateContent } from "./CertificateContent";
|
||||||
@@ -10,10 +16,29 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
const { data } = useGetCertBody(
|
const { permission } = useProjectPermission();
|
||||||
(popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || ""
|
|
||||||
|
const serialNumber =
|
||||||
|
(popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || "";
|
||||||
|
|
||||||
|
const canReadPrivateKey = permission.can(
|
||||||
|
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// useGetCertBundle fails unless user has the correct permissions
|
||||||
|
const { data: bundleData } = useGetCertBundle(serialNumber);
|
||||||
|
const { data: bodyData } = useGetCertBody(serialNumber);
|
||||||
|
|
||||||
|
const data:
|
||||||
|
| {
|
||||||
|
certificate: string;
|
||||||
|
certificateChain: string;
|
||||||
|
serialNumber: string;
|
||||||
|
privateKey?: string;
|
||||||
|
}
|
||||||
|
| undefined = canReadPrivateKey ? bundleData : bodyData;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.certificateCert?.isOpen}
|
isOpen={popUp?.certificateCert?.isOpen}
|
||||||
@@ -27,6 +52,7 @@ export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
serialNumber={data.serialNumber}
|
serialNumber={data.serialNumber}
|
||||||
certificate={data.certificate}
|
certificate={data.certificate}
|
||||||
certificateChain={data.certificateChain}
|
certificateChain={data.certificateChain}
|
||||||
|
privateKey={data.privateKey}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div />
|
<div />
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import {
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
import { useDeleteCert } from "@app/hooks/api";
|
import { useDeleteCert } from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
@@ -50,7 +54,7 @@ export const CertificatesSection = () => {
|
|||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Certificates</p>
|
<p className="text-xl font-semibold text-mineshaft-100">Certificates</p>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionCertificateActions.Create}
|
||||||
a={ProjectPermissionSub.Certificates}
|
a={ProjectPermissionSub.Certificates}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import {
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
import { useListWorkspaceCertificates } from "@app/hooks/api";
|
import { useListWorkspaceCertificates } from "@app/hooks/api";
|
||||||
import { CertStatus } from "@app/hooks/api/certificates/enums";
|
import { CertStatus } from "@app/hooks/api/certificates/enums";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
@@ -110,7 +114,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Read}
|
I={ProjectPermissionCertificateActions.Read}
|
||||||
a={ProjectPermissionSub.Certificates}
|
a={ProjectPermissionSub.Certificates}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
@@ -131,7 +135,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Read}
|
I={ProjectPermissionCertificateActions.Read}
|
||||||
a={ProjectPermissionSub.Certificates}
|
a={ProjectPermissionSub.Certificates}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
@@ -152,7 +156,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionCertificateActions.Delete}
|
||||||
a={ProjectPermissionSub.Certificates}
|
a={ProjectPermissionSub.Certificates}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
@@ -173,7 +177,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionCertificateActions.Delete}
|
||||||
a={ProjectPermissionSub.Certificates}
|
a={ProjectPermissionSub.Certificates}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
|
|||||||
+2
-2
@@ -114,7 +114,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
|||||||
<div
|
<div
|
||||||
key={el.id}
|
key={el.id}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"relative bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md",
|
"relative bg-mineshaft-800 p-5 pr-10 first:rounded-t-md last:rounded-b-md",
|
||||||
dragOverItem === rootIndex ? "border-2 border-blue-400" : "",
|
dragOverItem === rootIndex ? "border-2 border-blue-400" : "",
|
||||||
draggedItem === rootIndex ? "opacity-50" : ""
|
draggedItem === rootIndex ? "opacity-50" : ""
|
||||||
)}
|
)}
|
||||||
@@ -179,7 +179,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex text-gray-300">
|
<div className="flex gap-4 text-gray-300">
|
||||||
<div className="w-1/4">Actions</div>
|
<div className="w-1/4">Actions</div>
|
||||||
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
||||||
{actions.map(({ label, value }, index) => {
|
{actions.map(({ label, value }, index) => {
|
||||||
|
|||||||
+34
-6
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|||||||
import { Tooltip } from "@app/components/v2";
|
import { Tooltip } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
@@ -32,6 +33,14 @@ const GeneralPolicyActionSchema = z.object({
|
|||||||
create: z.boolean().optional()
|
create: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const CertificatePolicyActionSchema = z.object({
|
||||||
|
[ProjectPermissionCertificateActions.Create]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionCertificateActions.Delete]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionCertificateActions.Edit]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionCertificateActions.Read]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionCertificateActions.ReadPrivateKey]: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
const SecretPolicyActionSchema = z.object({
|
const SecretPolicyActionSchema = z.object({
|
||||||
[ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value
|
[ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value
|
||||||
[ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(),
|
[ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(),
|
||||||
@@ -219,7 +228,7 @@ export const projectRoleFormSchema = z.object({
|
|||||||
[ProjectPermissionSub.AuditLogs]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.AuditLogs]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Certificates]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
||||||
@@ -371,7 +380,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
ProjectPermissionSub.AuditLogs,
|
ProjectPermissionSub.AuditLogs,
|
||||||
ProjectPermissionSub.IpAllowList,
|
ProjectPermissionSub.IpAllowList,
|
||||||
ProjectPermissionSub.CertificateAuthorities,
|
ProjectPermissionSub.CertificateAuthorities,
|
||||||
ProjectPermissionSub.Certificates,
|
|
||||||
ProjectPermissionSub.PkiAlerts,
|
ProjectPermissionSub.PkiAlerts,
|
||||||
ProjectPermissionSub.PkiCollections,
|
ProjectPermissionSub.PkiCollections,
|
||||||
ProjectPermissionSub.CertificateTemplates,
|
ProjectPermissionSub.CertificateTemplates,
|
||||||
@@ -507,6 +515,25 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subject === ProjectPermissionSub.Certificates) {
|
||||||
|
const canRead = action.includes(ProjectPermissionCertificateActions.Read);
|
||||||
|
const canEdit = action.includes(ProjectPermissionCertificateActions.Edit);
|
||||||
|
const canDelete = action.includes(ProjectPermissionCertificateActions.Delete);
|
||||||
|
const canCreate = action.includes(ProjectPermissionCertificateActions.Create);
|
||||||
|
const canReadPrivateKey = action.includes(ProjectPermissionCertificateActions.ReadPrivateKey);
|
||||||
|
|
||||||
|
if (!formVal[subject]) formVal[subject] = [{}];
|
||||||
|
|
||||||
|
// from above statement we are sure it won't be undefined
|
||||||
|
if (canRead) formVal[subject]![0].read = true;
|
||||||
|
if (canEdit) formVal[subject]![0].edit = true;
|
||||||
|
if (canCreate) formVal[subject]![0].create = true;
|
||||||
|
if (canDelete) formVal[subject]![0].delete = true;
|
||||||
|
if (canReadPrivateKey)
|
||||||
|
formVal[subject]![0][ProjectPermissionCertificateActions.ReadPrivateKey] = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (subject === ProjectPermissionSub.Project) {
|
if (subject === ProjectPermissionSub.Project) {
|
||||||
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
||||||
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
||||||
@@ -1014,10 +1041,11 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
[ProjectPermissionSub.Certificates]: {
|
[ProjectPermissionSub.Certificates]: {
|
||||||
title: "Certificates",
|
title: "Certificates",
|
||||||
actions: [
|
actions: [
|
||||||
{ label: "Read", value: "read" },
|
{ label: "Read", value: ProjectPermissionCertificateActions.Read },
|
||||||
{ label: "Create", value: "create" },
|
{ label: "Read Private Key", value: ProjectPermissionCertificateActions.ReadPrivateKey },
|
||||||
{ label: "Modify", value: "edit" },
|
{ label: "Create", value: ProjectPermissionCertificateActions.Create },
|
||||||
{ label: "Remove", value: "delete" }
|
{ label: "Modify", value: ProjectPermissionCertificateActions.Edit },
|
||||||
|
{ label: "Remove", value: ProjectPermissionCertificateActions.Delete }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
[ProjectPermissionSub.CertificateTemplates]: {
|
[ProjectPermissionSub.CertificateTemplates]: {
|
||||||
|
|||||||
Reference in New Issue
Block a user