mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Continue cert mgmt mvp
This commit is contained in:
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -29,6 +29,7 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||
@@ -133,6 +134,7 @@ declare module "fastify" {
|
||||
ldap: TLdapConfigServiceFactory;
|
||||
auditLog: TAuditLogServiceFactory;
|
||||
auditLogStream: TAuditLogStreamServiceFactory;
|
||||
certificate: TCertificateServiceFactory;
|
||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||
secretScanning: TSecretScanningServiceFactory;
|
||||
license: TLicenseServiceFactory;
|
||||
|
||||
20
backend/src/@types/knex.d.ts
vendored
20
backend/src/@types/knex.d.ts
vendored
@@ -41,6 +41,15 @@ import {
|
||||
TCertificateAuthoritySk,
|
||||
TCertificateAuthoritySkInsert,
|
||||
TCertificateAuthoritySkUpdate,
|
||||
TCertificateCerts,
|
||||
TCertificateCertsInsert,
|
||||
TCertificateCertsUpdate,
|
||||
TCertificates,
|
||||
TCertificateSecrets,
|
||||
TCertificateSecretsInsert,
|
||||
TCertificateSecretsUpdate,
|
||||
TCertificatesInsert,
|
||||
TCertificatesUpdate,
|
||||
TDynamicSecretLeases,
|
||||
TDynamicSecretLeasesInsert,
|
||||
TDynamicSecretLeasesUpdate,
|
||||
@@ -264,6 +273,17 @@ declare module "knex/types/tables" {
|
||||
TCertificateAuthoritySkInsert,
|
||||
TCertificateAuthoritySkUpdate
|
||||
>;
|
||||
[TableName.Certificate]: Knex.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
|
||||
[TableName.CertificateCert]: Knex.CompositeTableType<
|
||||
TCertificateCerts,
|
||||
TCertificateCertsInsert,
|
||||
TCertificateCertsUpdate
|
||||
>;
|
||||
[TableName.CertificateSecret]: Knex.CompositeTableType<
|
||||
TCertificateSecrets,
|
||||
TCertificateSecretsInsert,
|
||||
TCertificateSecretsUpdate
|
||||
>;
|
||||
[TableName.UserGroupMembership]: Knex.CompositeTableType<
|
||||
TUserGroupMembership,
|
||||
TUserGroupMembershipInsert,
|
||||
|
||||
@@ -14,11 +14,23 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.string("type").notNullable(); // root / intermediate
|
||||
t.string("status").notNullable(); // active / pending-certificate
|
||||
t.string("organization").notNullable();
|
||||
t.string("ou").notNullable();
|
||||
t.string("country").notNullable();
|
||||
t.string("province").notNullable();
|
||||
t.string("locality").notNullable();
|
||||
t.string("commonName").notNullable();
|
||||
t.string("dn").notNullable();
|
||||
t.unique(["dn", "projectId"]);
|
||||
t.integer("maxPathLength").nullable();
|
||||
t.datetime("notBefore").nullable();
|
||||
t.datetime("notAfter").nullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.CertificateAuthorityCert))) {
|
||||
// table to keep track of certificates belonging to CA
|
||||
await knex.schema.createTable(TableName.CertificateAuthorityCert, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
@@ -41,12 +53,61 @@ export async function up(knex: Knex): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.Certificate))) {
|
||||
// TODO: consider adding name
|
||||
await knex.schema.createTable(TableName.Certificate, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("caId").notNullable();
|
||||
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||
t.string("commonName").notNullable();
|
||||
t.datetime("notBefore").notNullable();
|
||||
t.datetime("notAfter").notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.CertificateCert))) {
|
||||
await knex.schema.createTable(TableName.CertificateCert, (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.text("certificate").notNullable(); // TODO: encrypt
|
||||
t.text("certificateChain").notNullable(); // TODO: encrypt
|
||||
});
|
||||
}
|
||||
|
||||
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.text("pk").notNullable(); // TODO: encrypt
|
||||
t.text("sk").notNullable(); // TODO: encrypt
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateAuthority);
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateAuthorityCert);
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateAuthoritySk);
|
||||
await createOnUpdateTrigger(knex, TableName.Certificate);
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateCert);
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateSecret);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
// certificates
|
||||
await knex.schema.dropTableIfExists(TableName.CertificateSecret);
|
||||
await dropOnUpdateTrigger(knex, TableName.CertificateSecret);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.CertificateCert);
|
||||
await dropOnUpdateTrigger(knex, TableName.CertificateCert);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.Certificate);
|
||||
await dropOnUpdateTrigger(knex, TableName.Certificate);
|
||||
|
||||
// certificate authorities
|
||||
await knex.schema.dropTableIfExists(TableName.CertificateAuthoritySk);
|
||||
await dropOnUpdateTrigger(knex, TableName.CertificateAuthoritySk);
|
||||
|
||||
|
||||
@@ -14,7 +14,17 @@ export const CertificateAuthoritiesSchema = z.object({
|
||||
parentCaId: z.string().uuid().nullable().optional(),
|
||||
projectId: z.string(),
|
||||
type: z.string(),
|
||||
dn: z.string()
|
||||
status: z.string(),
|
||||
organization: z.string(),
|
||||
ou: z.string(),
|
||||
country: z.string(),
|
||||
province: z.string(),
|
||||
locality: z.string(),
|
||||
commonName: z.string(),
|
||||
dn: z.string(),
|
||||
maxPathLength: z.number().nullable().optional(),
|
||||
notBefore: z.date().nullable().optional(),
|
||||
notAfter: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
|
||||
|
||||
21
backend/src/db/schemas/certificate-certs.ts
Normal file
21
backend/src/db/schemas/certificate-certs.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const CertificateCertsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
certId: z.string().uuid(),
|
||||
certificate: z.string(),
|
||||
certificateChain: z.string()
|
||||
});
|
||||
|
||||
export type TCertificateCerts = z.infer<typeof CertificateCertsSchema>;
|
||||
export type TCertificateCertsInsert = Omit<z.input<typeof CertificateCertsSchema>, TImmutableDBKeys>;
|
||||
export type TCertificateCertsUpdate = Partial<Omit<z.input<typeof CertificateCertsSchema>, TImmutableDBKeys>>;
|
||||
21
backend/src/db/schemas/certificate-secrets.ts
Normal file
21
backend/src/db/schemas/certificate-secrets.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const CertificateSecretsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
certId: z.string().uuid(),
|
||||
pk: z.string(),
|
||||
sk: z.string()
|
||||
});
|
||||
|
||||
export type TCertificateSecrets = z.infer<typeof CertificateSecretsSchema>;
|
||||
export type TCertificateSecretsInsert = Omit<z.input<typeof CertificateSecretsSchema>, TImmutableDBKeys>;
|
||||
export type TCertificateSecretsUpdate = Partial<Omit<z.input<typeof CertificateSecretsSchema>, TImmutableDBKeys>>;
|
||||
22
backend/src/db/schemas/certificates.ts
Normal file
22
backend/src/db/schemas/certificates.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const CertificatesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
caId: z.string().uuid(),
|
||||
commonName: z.string(),
|
||||
notBefore: z.date(),
|
||||
notAfter: z.date()
|
||||
});
|
||||
|
||||
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||
export type TCertificatesInsert = Omit<z.input<typeof CertificatesSchema>, TImmutableDBKeys>;
|
||||
export type TCertificatesUpdate = Partial<Omit<z.input<typeof CertificatesSchema>, TImmutableDBKeys>>;
|
||||
@@ -11,6 +11,9 @@ export * from "./backup-private-key";
|
||||
export * from "./certificate-authorities";
|
||||
export * from "./certificate-authority-certs";
|
||||
export * from "./certificate-authority-sk";
|
||||
export * from "./certificate-certs";
|
||||
export * from "./certificate-secrets";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
export * from "./git-app-install-sessions";
|
||||
|
||||
@@ -5,6 +5,9 @@ export enum TableName {
|
||||
CertificateAuthority = "certificate_authorities",
|
||||
CertificateAuthorityCert = "certificate_authority_certs",
|
||||
CertificateAuthoritySk = "certificate_authority_sk",
|
||||
Certificate = "certificates",
|
||||
CertificateCert = "certificate_certs",
|
||||
CertificateSecret = "certificate_secrets",
|
||||
Groups = "groups",
|
||||
GroupProjectMembership = "group_project_memberships",
|
||||
GroupProjectMembershipRole = "group_project_membership_roles",
|
||||
|
||||
@@ -26,7 +26,9 @@ export enum ProjectPermissionSub {
|
||||
SecretRollback = "secret-rollback",
|
||||
SecretApproval = "secret-approval",
|
||||
SecretRotation = "secret-rotation",
|
||||
Identity = "identity"
|
||||
Identity = "identity",
|
||||
CertificateAuthorities = "certificate-authorities",
|
||||
Certificates = "certificates"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
@@ -53,6 +55,8 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
@@ -139,6 +143,17 @@ const buildAdminPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
|
||||
|
||||
// double check if all CRUD are needed for CA and Certificates
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||
|
||||
@@ -205,6 +220,14 @@ const buildMemberPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||
|
||||
// double check if all CRUD are needed for CA and Certificates
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -229,6 +252,8 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -71,8 +71,11 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv
|
||||
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
||||
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
||||
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { certificateCertDALFactory } from "@app/services/certificate/certificate-cert-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 { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
// ca / certs
|
||||
import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { certificateAuthoritySkDALFactory } from "@app/services/certificate-authority/certificate-authority-sk-dal";
|
||||
@@ -226,16 +229,6 @@ export const registerRoutes = async (
|
||||
const trustedIpDAL = trustedIpDALFactory(db);
|
||||
const telemetryDAL = telemetryDALFactory(db);
|
||||
|
||||
const certificateAuthorityDAL = certificateAuthorityDALFactory(db);
|
||||
const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db);
|
||||
const certificateAuthoritySkDAL = certificateAuthoritySkDALFactory(db);
|
||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySkDAL,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
// ee db layer ops
|
||||
const permissionDAL = permissionDALFactory(db);
|
||||
const samlConfigDAL = samlConfigDALFactory(db);
|
||||
@@ -503,6 +496,34 @@ export const registerRoutes = async (
|
||||
projectUserMembershipRoleDAL
|
||||
});
|
||||
|
||||
const certificateAuthorityDAL = certificateAuthorityDALFactory(db);
|
||||
const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db);
|
||||
const certificateAuthoritySkDAL = certificateAuthoritySkDALFactory(db);
|
||||
|
||||
const certificateDAL = certificateDALFactory(db);
|
||||
const certificateCertDAL = certificateCertDALFactory(db);
|
||||
const certificateSecretDAL = certificateSecretDALFactory(db);
|
||||
|
||||
const certificateService = certificateServiceFactory({
|
||||
certificateDAL,
|
||||
certificateCertDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySkDAL,
|
||||
certificateDAL,
|
||||
certificateCertDAL,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const projectService = projectServiceFactory({
|
||||
permissionService,
|
||||
projectDAL,
|
||||
@@ -520,6 +541,7 @@ export const registerRoutes = async (
|
||||
folderDAL,
|
||||
licenseService,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
identityProjectMembershipRoleDAL,
|
||||
keyStore
|
||||
@@ -842,6 +864,7 @@ export const registerRoutes = async (
|
||||
ldap: ldapService,
|
||||
auditLog: auditLogService,
|
||||
auditLogStream: auditLogStreamService,
|
||||
certificate: certificateService,
|
||||
certificateAuthority: certificateAuthorityService,
|
||||
secretScanning: secretScanningService,
|
||||
license: licenseService,
|
||||
|
||||
@@ -4,7 +4,8 @@ import { CertificateAuthoritiesSchema } from "@app/db/schemas";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CAType } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
|
||||
export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -16,18 +17,38 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Create CA",
|
||||
body: z.object({
|
||||
projectSlug: z.string().trim(),
|
||||
type: z.enum([CAType.ROOT, CAType.INTERMEDIATE]),
|
||||
commonName: z.string().trim(),
|
||||
organization: z.string().trim(),
|
||||
ou: z.string().trim(),
|
||||
country: z.string().trim(),
|
||||
province: z.string().trim(),
|
||||
locality: z.string().trim()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
projectSlug: z.string().trim(),
|
||||
type: z.enum([CaType.ROOT, CaType.INTERMEDIATE]),
|
||||
commonName: z.string().trim(),
|
||||
organization: z.string().trim(),
|
||||
ou: z.string().trim(),
|
||||
country: z.string().trim(),
|
||||
province: z.string().trim(),
|
||||
locality: z.string().trim(),
|
||||
// format: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format
|
||||
notBefore: validateCaDateField.optional(),
|
||||
notAfter: validateCaDateField.optional(),
|
||||
maxPathLength: z.number().min(-1).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// Check that at least one of the specified fields is non-empty
|
||||
return [data.commonName, data.organization, data.ou, data.country, data.province, data.locality].some(
|
||||
(field) => field !== ""
|
||||
);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"At least one of the fields commonName, organization, ou, country, province, or locality must be non-empty",
|
||||
path: []
|
||||
}
|
||||
),
|
||||
response: {
|
||||
200: CertificateAuthoritiesSchema
|
||||
200: z.object({
|
||||
ca: CertificateAuthoritiesSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
@@ -38,7 +59,109 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
return ca;
|
||||
return {
|
||||
ca
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
ca: CertificateAuthoritiesSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const ca = await server.services.certificateAuthority.getCaById({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return {
|
||||
ca
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:caId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Update CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
ca: CertificateAuthoritiesSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const ca = await server.services.certificateAuthority.updateCaById({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
return {
|
||||
ca
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:caId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Delete CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
ca: CertificateAuthoritiesSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const ca = await server.services.certificateAuthority.deleteCaById({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return {
|
||||
ca
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -76,7 +199,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: ":caId/certificate",
|
||||
url: "/:caId/certificate",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
@@ -89,12 +212,13 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string(),
|
||||
certificateChain: z.string()
|
||||
certificateChain: z.string(),
|
||||
serialNumber: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain } = await server.services.certificateAuthority.getCaCert({
|
||||
const { certificate, certificateChain, serialNumber } = await server.services.certificateAuthority.getCaCert({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -103,49 +227,155 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
return {
|
||||
certificate,
|
||||
certificateChain
|
||||
certificateChain,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: ":caId/issue-certificate",
|
||||
url: "/:caId/sign-intermediate",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Issue (leaf) certificate from CA",
|
||||
description: "Create intermediate CA certificate from parent CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
csr: z.string().trim(),
|
||||
notBefore: z.string().trim(),
|
||||
notAfter: z.string().trim()
|
||||
notBefore: validateCaDateField.optional(),
|
||||
notAfter: validateCaDateField,
|
||||
maxPathLength: z.number().min(-1).default(-1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificateId: z.string().trim()
|
||||
certificate: z.string().trim(),
|
||||
certificateChain: z.string().trim(),
|
||||
issuingCaCertificate: z.string().trim(),
|
||||
serialNumber: z.string().trim()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async () => {
|
||||
// await server.services.certificateAuthority.issueCertFromCa({
|
||||
// caId: req.params.caId,
|
||||
// actor: req.permission.type,
|
||||
// actorId: req.permission.id,
|
||||
// actorAuthMethod: req.permission.authMethod,
|
||||
// actorOrgId: req.permission.orgId,
|
||||
// ...req.body
|
||||
// });
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
||||
await server.services.certificateAuthority.signIntermediate({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
return {
|
||||
certificateId: "111"
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// TODO 2: logic for creating intermediary ca
|
||||
// TODO 1: get certificate + certificate chain for the root and intermediary CA GET /ca/:caId/certificate
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:caId/import-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Import certificate and chain to CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
certificate: z.string().trim(),
|
||||
certificateChain: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string().trim(),
|
||||
caId: z.string().trim()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
await server.services.certificateAuthority.importCertToCa({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
return {
|
||||
message: "Successfully imported certificate to CA",
|
||||
caId: req.params.caId
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:caId/issue-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Issue certificate from CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
commonName: z.string().trim().min(1),
|
||||
ttl: z.number().int().min(0).optional(),
|
||||
notBefore: validateCaDateField.optional(),
|
||||
notAfter: validateCaDateField.optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const { ttl, notAfter } = data;
|
||||
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
|
||||
},
|
||||
{
|
||||
message: "Either ttl or notAfter must be present, but not both",
|
||||
path: ["ttl", "notAfter"]
|
||||
}
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim(),
|
||||
issuingCaCertificate: z.string().trim(),
|
||||
certificateChain: z.string().trim(),
|
||||
privateKey: z.string().trim(),
|
||||
serialNumber: z.string().trim()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
|
||||
await server.services.certificateAuthority.issueCertFromCa({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
privateKey,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
113
backend/src/server/routes/v1/certificate-router.ts
Normal file
113
backend/src/server/routes/v1/certificate-router.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificatesSchema } from "@app/db/schemas";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:certId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get certificate",
|
||||
params: z.object({
|
||||
certId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: CertificatesSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const certificate = await server.services.certificate.getCertById({
|
||||
certId: req.params.certId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return {
|
||||
certificate
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:certId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Delete certificate",
|
||||
params: z.object({
|
||||
certId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: CertificatesSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const certificate = await server.services.certificate.deleteCertById({
|
||||
certId: req.params.certId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return {
|
||||
certificate
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:certId/certificate",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get certificate of certificate",
|
||||
params: z.object({
|
||||
certId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim(),
|
||||
certificateChain: z.string().trim(),
|
||||
issuingCaCertificate: z.string().trim(),
|
||||
privateKey: z.string().trim(),
|
||||
serialNumber: z.string().trim()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
|
||||
await server.services.certificate.getCertCert({
|
||||
certId: req.params.certId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
privateKey,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { registerAdminRouter } from "./admin-router";
|
||||
import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { registerCertRouter } from "./certificate-router";
|
||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
|
||||
@@ -59,12 +60,8 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
{ prefix: "/workspace" }
|
||||
);
|
||||
|
||||
await server.register(
|
||||
async (certRouter) => {
|
||||
await certRouter.register(registerCaRouter);
|
||||
},
|
||||
{ prefix: "/ca" }
|
||||
);
|
||||
await server.register(registerCaRouter, { prefix: "/ca" });
|
||||
await server.register(registerCertRouter, { prefix: "/certificates" });
|
||||
|
||||
await server.register(registerProjectBotRouter, { prefix: "/bot" });
|
||||
await server.register(registerIntegrationRouter, { prefix: "/integration" });
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificateAuthoritiesSchema, ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
|
||||
import { CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { PROJECTS } from "@app/lib/api-docs";
|
||||
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { ProjectFilterType } from "@app/services/project/project-types";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
@@ -312,22 +313,58 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
method: "GET",
|
||||
url: "/:slug/cas",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: slugSchema.describe("The slug of the project to list CAs.")
|
||||
}),
|
||||
querystring: z.object({
|
||||
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
cas: z.array(CertificateAuthoritiesSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const cas = await server.services.project.listProjectCas({
|
||||
filter: {
|
||||
slug: req.params.slug,
|
||||
orgId: req.permission.orgId,
|
||||
type: ProjectFilterType.SLUG
|
||||
},
|
||||
status: req.query.status,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type
|
||||
});
|
||||
return { cas };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:slug/certificates",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: slugSchema.describe("The slug of the project to list certificates.")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificates: z.array(CertificatesSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const certificates = await server.services.project.listProjectCertificates({
|
||||
filter: {
|
||||
slug: req.params.slug,
|
||||
orgId: req.permission.orgId,
|
||||
@@ -338,7 +375,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type
|
||||
});
|
||||
return { cas };
|
||||
return { certificates };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,10 +1,47 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TCertificateAuthorityDALFactory = ReturnType<typeof certificateAuthorityDALFactory>;
|
||||
|
||||
export const certificateAuthorityDALFactory = (db: TDbClient) => {
|
||||
const caOrm = ormify(db, TableName.CertificateAuthority);
|
||||
return caOrm;
|
||||
|
||||
const buildCertificateChain = async (caId: string) => {
|
||||
try {
|
||||
const result: {
|
||||
caId: string;
|
||||
parentCaId?: string;
|
||||
certificate: string;
|
||||
}[] = await db
|
||||
.withRecursive("cte", (cte) => {
|
||||
void cte
|
||||
.select("ca.id as caId", "ca.parentCaId", "cert.certificate")
|
||||
.from({ ca: TableName.CertificateAuthority })
|
||||
.leftJoin({ cert: TableName.CertificateAuthorityCert }, "ca.id", "cert.caId")
|
||||
.where("ca.id", caId)
|
||||
.unionAll((builder) => {
|
||||
void builder
|
||||
.select("ca.id as caId", "ca.parentCaId", "cert.certificate")
|
||||
.from({ ca: TableName.CertificateAuthority })
|
||||
.leftJoin({ cert: TableName.CertificateAuthorityCert }, "ca.id", "cert.caId")
|
||||
.innerJoin("cte", "cte.parentCaId", "ca.id");
|
||||
});
|
||||
})
|
||||
.select("*")
|
||||
.from("cte");
|
||||
|
||||
// Extract certificates and reverse the order to have the root CA at the end
|
||||
const certChain: string[] = result.map((row) => row.certificate);
|
||||
return certChain;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "BuildCertificateChain" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...caOrm,
|
||||
buildCertificateChain
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { TDNParts } from "./certificate-authority-types";
|
||||
|
||||
export const createDistinguishedName = (parts: TDNParts) => {
|
||||
const dnParts = [];
|
||||
if (parts.country) dnParts.push(`C=${parts.country}`);
|
||||
if (parts.organization) dnParts.push(`O=${parts.organization}`);
|
||||
if (parts.ou) dnParts.push(`OU=${parts.ou}`);
|
||||
if (parts.province) dnParts.push(`ST=${parts.province}`);
|
||||
if (parts.commonName) dnParts.push(`CN=${parts.commonName}`);
|
||||
if (parts.locality) dnParts.push(`L=${parts.locality}`);
|
||||
return dnParts.join(", ");
|
||||
};
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import crypto, { KeyObject } from "crypto";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TCertificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import { createDistinguishedName } from "./certificate-authority-fns";
|
||||
import { TCertificateAuthoritySkDALFactory } from "./certificate-authority-sk-dal";
|
||||
import {
|
||||
// TIssueCertFromCaDTO,
|
||||
CAType,
|
||||
CaStatus,
|
||||
CaType,
|
||||
TCreateCaDTO,
|
||||
TDeleteCaDTO,
|
||||
TGetCaCertDTO,
|
||||
TGetCaCsrDTO
|
||||
TGetCaCsrDTO,
|
||||
TGetCaDTO,
|
||||
TImportCertToCaDTO,
|
||||
TIssueCertFromCaDTO,
|
||||
TSignIntermediateDTO,
|
||||
TUpdateCaDTO
|
||||
} from "./certificate-authority-types";
|
||||
|
||||
type TCertificateAuthorityServiceFactoryDep = {
|
||||
@@ -20,41 +33,27 @@ type TCertificateAuthorityServiceFactoryDep = {
|
||||
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
||||
certificateAuthorityCertDAL: TCertificateAuthorityCertDALFactory;
|
||||
certificateAuthoritySkDAL: TCertificateAuthoritySkDALFactory;
|
||||
certificateDAL: TCertificateDALFactory;
|
||||
certificateCertDAL: TCertificateCertDALFactory;
|
||||
certificateSecretDAL: TCertificateSecretDALFactory;
|
||||
projectDAL: TProjectDALFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
};
|
||||
|
||||
export type TCertificateAuthorityServiceFactory = ReturnType<typeof certificateAuthorityServiceFactory>;
|
||||
|
||||
type DNParts = {
|
||||
commonName?: string;
|
||||
organization?: string;
|
||||
ou?: string;
|
||||
country?: string;
|
||||
province?: string;
|
||||
locality?: string;
|
||||
};
|
||||
|
||||
function createDistinguishedName(parts: DNParts) {
|
||||
const dnParts = [];
|
||||
if (parts.country) dnParts.push(`C=${parts.country}`);
|
||||
if (parts.organization) dnParts.push(`O=${parts.organization}`);
|
||||
if (parts.ou) dnParts.push(`OU=${parts.ou}`);
|
||||
if (parts.province) dnParts.push(`ST=${parts.province}`);
|
||||
if (parts.commonName) dnParts.push(`CN=${parts.commonName}`);
|
||||
if (parts.locality) dnParts.push(`L=${parts.locality}`);
|
||||
return dnParts.join(", ");
|
||||
}
|
||||
|
||||
export const certificateAuthorityServiceFactory = ({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySkDAL,
|
||||
projectDAL
|
||||
certificateDAL,
|
||||
certificateCertDAL,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
permissionService
|
||||
}: TCertificateAuthorityServiceFactoryDep) => {
|
||||
/**
|
||||
* Generates a new root or intermediate CA
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
const createCa = async ({
|
||||
projectSlug,
|
||||
@@ -65,14 +64,30 @@ export const certificateAuthorityServiceFactory = ({
|
||||
country,
|
||||
province,
|
||||
locality,
|
||||
// actorId,
|
||||
// actorAuthMethod,
|
||||
// actor,
|
||||
notBefore,
|
||||
notAfter,
|
||||
maxPathLength,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreateCaDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const dn = createDistinguishedName({
|
||||
commonName,
|
||||
organization,
|
||||
@@ -97,25 +112,41 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const pk = pkObj.export({ format: "pem", type: "spki" }) as string;
|
||||
|
||||
const newCa = await certificateAuthorityDAL.transaction(async (tx) => {
|
||||
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
|
||||
|
||||
// if undefined, set [notAfterDate] to 10 years from now
|
||||
const notAfterDate = notAfter
|
||||
? new Date(notAfter)
|
||||
: new Date(new Date().setFullYear(new Date().getFullYear() + 10));
|
||||
|
||||
const ca = await certificateAuthorityDAL.create(
|
||||
{
|
||||
projectId: project.id,
|
||||
type,
|
||||
dn
|
||||
organization,
|
||||
ou,
|
||||
country,
|
||||
province,
|
||||
locality,
|
||||
commonName,
|
||||
status: type === CaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE,
|
||||
dn,
|
||||
...(type === CaType.ROOT && { maxPathLength, notBefore: notBeforeDate, notAfter: notAfterDate })
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (type === CAType.ROOT) {
|
||||
if (type === CaType.ROOT) {
|
||||
// note: self-signed cert only applicable for root CA
|
||||
|
||||
const cert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||
name: dn,
|
||||
notBefore: new Date("2020/01/01"),
|
||||
notAfter: new Date("2020/01/02"),
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
signingAlgorithm: alg,
|
||||
keys,
|
||||
extensions: [
|
||||
new x509.BasicConstraintsExtension(true, 2, true),
|
||||
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
|
||||
new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
|
||||
// eslint-disable-next-line no-bitwise
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
|
||||
@@ -148,15 +179,94 @@ export const certificateAuthorityServiceFactory = ({
|
||||
return newCa;
|
||||
};
|
||||
|
||||
const getCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
return ca;
|
||||
};
|
||||
|
||||
const updateCaById = async ({ caId, status, actorId, actorAuthMethod, actor, actorOrgId }: TUpdateCaDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const updatedCa = await certificateAuthorityDAL.updateById(caId, { status });
|
||||
|
||||
return updatedCa;
|
||||
};
|
||||
|
||||
const deleteCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCaDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const deletedCa = await certificateAuthorityDAL.deleteById(caId);
|
||||
|
||||
return deletedCa;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a CSR for a CA
|
||||
*/
|
||||
const getCaCsr = async ({ caId }: TGetCaCsrDTO) => {
|
||||
const getCaCsr = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCsrDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
// TODO: permissioning
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" });
|
||||
|
||||
const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id });
|
||||
|
||||
const alg = {
|
||||
@@ -176,10 +286,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
"verify"
|
||||
]);
|
||||
|
||||
const cert = new x509.X509Certificate(caCert.certificate);
|
||||
|
||||
const csr = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||
name: cert.subject || ca.dn,
|
||||
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||
name: ca.dn,
|
||||
keys: { privateKey: sk, publicKey: pk },
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
@@ -189,80 +297,389 @@ export const certificateAuthorityServiceFactory = ({
|
||||
attributes: [new x509.ChallengePasswordAttribute("password")]
|
||||
});
|
||||
|
||||
return csr.toString("base64");
|
||||
return csrObj.toString("pem");
|
||||
};
|
||||
|
||||
/**
|
||||
* Return certificate and certificate chain for CA
|
||||
*/
|
||||
const getCaCert = async ({ caId }: TGetCaCertDTO) => {
|
||||
const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
// TODO: permissioning
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
const certObj = new x509.X509Certificate(caCert.certificate);
|
||||
|
||||
return {
|
||||
certificate: caCert.certificate,
|
||||
certificateChain: caCert.certificateChain
|
||||
certificateChain: caCert.certificateChain,
|
||||
serialNumber: certObj.serialNumber
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Issue new certificate
|
||||
* Issue certificate to be imported back in for intermediate CA
|
||||
* TODO: cannot chain to self
|
||||
*/
|
||||
const issueCertFromCa = async () => {
|
||||
// WIP: parse publicKey from CSR
|
||||
// const csrArrayBuffer = Uint8Array.from(atob(csr), (c) => c.charCodeAt(0));
|
||||
// const csrR = new x509.Pkcs10CertificateRequest(csrArrayBuffer);
|
||||
const signIntermediate = async ({
|
||||
caId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId,
|
||||
csr,
|
||||
notBefore,
|
||||
notAfter,
|
||||
maxPathLength
|
||||
}: TSignIntermediateDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
|
||||
// const ca = await certificateAuthorityDAL.findById(caId);
|
||||
// if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
// // TODO: permissioning
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
// const alg = {
|
||||
// name: "RSASSA-PKCS1-v1_5",
|
||||
// hash: "SHA-256",
|
||||
// publicExponent: new Uint8Array([1, 0, 1]),
|
||||
// modulusLength: 2048
|
||||
// };
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
// const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
// const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id });
|
||||
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||
|
||||
// const skObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" });
|
||||
// const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
|
||||
// "sign"
|
||||
// ]);
|
||||
const alg = {
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
hash: "SHA-256",
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
modulusLength: 2048
|
||||
};
|
||||
|
||||
// const cert = new x509.X509Certificate(caCert.certificate);
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id });
|
||||
|
||||
// const leafCert = await x509.X509CertificateGenerator.create({
|
||||
// // serialNumber: "03",
|
||||
// subject: "CN=Leaf", // TODO: make dynamic
|
||||
// issuer: cert.subject,
|
||||
// notBefore: new Date(notBefore),
|
||||
// notAfter: new Date(notAfter),
|
||||
// signingKey: sk,
|
||||
// publicKey: leafKeys.publicKey,
|
||||
// signingAlgorithm: alg,
|
||||
// extensions: [
|
||||
// new x509.KeyUsagesExtension(x509.KeyUsageFlags.dataEncipherment, true),
|
||||
// new x509.BasicConstraintsExtension(false),
|
||||
// await x509.AuthorityKeyIdentifierExtension.create(cert, false),
|
||||
// await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey)
|
||||
// ]
|
||||
// });
|
||||
const skObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" });
|
||||
const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
|
||||
"sign"
|
||||
]);
|
||||
|
||||
// console.log("leafCert: ", leafCert);
|
||||
const certObj = new x509.X509Certificate(caCert.certificate);
|
||||
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||
|
||||
return "";
|
||||
// check path length constraint
|
||||
const caPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
|
||||
if (caPathLength !== undefined) {
|
||||
if (caPathLength === 0)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to issue intermediate certificate due to CA path length constraint"
|
||||
});
|
||||
if (maxPathLength >= caPathLength || (maxPathLength === -1 && caPathLength !== -1))
|
||||
throw new BadRequestError({
|
||||
message: "The requested path length constraint exceeds the CA's allowed path length"
|
||||
});
|
||||
}
|
||||
|
||||
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
|
||||
const notAfterDate = new Date(notAfter);
|
||||
|
||||
const caCertNotBeforeDate = new Date(certObj.notBefore);
|
||||
const caCertNotAfterDate = new Date(certObj.notAfter);
|
||||
|
||||
// check not before constraint
|
||||
if (notBeforeDate < caCertNotBeforeDate) {
|
||||
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||
}
|
||||
|
||||
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
|
||||
|
||||
// check not after constraint
|
||||
if (notAfterDate > caCertNotAfterDate) {
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
const intermediateCert = await x509.X509CertificateGenerator.create({
|
||||
// serialNumber: "03",
|
||||
subject: csrObj.subject,
|
||||
issuer: certObj.subject,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
signingKey: sk,
|
||||
publicKey: csrObj.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.dataEncipherment, true),
|
||||
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(certObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
const chain = await certificateAuthorityDAL.buildCertificateChain(caId);
|
||||
|
||||
return {
|
||||
certificate: intermediateCert.toString("pem"),
|
||||
issuingCaCertificate: caCert.certificate,
|
||||
certificateChain: chain.join("\n"),
|
||||
serialNumber: intermediateCert.serialNumber
|
||||
};
|
||||
};
|
||||
|
||||
const importCertToCa = async ({
|
||||
caId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId,
|
||||
certificate,
|
||||
certificateChain
|
||||
}: TImportCertToCaDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" });
|
||||
|
||||
const certObj = new x509.X509Certificate(certificate);
|
||||
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
|
||||
|
||||
// validate imported certificate and certificate chain
|
||||
const certificates = certificateChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
|
||||
const chain = new x509.X509ChainBuilder({
|
||||
certificates
|
||||
});
|
||||
|
||||
const chainItems = await chain.build(certObj);
|
||||
|
||||
// chain.build() implicitly verifies the chain
|
||||
if (chainItems.length !== certificates.length + 1)
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
|
||||
const parentCertObj = chainItems[1];
|
||||
const parentCertSubject = parentCertObj.subject;
|
||||
|
||||
const parentCa = await certificateAuthorityDAL.findOne({
|
||||
projectId: ca.projectId,
|
||||
dn: parentCertSubject
|
||||
});
|
||||
|
||||
await certificateAuthorityCertDAL.transaction(async (tx) => {
|
||||
await certificateAuthorityCertDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
certificate, // TODO: encrypt
|
||||
certificateChain // TODO: encrypt
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateAuthorityDAL.updateById(
|
||||
ca.id,
|
||||
{
|
||||
status: CaStatus.ACTIVE,
|
||||
maxPathLength: maxPathLength === undefined ? -1 : maxPathLength,
|
||||
notBefore: new Date(certObj.notBefore),
|
||||
notAfter: new Date(certObj.notAfter),
|
||||
parentCaId: parentCa?.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const issueCertFromCa = async ({
|
||||
caId,
|
||||
commonName,
|
||||
ttl,
|
||||
notBefore,
|
||||
notAfter,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TIssueCertFromCaDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
|
||||
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||
|
||||
const caCertObj = new x509.X509Certificate(caCert.certificate);
|
||||
|
||||
const alg = {
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
hash: "SHA-256",
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
modulusLength: 2048
|
||||
};
|
||||
|
||||
const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id });
|
||||
|
||||
const caSkObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" });
|
||||
const caSk = await crypto.subtle.importKey("pkcs8", caSkObj.export({ format: "der", type: "pkcs8" }), alg, true, [
|
||||
"sign"
|
||||
]);
|
||||
|
||||
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
|
||||
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||
name: `CN=${commonName}`,
|
||||
keys: leafKeys,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
// eslint-disable-next-line no-bitwise
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
|
||||
],
|
||||
attributes: [new x509.ChallengePasswordAttribute("password")]
|
||||
});
|
||||
|
||||
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
|
||||
|
||||
let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1));
|
||||
if (notAfter) {
|
||||
notAfterDate = new Date(notAfter);
|
||||
} else if (ttl) {
|
||||
// ttl in seconds
|
||||
notAfterDate = new Date(new Date().getTime() + ttl * 1000);
|
||||
}
|
||||
|
||||
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||
|
||||
// check not before constraint
|
||||
if (notBeforeDate < caCertNotBeforeDate) {
|
||||
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||
}
|
||||
|
||||
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
|
||||
|
||||
// check not after constraint
|
||||
if (notAfterDate > caCertNotAfterDate) {
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
// serialNumber: "03",
|
||||
subject: csrObj.subject,
|
||||
issuer: caCertObj.subject,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
signingKey: caSk,
|
||||
publicKey: csrObj.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.dataEncipherment, true),
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||
|
||||
const chain = await certificateAuthorityDAL.buildCertificateChain(caId);
|
||||
|
||||
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
|
||||
const skObj = KeyObject.from(leafKeys.privateKey);
|
||||
const sk = skObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||
const pkObj = KeyObject.from(leafKeys.publicKey);
|
||||
const pk = pkObj.export({ format: "pem", type: "spki" }) as string;
|
||||
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
commonName,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateCertDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
certificate: leafCert.toString("pem"), // TODO: encrypt
|
||||
certificateChain: chain.join("\n") // TODO: encrypt
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateSecretDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
pk, // TODO: encrypt
|
||||
sk // TODO: encrypt
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return cert;
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: leafCert.toString("pem"),
|
||||
certificateChain: chain.join("\n"),
|
||||
issuingCaCertificate: caCert.certificate,
|
||||
privateKey: skLeaf,
|
||||
serialNumber: leafCert.serialNumber
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createCa,
|
||||
getCaById,
|
||||
updateCaById,
|
||||
deleteCaById,
|
||||
getCaCsr,
|
||||
getCaCert,
|
||||
signIntermediate,
|
||||
importCertToCa,
|
||||
issueCertFromCa
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,21 +1,43 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum CAType {
|
||||
export enum CaType {
|
||||
ROOT = "root",
|
||||
INTERMEDIATE = "intermediate"
|
||||
}
|
||||
|
||||
export enum CaStatus {
|
||||
ACTIVE = "active",
|
||||
DISABLED = "disabled",
|
||||
PENDING_CERTIFICATE = "pending-certificate"
|
||||
}
|
||||
|
||||
// TODO: attach permissions after draft impl
|
||||
|
||||
export type TCreateCaDTO = {
|
||||
projectSlug: string;
|
||||
type: CAType;
|
||||
type: CaType;
|
||||
commonName: string;
|
||||
organization: string;
|
||||
ou: string;
|
||||
country: string;
|
||||
province: string;
|
||||
locality: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
maxPathLength?: number;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCaDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateCaDTO = {
|
||||
caId: string;
|
||||
status?: CaStatus;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteCaDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCaCsrDTO = {
|
||||
@@ -26,9 +48,33 @@ export type TGetCaCertDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIssueCertFromCaDTO = {
|
||||
export type TSignIntermediateDTO = {
|
||||
caId: string;
|
||||
csr: string;
|
||||
notBefore: string;
|
||||
notBefore?: string;
|
||||
notAfter: string;
|
||||
maxPathLength: number;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TImportCertToCaDTO = {
|
||||
caId: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIssueCertFromCaDTO = {
|
||||
caId: string;
|
||||
commonName: string;
|
||||
ttl?: number;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDNParts = {
|
||||
commonName?: string;
|
||||
organization?: string;
|
||||
ou?: string;
|
||||
country?: string;
|
||||
province?: string;
|
||||
locality?: string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const isValidDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return !Number.isNaN(date.getTime());
|
||||
};
|
||||
|
||||
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
|
||||
10
backend/src/services/certificate/certificate-cert-dal.ts
Normal file
10
backend/src/services/certificate/certificate-cert-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TCertificateCertDALFactory = ReturnType<typeof certificateCertDALFactory>;
|
||||
|
||||
export const certificateCertDALFactory = (db: TDbClient) => {
|
||||
const certificateCertOrm = ormify(db, TableName.CertificateCert);
|
||||
return certificateCertOrm;
|
||||
};
|
||||
10
backend/src/services/certificate/certificate-dal.ts
Normal file
10
backend/src/services/certificate/certificate-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;
|
||||
|
||||
export const certificateDALFactory = (db: TDbClient) => {
|
||||
const certificateOrm = ormify(db, TableName.Certificate);
|
||||
return certificateOrm;
|
||||
};
|
||||
10
backend/src/services/certificate/certificate-secret-dal.ts
Normal file
10
backend/src/services/certificate/certificate-secret-dal.ts
Normal file
@@ -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 certificateSecretOrm = ormify(db, TableName.CertificateSecret);
|
||||
return certificateSecretOrm;
|
||||
};
|
||||
102
backend/src/services/certificate/certificate-service.ts
Normal file
102
backend/src/services/certificate/certificate-service.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TCertificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
|
||||
import { TDeleteCertDTO, TGetCertCertDTO, TGetCertDTO } from "./certificate-types";
|
||||
|
||||
type TCertificateServiceFactoryDep = {
|
||||
// TODO: Pick
|
||||
certificateDAL: TCertificateDALFactory;
|
||||
certificateCertDAL: TCertificateCertDALFactory;
|
||||
certificateSecretDAL: TCertificateSecretDALFactory;
|
||||
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
||||
certificateAuthorityCertDAL: TCertificateAuthorityCertDALFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
};
|
||||
|
||||
export type TCertificateServiceFactory = ReturnType<typeof certificateServiceFactory>;
|
||||
|
||||
export const certificateServiceFactory = ({
|
||||
certificateDAL,
|
||||
certificateCertDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
permissionService
|
||||
}: TCertificateServiceFactoryDep) => {
|
||||
const getCertById = async ({ certId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => {
|
||||
const cert = await certificateDAL.findById(certId);
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
|
||||
return cert;
|
||||
};
|
||||
|
||||
const deleteCertById = async ({ certId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => {
|
||||
const cert = await certificateDAL.findById(certId);
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||
|
||||
const deletedCert = await certificateDAL.deleteById(cert.id);
|
||||
return deletedCert;
|
||||
};
|
||||
|
||||
const getCertCert = async ({ certId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertCertDTO) => {
|
||||
const cert = await certificateDAL.findById(certId);
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
// TODO: re-evaluate this permission
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
const certCert = await certificateCertDAL.findOne({ certId });
|
||||
const certSecret = await certificateSecretDAL.findOne({ certId });
|
||||
const certObj = new x509.X509Certificate(certCert.certificate);
|
||||
|
||||
return {
|
||||
certificate: certCert.certificate,
|
||||
certificateChain: certCert.certificateChain,
|
||||
issuingCaCertificate: caCert.certificate,
|
||||
privateKey: certSecret.sk,
|
||||
serialNumber: certObj.serialNumber
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
getCertById,
|
||||
deleteCertById,
|
||||
getCertCert
|
||||
};
|
||||
};
|
||||
13
backend/src/services/certificate/certificate-types.ts
Normal file
13
backend/src/services/certificate/certificate-types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TGetCertDTO = {
|
||||
certId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteCertDTO = {
|
||||
certId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCertCertDTO = {
|
||||
certId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
@@ -16,6 +16,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||
@@ -51,6 +52,7 @@ export const DEFAULT_PROJECT_ENVS = [
|
||||
];
|
||||
|
||||
type TProjectServiceFactoryDep = {
|
||||
// TODO: Pick
|
||||
projectDAL: TProjectDALFactory;
|
||||
projectQueue: TProjectQueueFactory;
|
||||
userDAL: TUserDALFactory;
|
||||
@@ -65,6 +67,7 @@ type TProjectServiceFactoryDep = {
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||
certificateDAL: TCertificateDALFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
@@ -93,6 +96,7 @@ export const projectServiceFactory = ({
|
||||
projectUserMembershipRoleDAL,
|
||||
identityProjectMembershipRoleDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
keyStore
|
||||
}: TProjectServiceFactoryDep) => {
|
||||
/*
|
||||
@@ -500,24 +504,66 @@ export const projectServiceFactory = ({
|
||||
* Return list of CAs for project
|
||||
*/
|
||||
const listProjectCas = async ({
|
||||
// actorId,
|
||||
// actorOrgId,
|
||||
// actorAuthMethod,
|
||||
filter // actor
|
||||
status,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
filter,
|
||||
actor
|
||||
}: TListProjectCasDTO) => {
|
||||
const project = await projectDAL.findProjectByFilter(filter);
|
||||
|
||||
// const { permission } = await permissionService.getProjectPermission(
|
||||
// actor,
|
||||
// actorId,
|
||||
// project.id,
|
||||
// actorAuthMethod,
|
||||
// actorOrgId
|
||||
// );
|
||||
// TODO: add permissioning
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const cas = await certificateAuthorityDAL.find({
|
||||
projectId: project.id,
|
||||
...(status && { status })
|
||||
});
|
||||
|
||||
return cas;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of certificates for project
|
||||
*/
|
||||
const listProjectCertificates = async ({
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
filter,
|
||||
actor
|
||||
}: TListProjectCasDTO) => {
|
||||
const project = await projectDAL.findProjectByFilter(filter);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
|
||||
const cas = await certificateAuthorityDAL.find({ projectId: project.id });
|
||||
return cas;
|
||||
|
||||
const certificates = await certificateDAL.find({
|
||||
$in: {
|
||||
caId: cas.map((ca) => ca.id)
|
||||
}
|
||||
});
|
||||
return certificates;
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -530,6 +576,7 @@ export const projectServiceFactory = ({
|
||||
toggleAutoCapitalization,
|
||||
updateName,
|
||||
upgradeProject,
|
||||
listProjectCas
|
||||
listProjectCas,
|
||||
listProjectCertificates
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
import { CaStatus } from "../certificate-authority/certificate-authority-types";
|
||||
|
||||
export enum ProjectFilterType {
|
||||
ID = "id",
|
||||
@@ -77,5 +78,6 @@ export type AddUserToWsDTO = {
|
||||
};
|
||||
|
||||
export type TListProjectCasDTO = {
|
||||
status?: CaStatus;
|
||||
filter: Filter;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@@ -24,7 +24,9 @@ export enum ProjectPermissionSub {
|
||||
SecretRollback = "secret-rollback",
|
||||
SecretApproval = "secret-approval",
|
||||
SecretRotation = "secret-rotation",
|
||||
Identity = "identity"
|
||||
Identity = "identity",
|
||||
CertificateAuthorities = "certificate-authorities",
|
||||
Certificates = "certificates"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
@@ -51,6 +53,8 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { CertificateAuthorityType } from "./enums";
|
||||
import { CaStatus,CaType } from "./enums";
|
||||
|
||||
export const caTypeToNameMap: { [K in CertificateAuthorityType]: string } = {
|
||||
[CertificateAuthorityType.ROOT]: "Root",
|
||||
[CertificateAuthorityType.INTERMEDIATE]: "Intermediate"
|
||||
export const caTypeToNameMap: { [K in CaType]: string } = {
|
||||
[CaType.ROOT]: "Root",
|
||||
[CaType.INTERMEDIATE]: "Intermediate"
|
||||
};
|
||||
|
||||
export const caStatusToNameMap: { [K in CaStatus]: string } = {
|
||||
[CaStatus.ACTIVE]: "Active",
|
||||
[CaStatus.DISABLED]: "Disabled",
|
||||
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
|
||||
};
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
export enum CertificateAuthorityType {
|
||||
export enum CaType {
|
||||
ROOT = "root",
|
||||
INTERMEDIATE = "intermediate"
|
||||
}
|
||||
|
||||
export enum CaStatus {
|
||||
ACTIVE = "active",
|
||||
DISABLED = "disabled",
|
||||
PENDING_CERTIFICATE = "pending-certificate"
|
||||
}
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
export { CertificateAuthorityType } from "./enums";
|
||||
export { useCreateCa } from "./mutations";
|
||||
export { CaStatus,CaType } from "./enums";
|
||||
export {
|
||||
useCreateCa,
|
||||
useCreateCertificate,
|
||||
useDeleteCa,
|
||||
useImportCaCertificate,
|
||||
useSignIntermediate,
|
||||
useUpdateCa} from "./mutations";
|
||||
export { useGetCaById, useGetCaCert, useGetCaCsr } from "./queries";
|
||||
|
||||
@@ -1,23 +1,107 @@
|
||||
import {
|
||||
useMutation
|
||||
// useQueryClient
|
||||
} from "@tanstack/react-query";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TCertificateAuthority, TCreateCaDTO } from "./types";
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import {
|
||||
TCertificateAuthority,
|
||||
TCreateCaDTO,
|
||||
TCreateCertificateDTO,
|
||||
TCreateCertificateResponse,
|
||||
TDeleteCaDTO,
|
||||
TImportCaCertificateDTO,
|
||||
TImportCaCertificateResponse,
|
||||
TSignIntermediateDTO,
|
||||
TSignIntermediateResponse,
|
||||
TUpdateCaDTO} from "./types";
|
||||
|
||||
export const useCreateCa = () => {
|
||||
// const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateAuthority, {}, TCreateCaDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const {
|
||||
data: { identity }
|
||||
} = await apiRequest.post("/api/v1/ca/", body);
|
||||
return identity;
|
||||
data: { ca }
|
||||
} = await apiRequest.post<{ ca: TCertificateAuthority }>("/api/v1/ca/", body);
|
||||
return ca;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateAuthority, {}, TUpdateCaDTO>({
|
||||
mutationFn: async ({ caId, projectSlug, ...body }) => {
|
||||
const {
|
||||
data: { ca }
|
||||
} = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/ca/${caId}`, body);
|
||||
return ca;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateAuthority, {}, TDeleteCaDTO>({
|
||||
mutationFn: async ({ caId }) => {
|
||||
const {
|
||||
data: { ca }
|
||||
} = await apiRequest.delete<{ ca: TCertificateAuthority }>(`/api/v1/ca/${caId}`);
|
||||
return ca;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useSignIntermediate = () => {
|
||||
// TODO: consider renaming
|
||||
return useMutation<TSignIntermediateResponse, {}, TSignIntermediateDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TSignIntermediateResponse>(
|
||||
`/api/v1/ca/${body.caId}/sign-intermediate`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useImportCaCertificate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TImportCaCertificateResponse, {}, TImportCaCertificateDTO>({
|
||||
mutationFn: async ({ caId, ...body }) => {
|
||||
const { data } = await apiRequest.post<TImportCaCertificateResponse>(
|
||||
`/api/v1/ca/${caId}/import-certificate`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// consider rename to issue certificate
|
||||
export const useCreateCertificate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCreateCertificateResponse, {}, TCreateCertificateDTO>({
|
||||
mutationFn: async ({ caId, ...body }) => {
|
||||
const { data } = await apiRequest.post<TCreateCertificateResponse>(
|
||||
`/api/v1/ca/${caId}/issue-certificate`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificates(projectSlug));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
54
frontend/src/hooks/api/ca/queries.tsx
Normal file
54
frontend/src/hooks/api/ca/queries.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TCertificateAuthority } from "./types";
|
||||
|
||||
export const caKeys = {
|
||||
getCaById: (caId: string) => [{ caId }, "ca"],
|
||||
getCaCert: (caId: string) => [{ caId }, "ca-cert"],
|
||||
getCaCsr: (caId: string) => [{ caId }, "ca-csr"]
|
||||
};
|
||||
|
||||
export const useGetCaById = (caId: string) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.getCaById(caId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { ca }
|
||||
} = await apiRequest.get<{ ca: TCertificateAuthority }>(`/api/v1/ca/${caId}`);
|
||||
return ca;
|
||||
},
|
||||
enabled: Boolean(caId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCaCert = (caId: string) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.getCaCert(caId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
serialNumber: string;
|
||||
}>(`/api/v1/ca/${caId}/certificate`);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(caId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCaCsr = (caId: string) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.getCaCsr(caId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { csr }
|
||||
} = await apiRequest.get<{
|
||||
csr: string;
|
||||
}>(`/api/v1/ca/${caId}/csr`);
|
||||
return csr;
|
||||
},
|
||||
enabled: Boolean(caId)
|
||||
});
|
||||
};
|
||||
@@ -1,12 +1,21 @@
|
||||
import { CertificateAuthorityType } from "./enums";
|
||||
import { CaStatus,CaType } from "./enums";
|
||||
|
||||
export type TCertificateAuthority = {
|
||||
id: string;
|
||||
parentCaId?: string;
|
||||
projectId: string;
|
||||
type: CertificateAuthorityType;
|
||||
dn: string;
|
||||
type: CaType;
|
||||
status: CaStatus;
|
||||
organization: string;
|
||||
ou: string;
|
||||
country: string;
|
||||
province: string;
|
||||
locality: string;
|
||||
commonName: string;
|
||||
dn: string;
|
||||
maxPathLength?: number;
|
||||
notAfter?: string;
|
||||
notBefore?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@@ -20,4 +29,62 @@ export type TCreateCaDTO = {
|
||||
province: string;
|
||||
locality: string;
|
||||
commonName: string;
|
||||
notAfter?: string;
|
||||
maxPathLength: number;
|
||||
};
|
||||
|
||||
export type TUpdateCaDTO = {
|
||||
projectSlug: string;
|
||||
caId: string;
|
||||
status?: CaStatus;
|
||||
};
|
||||
|
||||
export type TDeleteCaDTO = {
|
||||
projectSlug: string;
|
||||
caId: string;
|
||||
};
|
||||
|
||||
export type TSignIntermediateDTO = {
|
||||
caId: string;
|
||||
csr: string;
|
||||
maxPathLength: number;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
};
|
||||
|
||||
export type TSignIntermediateResponse = {
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
issuingCaCertificate: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
|
||||
export type TImportCaCertificateDTO = {
|
||||
caId: string;
|
||||
projectSlug: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
};
|
||||
|
||||
export type TImportCaCertificateResponse = {
|
||||
message: string;
|
||||
caId: string;
|
||||
};
|
||||
|
||||
// TODO: add TTL
|
||||
export type TCreateCertificateDTO = {
|
||||
projectSlug: string;
|
||||
caId: string;
|
||||
commonName: string;
|
||||
ttl?: number;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
};
|
||||
|
||||
export type TCreateCertificateResponse = {
|
||||
certificate: string;
|
||||
issuingCertificate: string;
|
||||
certificateChain: string;
|
||||
sk: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
|
||||
2
frontend/src/hooks/api/certificates/index.tsx
Normal file
2
frontend/src/hooks/api/certificates/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { useDeleteCert } from "./mutations";
|
||||
export { useGetCertById, useGetCertCert } from "./queries";
|
||||
26
frontend/src/hooks/api/certificates/mutations.tsx
Normal file
26
frontend/src/hooks/api/certificates/mutations.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { TCertificate } from "./types";
|
||||
|
||||
export type TDeleteCaDTO = {
|
||||
projectSlug: string;
|
||||
certId: string;
|
||||
};
|
||||
|
||||
export const useDeleteCert = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificate, {}, TDeleteCaDTO>({
|
||||
mutationFn: async ({ certId }) => {
|
||||
const {
|
||||
data: { certificate }
|
||||
} = await apiRequest.delete<{ certificate: TCertificate }>(`/api/v1/certificates/${certId}`);
|
||||
return certificate;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificates(projectSlug));
|
||||
}
|
||||
});
|
||||
};
|
||||
40
frontend/src/hooks/api/certificates/queries.tsx
Normal file
40
frontend/src/hooks/api/certificates/queries.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TCertificate } from "./types";
|
||||
|
||||
export const certKeys = {
|
||||
getCertById: (certId: string) => [{ certId }, "cert"],
|
||||
getCertCert: (certId: string) => [{ certId }, "certCert"]
|
||||
};
|
||||
|
||||
export const useGetCertById = (certId: string) => {
|
||||
return useQuery({
|
||||
queryKey: certKeys.getCertById(certId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { certificate }
|
||||
} = await apiRequest.get<{ certificate: TCertificate }>(`/api/v1/certificates/${certId}`);
|
||||
return certificate;
|
||||
},
|
||||
enabled: Boolean(certId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCertCert = (certId: string) => {
|
||||
return useQuery({
|
||||
queryKey: certKeys.getCertCert(certId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
issuingCaCertificate: string;
|
||||
privateKey: string;
|
||||
serialNumber: string;
|
||||
}>(`/api/v1/certificates/${certId}/certificate`);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(certId)
|
||||
});
|
||||
};
|
||||
7
frontend/src/hooks/api/certificates/types.ts
Normal file
7
frontend/src/hooks/api/certificates/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type TCertificate = {
|
||||
id: string;
|
||||
caId: string;
|
||||
commonName: string;
|
||||
notBefore: string;
|
||||
notAfter: string;
|
||||
};
|
||||
@@ -6,6 +6,7 @@ export * from "./auditLogStreams";
|
||||
export * from "./auth";
|
||||
export * from "./bots";
|
||||
export * from "./ca";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamicSecret";
|
||||
export * from "./dynamicSecretLease";
|
||||
export * from "./groups";
|
||||
|
||||
@@ -22,6 +22,7 @@ export {
|
||||
useGetWorkspaceSecrets,
|
||||
useGetWorkspaceUsers,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspaceCertificates,
|
||||
useListWorkspaceGroups,
|
||||
useNameWorkspaceSecrets,
|
||||
useRenameWorkspace,
|
||||
|
||||
@@ -2,7 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { CaStatus } from "../ca/enums";
|
||||
import { TCertificateAuthority } from "../ca/types";
|
||||
import { TCertificate } from "../certificates/types";
|
||||
import { TGroupMembership } from "../groups/types";
|
||||
import { IdentityMembership } from "../identities/types";
|
||||
import { IntegrationAuth } from "../integrationAuth/types";
|
||||
@@ -41,7 +43,12 @@ export const workspaceKeys = {
|
||||
[{ workspaceId }, "workspace-identity-memberships"] as const,
|
||||
getWorkspaceGroupMemberships: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-groups"] as const,
|
||||
getWorkspaceCas: (workspaceId: string) => [{ workspaceId }, "workspace-cas"] as const
|
||||
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>
|
||||
[{ projectSlug }, "workspace-cas"] as const,
|
||||
specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) =>
|
||||
[...workspaceKeys.getWorkspaceCas({ projectSlug }), { status }] as const,
|
||||
getWorkspaceCertificates: (projectSlug: string) =>
|
||||
[{ projectSlug }, "workspace-certificates"] as const
|
||||
};
|
||||
|
||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||
@@ -472,17 +479,48 @@ export const useListWorkspaceGroups = (projectSlug: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useListWorkspaceCas = (projectSlug: string) => {
|
||||
export const useListWorkspaceCas = ({
|
||||
projectSlug,
|
||||
status
|
||||
}: {
|
||||
projectSlug: string;
|
||||
status?: CaStatus;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceCas(projectSlug),
|
||||
queryKey: workspaceKeys.specificWorkspaceCas({
|
||||
projectSlug,
|
||||
status
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({
|
||||
...(status && { status })
|
||||
});
|
||||
|
||||
const {
|
||||
data: { cas }
|
||||
} = await apiRequest.get<{ cas: TCertificateAuthority[] }>(
|
||||
`/api/v2/workspace/${projectSlug}/cas`
|
||||
`/api/v2/workspace/${projectSlug}/cas`,
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
return cas;
|
||||
},
|
||||
enabled: Boolean(projectSlug)
|
||||
});
|
||||
};
|
||||
|
||||
export const useListWorkspaceCertificates = (projectSlug: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceCertificates(projectSlug),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { certificates }
|
||||
} = await apiRequest.get<{ certificates: TCertificate[] }>(
|
||||
`/api/v2/workspace/${projectSlug}/certificates`
|
||||
);
|
||||
return certificates;
|
||||
},
|
||||
enabled: Boolean(projectSlug)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9,11 +9,10 @@ const Certificates = () => {
|
||||
return (
|
||||
<div className="h-full bg-bunker-800">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
<title>{t("common.head-title", { title: "Certificates" })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<div>Test CA page</div>
|
||||
<CertificatesPage />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,15 +2,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
import { CaTab } from "./components";
|
||||
// import { CaTable } from "./components";
|
||||
|
||||
// TODO: fix permission
|
||||
|
||||
/**
|
||||
* TODO 1: CA section
|
||||
* TODO 2: Certificates section
|
||||
*/
|
||||
import { CaTab, CertificatesTab } from "./components";
|
||||
|
||||
enum TabSections {
|
||||
Ca = "certificate-authorities",
|
||||
@@ -23,18 +15,17 @@ export const CertificatesPage = withProjectPermission(
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
|
||||
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Certificates</p>
|
||||
<Tabs defaultValue={TabSections.Ca}>
|
||||
<Tabs defaultValue={TabSections.Certificates}>
|
||||
<TabList>
|
||||
<Tab value={TabSections.Ca}>Certificate Authorities</Tab>
|
||||
<Tab value={TabSections.Certificates}>Certificates</Tab>
|
||||
<Tab value={TabSections.Ca}>Certificate Authorities</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Certificates}>
|
||||
<CertificatesTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Ca}>
|
||||
<CaTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Certificates}>
|
||||
<div>Certs</div>
|
||||
{/* <OrgGroupsTab /> */}
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useEffect } from "react";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { IconButton,Modal, ModalContent } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetCaCert } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["caCert"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["caCert"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const CaCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { data } = useGetCaCert((popUp?.caCert?.data as { caId: string })?.caId || "");
|
||||
const [isSerialNumberCopied, setIsSerialNumberCopied] = useToggle(false);
|
||||
const [isCertificateCopied, setIsCertificateCopied] = useToggle(false);
|
||||
const [isCertificateChainCopied, setIsCertificateChainCopied] = useToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isSerialNumberCopied) {
|
||||
timer = setTimeout(() => setIsSerialNumberCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isCertificateCopied) {
|
||||
timer = setTimeout(() => setIsCertificateCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isCertificateChainCopied) {
|
||||
timer = setTimeout(() => setIsCertificateChainCopied.off(), 2000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isSerialNumberCopied, isCertificateCopied, isCertificateChainCopied]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.caCert?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("caCert", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title="CA Certificate">
|
||||
{data ? (
|
||||
<div>
|
||||
<h2 className="mb-4">Serial Number</h2>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{data.serialNumber}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.serialNumber);
|
||||
setIsSerialNumberCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSerialNumberCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Click to copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<h2 className="mb-4">Certificate Body</h2>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{data.certificate}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.certificate);
|
||||
setIsCertificateCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCertificateCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Click to copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<h2 className="mb-4">Certificate Chain</h2>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{data.certificateChain}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.certificateChain);
|
||||
setIsCertificateChainCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCertificateChainCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Click to copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,328 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { format } from "date-fns";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
// DatePicker,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
CaStatus,
|
||||
useGetCaById,
|
||||
useGetCaCsr,
|
||||
useImportCaCertificate,
|
||||
useListWorkspaceCas,
|
||||
useSignIntermediate
|
||||
} from "@app/hooks/api";
|
||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const isValidDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return !Number.isNaN(date.getTime());
|
||||
};
|
||||
|
||||
const getMiddleDate = (date1: Date, date2: Date) => {
|
||||
const timestamp1 = date1.getTime();
|
||||
const timestamp2 = date2.getTime();
|
||||
|
||||
const middleTimestamp = (timestamp1 + timestamp2) / 2;
|
||||
|
||||
return new Date(middleTimestamp);
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
parentCaId: z.string(),
|
||||
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
|
||||
maxPathLength: z.string()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["installCaCert"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["installCaCert"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
enum ParentCaType {
|
||||
Internal = "internal",
|
||||
External = "external"
|
||||
}
|
||||
|
||||
export const CaInstallCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [parentCaType] = useState<ParentCaType>(ParentCaType.Internal);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const caId = (popUp?.installCaCert?.data as { caId: string })?.caId || "";
|
||||
|
||||
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
const { data: cas } = useListWorkspaceCas({
|
||||
projectSlug: currentWorkspace?.slug ?? "",
|
||||
status: CaStatus.ACTIVE
|
||||
});
|
||||
const { data: ca } = useGetCaById(caId);
|
||||
const { data: csr } = useGetCaCsr(caId);
|
||||
|
||||
const { mutateAsync: signIntermediate } = useSignIntermediate();
|
||||
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
setValue,
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
maxPathLength: "0"
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (cas?.length) {
|
||||
setValue("parentCaId", cas[0].id);
|
||||
}
|
||||
}, [cas, setValue]);
|
||||
|
||||
const parentCaId = watch("parentCaId");
|
||||
const { data: parentCa } = useGetCaById(parentCaId);
|
||||
|
||||
useEffect(() => {
|
||||
if (parentCa?.maxPathLength) {
|
||||
setValue(
|
||||
"maxPathLength",
|
||||
(parentCa.maxPathLength === -1 ? 3 : parentCa.maxPathLength - 1).toString()
|
||||
);
|
||||
}
|
||||
|
||||
if (parentCa?.notAfter) {
|
||||
const parentCaNotAfter = new Date(parentCa.notAfter);
|
||||
const middleDate = getMiddleDate(new Date(), parentCaNotAfter);
|
||||
setValue("notAfter", format(middleDate, "yyyy-MM-dd"));
|
||||
}
|
||||
}, [parentCa]);
|
||||
|
||||
const onFormSubmit = async ({ notAfter, maxPathLength }: FormData) => {
|
||||
try {
|
||||
if (!csr || !caId || !currentWorkspace?.slug) return;
|
||||
|
||||
const { certificate, certificateChain } = await signIntermediate({
|
||||
caId: parentCaId,
|
||||
csr,
|
||||
maxPathLength: Number(maxPathLength),
|
||||
notAfter,
|
||||
notBefore: new Date().toISOString()
|
||||
});
|
||||
|
||||
await importCaCertificate({
|
||||
caId,
|
||||
projectSlug: currentWorkspace?.slug,
|
||||
certificate,
|
||||
certificateChain
|
||||
});
|
||||
|
||||
reset();
|
||||
|
||||
createNotification({
|
||||
text: "Successfully installed certificate for CA",
|
||||
type: "success"
|
||||
});
|
||||
handlePopUpToggle("installCaCert", false);
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Failed to install certificate for CA",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function generatePathLengthOpts(parentCaMaxPathLength: number): number[] {
|
||||
if (parentCaMaxPathLength === -1) {
|
||||
return [-1, 0, 1, 2, 3];
|
||||
}
|
||||
|
||||
return Array.from({ length: parentCaMaxPathLength }, (_, index) => index);
|
||||
}
|
||||
|
||||
const renderForm = (parentCaTypeInput: ParentCaType) => {
|
||||
switch (parentCaTypeInput) {
|
||||
case ParentCaType.Internal:
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parentCaId"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Parent CA"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(cas || [])
|
||||
.filter((c) => {
|
||||
const isParentCaNotSelf = c.id !== ca?.id;
|
||||
const isParentCaActive = c.status === CaStatus.ACTIVE;
|
||||
const isParentCaAllowedChildrenCas =
|
||||
c.maxPathLength && c.maxPathLength !== 0;
|
||||
|
||||
return (
|
||||
isParentCaNotSelf && isParentCaActive && isParentCaAllowedChildrenCas
|
||||
);
|
||||
})
|
||||
.map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`parent-ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{/* <Controller
|
||||
name="notAfter"
|
||||
control={control}
|
||||
defaultValue={getDefaultNotAfterDate()}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
label="Validity"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-4"
|
||||
>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={(date) => {
|
||||
onChange(date);
|
||||
setIsStartDatePickerOpen(false);
|
||||
}}
|
||||
popUpProps={{
|
||||
open: isStartDatePickerOpen,
|
||||
onOpenChange: setIsStartDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/> */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="notAfter"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Valid Until"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="YYYY-MM-DD" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxPathLength"
|
||||
// defaultValue="0"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Path Length"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{generatePathLengthOpts(parentCa?.maxPathLength || 0).map((value) => (
|
||||
<SelectItem value={String(value)} key={`ca-path-length-${value}`}>
|
||||
{`${value}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Install
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("installCaCert", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
default:
|
||||
return <div>External TODO</div>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.installCaCert?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("installCaCert", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Install Intermediate CA certificate">
|
||||
{/* <FormControl label="Parent CA Type" className="mt-4">
|
||||
<Select
|
||||
defaultValue={ParentCaType.Internal}
|
||||
value={parentCaType}
|
||||
onValueChange={(e) => setParentCaType(e as ParentCaType)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem
|
||||
value={ParentCaType.Internal}
|
||||
key={`parent-ca-type-${ParentCaType.Internal}`}
|
||||
>
|
||||
Infisical Private CA
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value={ParentCaType.External}
|
||||
key={`parent-ca-type-${ParentCaType.External}`}
|
||||
>
|
||||
External Private CA
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl> */}
|
||||
{renderForm(parentCaType)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CaInstallCertModal } from "./CaInstallCertModal";
|
||||
@@ -1,8 +1,7 @@
|
||||
// import { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// import { yupResolver } from "@hookform/resolvers/yup";
|
||||
// import * as yup from "yup";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { format } from "date-fns";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -14,21 +13,34 @@ import {
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
// DatePicker
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { CertificateAuthorityType, useCreateCa } from "@app/hooks/api/ca";
|
||||
// import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
|
||||
import { CaType, useCreateCa, useGetCaById } from "@app/hooks/api/ca";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const isValidDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return !Number.isNaN(date.getTime());
|
||||
};
|
||||
|
||||
const getDateTenYearsFromToday = () => {
|
||||
const date = new Date();
|
||||
date.setFullYear(date.getFullYear() + 10);
|
||||
return format(date, "yyyy-MM-dd");
|
||||
};
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
type: z.enum(["root", "intermediate"]), // move to ref enum of hooks/api
|
||||
type: z.enum([CaType.ROOT, CaType.INTERMEDIATE]),
|
||||
organization: z.string(),
|
||||
ou: z.string(),
|
||||
country: z.string(),
|
||||
province: z.string(),
|
||||
locality: z.string(),
|
||||
commonName: z.string()
|
||||
commonName: z.string(),
|
||||
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
|
||||
maxPathLength: z.string()
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -36,75 +48,71 @@ export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["ca"]>;
|
||||
// handlePopUpOpen: (
|
||||
// popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
// data: {
|
||||
// identityId: string;
|
||||
// name: string;
|
||||
// authMethod?: IdentityAuthMethod;
|
||||
// }
|
||||
// ) => void;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
const caTypes = [
|
||||
{ label: "Root", value: CertificateAuthorityType.ROOT },
|
||||
{ label: "intermediate", value: CertificateAuthorityType.INTERMEDIATE }
|
||||
{ label: "Root", value: CaType.ROOT },
|
||||
{ label: "Intermediate", value: CaType.INTERMEDIATE }
|
||||
];
|
||||
|
||||
export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
console.log("CaModal currentWorkspace: ", currentWorkspace);
|
||||
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
|
||||
const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || "");
|
||||
const { mutateAsync: createMutateAsync } = useCreateCa();
|
||||
// const { data: roles } = useGetOrgRoles(orgId);
|
||||
// const { mutateAsync: createMutateAsync } = useCreateIdentity();
|
||||
// const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
||||
// const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
formState: { isSubmitting },
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
type: CertificateAuthorityType.ROOT,
|
||||
type: CaType.ROOT,
|
||||
organization: "",
|
||||
ou: "",
|
||||
country: "",
|
||||
province: "",
|
||||
locality: "",
|
||||
commonName: ""
|
||||
commonName: "",
|
||||
notAfter: getDateTenYearsFromToday(),
|
||||
maxPathLength: "-1"
|
||||
}
|
||||
});
|
||||
|
||||
// useEffect(() => {
|
||||
// const identity = popUp?.identity?.data as {
|
||||
// identityId: string;
|
||||
// name: string;
|
||||
// role: string;
|
||||
// customRole: {
|
||||
// name: string;
|
||||
// slug: string;
|
||||
// };
|
||||
// };
|
||||
const caType = watch("type");
|
||||
|
||||
// if (!roles?.length) return;
|
||||
|
||||
// if (identity) {
|
||||
// reset({
|
||||
// name: identity.name,
|
||||
// role: identity?.customRole?.slug ?? identity.role
|
||||
// });
|
||||
// } else {
|
||||
// reset({
|
||||
// name: "",
|
||||
// role: roles[0].slug
|
||||
// });
|
||||
// }
|
||||
// }, [popUp?.identity?.data, roles]);
|
||||
useEffect(() => {
|
||||
if (ca) {
|
||||
reset({
|
||||
type: ca.type,
|
||||
organization: ca.organization,
|
||||
ou: ca.ou,
|
||||
country: ca.country,
|
||||
province: ca.province,
|
||||
locality: ca.locality,
|
||||
commonName: ca.commonName,
|
||||
notAfter: ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "",
|
||||
maxPathLength: ca.maxPathLength ? String(ca.maxPathLength) : ""
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
type: CaType.ROOT,
|
||||
organization: "",
|
||||
ou: "",
|
||||
country: "",
|
||||
province: "",
|
||||
locality: "",
|
||||
commonName: "",
|
||||
notAfter: getDateTenYearsFromToday(),
|
||||
maxPathLength: "-1"
|
||||
});
|
||||
}
|
||||
}, [ca]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
type,
|
||||
@@ -113,18 +121,11 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
ou,
|
||||
country,
|
||||
locality,
|
||||
province
|
||||
province,
|
||||
notAfter,
|
||||
maxPathLength
|
||||
}: FormData) => {
|
||||
try {
|
||||
console.log("onFormSubmit args: ", {
|
||||
commonName,
|
||||
organization,
|
||||
ou,
|
||||
country,
|
||||
locality,
|
||||
province
|
||||
});
|
||||
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await createMutateAsync({
|
||||
@@ -135,90 +136,47 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
ou,
|
||||
country,
|
||||
province,
|
||||
locality
|
||||
});
|
||||
|
||||
// const identity = popUp?.identity?.data as {
|
||||
// identityId: string;
|
||||
// name: string;
|
||||
// role: string;
|
||||
// };
|
||||
|
||||
// if (identity) {
|
||||
// // update
|
||||
|
||||
// await updateMutateAsync({
|
||||
// identityId: identity.identityId,
|
||||
// name,
|
||||
// role: role || undefined,
|
||||
// organizationId: orgId
|
||||
// });
|
||||
|
||||
// handlePopUpToggle("identity", false);
|
||||
// } else {
|
||||
// // create
|
||||
|
||||
// const {
|
||||
// id: createdId,
|
||||
// name: createdName,
|
||||
// authMethod
|
||||
// } = await createMutateAsync({
|
||||
// name,
|
||||
// role: role || undefined,
|
||||
// organizationId: orgId
|
||||
// });
|
||||
|
||||
// await addMutateAsync({
|
||||
// organizationId: orgId,
|
||||
// identityId: createdId,
|
||||
// clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
// accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
// accessTokenTTL: 2592000,
|
||||
// accessTokenMaxTTL: 2592000,
|
||||
// accessTokenNumUsesLimit: 0
|
||||
// });
|
||||
|
||||
// handlePopUpToggle("identity", false);
|
||||
// handlePopUpOpen("identityAuthMethod", {
|
||||
// identityId: createdId,
|
||||
// name: createdName,
|
||||
// authMethod
|
||||
// });
|
||||
// }
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${popUp?.ca?.data ? "updated" : "created"} CA`,
|
||||
type: "success"
|
||||
locality,
|
||||
notAfter,
|
||||
maxPathLength: Number(maxPathLength)
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text =
|
||||
error?.response?.data?.message ?? `Failed to ${popUp?.ca?.data ? "update" : "create"} CA`;
|
||||
handlePopUpToggle("ca", false);
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
text: "Successfully created CA",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create CA",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// const getDefaultNotAfterDate = () => {
|
||||
// const date = new Date();
|
||||
// date.setFullYear(date.getFullYear() + 10);
|
||||
// return date;
|
||||
// };
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.ca?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("ca", isOpen);
|
||||
reset();
|
||||
handlePopUpToggle("ca", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${popUp?.ca?.data ? "Update" : "Create"} CA`}>
|
||||
<ModalContent title={`${ca ? "View" : "Create"} Private CA`}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="type"
|
||||
defaultValue={CertificateAuthorityType.ROOT}
|
||||
defaultValue={CaType.ROOT}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
@@ -226,7 +184,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
// isDisabled={!!identityAuthMethodData?.authMethod}
|
||||
isDisabled={Boolean(ca)}
|
||||
>
|
||||
{caTypes.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
@@ -237,6 +195,80 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{caType === CaType.ROOT && (
|
||||
<>
|
||||
{/* <Controller
|
||||
name="notAfter"
|
||||
control={control}
|
||||
defaultValue={getDefaultNotAfterDate()}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
label="Validity"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-4"
|
||||
>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={(date) => {
|
||||
onChange(date);
|
||||
setIsStartDatePickerOpen(false);
|
||||
}}
|
||||
popUpProps={{
|
||||
open: isStartDatePickerOpen,
|
||||
onOpenChange: setIsStartDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/> */}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="notAfter"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Valid Until"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="YYYY-MM-DD" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxPathLength"
|
||||
defaultValue="-1"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Path Length"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(ca)}
|
||||
>
|
||||
{[-1, 0, 1, 2, 3, 4].map((value) => (
|
||||
<SelectItem value={String(value)} key={`ca-path-length-${value}`}>
|
||||
{`${value}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
@@ -247,7 +279,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Acme Corp" />
|
||||
<Input {...field} placeholder="Acme Corp" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -261,7 +293,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Engineering" />
|
||||
<Input {...field} placeholder="Engineering" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -275,7 +307,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="United States (US)" />
|
||||
<Input {...field} placeholder="United States (US)" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -289,7 +321,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="California" />
|
||||
<Input {...field} placeholder="California" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -303,7 +335,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="San Francisco" />
|
||||
<Input {...field} placeholder="San Francisco" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -317,28 +349,30 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Example CA" />
|
||||
<Input {...field} placeholder="Example CA" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{popUp?.ca?.data ? "Update" : "Create"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("ca", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{!ca && (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{popUp?.ca?.data ? "Update" : "Create"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("ca", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
@@ -1,141 +1,126 @@
|
||||
// import { useState } from "react";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
// import { createNotification } from "@app/components/notifications";
|
||||
// import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button
|
||||
// DeleteActionModal,
|
||||
// EmailServiceSetupModal,
|
||||
// UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
// import {
|
||||
// OrgPermissionActions,
|
||||
// OrgPermissionSubjects,
|
||||
// useOrganization,
|
||||
// useSubscription
|
||||
// } from "@app/context";
|
||||
// import { useDeleteOrgMembership } from "@app/hooks/api";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { CaStatus,useDeleteCa, useUpdateCa } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CaCertModal } from "./CaCertModal";
|
||||
import { CaInstallCertModal } from "./CaInstallCertModal";
|
||||
import { CaModal } from "./CaModal";
|
||||
import { CaTable } from "./CaTable";
|
||||
// import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
||||
// import { OrgMembersTable } from "./OrgMembersTable";
|
||||
|
||||
export const CaSection = () => {
|
||||
// const { subscription } = useSubscription();
|
||||
// const { currentOrg } = useOrganization();
|
||||
// const orgId = currentOrg?.id ?? "";
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync: deleteCa } = useDeleteCa();
|
||||
const { mutateAsync: updateCa } = useUpdateCa();
|
||||
|
||||
// const [completeInviteLink, setCompleteInviteLink] = useState<string>("");
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"ca",
|
||||
"caCert",
|
||||
"installCaCert",
|
||||
"deleteCa",
|
||||
"caStatus" // enable / disable
|
||||
] as const);
|
||||
|
||||
const {
|
||||
popUp,
|
||||
handlePopUpOpen,
|
||||
// handlePopUpClose,
|
||||
handlePopUpToggle
|
||||
} = usePopUp(["ca"] as const);
|
||||
const onRemoveCaSubmit = async (caId: string) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
// const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
|
||||
await deleteCa({ caId, projectSlug: currentWorkspace.slug });
|
||||
|
||||
// const isMoreUsersNotAllowed = subscription?.memberLimit
|
||||
// ? subscription.membersUsed >= subscription.memberLimit
|
||||
// : false;
|
||||
await createNotification({
|
||||
text: "Successfully deleted CA",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
// const handleAddMemberModal = () => {
|
||||
// if (currentOrg?.authEnforced) {
|
||||
// createNotification({
|
||||
// text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
||||
// type: "error"
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
handlePopUpClose("deleteCa");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete CA",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// if (isMoreUsersNotAllowed) {
|
||||
// handlePopUpOpen("upgradePlan", {
|
||||
// description: "You can add more members if you upgrade your Infisical plan."
|
||||
// });
|
||||
// } else {
|
||||
// handlePopUpOpen("addMember");
|
||||
// }
|
||||
// };
|
||||
const onUpdateCaStatus = async ({ caId, status }: { caId: string; status: CaStatus }) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
// const onRemoveMemberSubmit = async (orgMembershipId: string) => {
|
||||
// try {
|
||||
// await deleteMutateAsync({
|
||||
// orgId,
|
||||
// membershipId: orgMembershipId
|
||||
// });
|
||||
await updateCa({ caId, projectSlug: currentWorkspace.slug, status });
|
||||
|
||||
// createNotification({
|
||||
// text: "Successfully removed user from org",
|
||||
// type: "success"
|
||||
// });
|
||||
// } catch (err) {
|
||||
// console.error(err);
|
||||
// createNotification({
|
||||
// text: "Failed to remove user from the organization",
|
||||
// type: "error"
|
||||
// });
|
||||
// }
|
||||
await createNotification({
|
||||
text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
// handlePopUpClose("removeMember");
|
||||
// };
|
||||
handlePopUpClose("caStatus");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Certificate Authorities</p>
|
||||
{/* <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => ( */}
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("ca")}
|
||||
// isDisabled={!isAllowed}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
Create CA
|
||||
</Button>
|
||||
{/* )} */}
|
||||
{/* </OrgPermissionCan> */}
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("ca")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create CA
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CaTable />
|
||||
{/* <OrgMembersTable
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
setCompleteInviteLink={setCompleteInviteLink}
|
||||
/>
|
||||
<AddOrgMemberModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
completeInviteLink={completeInviteLink}
|
||||
setCompleteInviteLink={setCompleteInviteLink}
|
||||
<CaInstallCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CaCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CaTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteCa.isOpen}
|
||||
title={`Are you sure want to remove the CA ${
|
||||
(popUp?.deleteCa?.data as { dn: string })?.dn || ""
|
||||
} from the project?`}
|
||||
subTitle="This action will delete other CAs and certificates below it in your CA hierarchy."
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteCa", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => onRemoveCaSubmit((popUp?.deleteCa?.data as { caId: string })?.caId)}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeMember.isOpen}
|
||||
title={`Are you sure want to remove member with username ${
|
||||
(popUp?.removeMember?.data as { username: string })?.username || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
||||
isOpen={popUp.caStatus.isOpen}
|
||||
title={`Are you sure want to ${
|
||||
(popUp?.caStatus?.data as { status: string })?.status === CaStatus.ACTIVE
|
||||
? "enable"
|
||||
: "disable"
|
||||
} the CA ${(popUp?.caStatus?.data as { dn: string })?.dn || ""} from the project?`}
|
||||
subTitle={
|
||||
(popUp?.caStatus?.data as { status: string })?.status === CaStatus.ACTIVE
|
||||
? "This action will allow the CA to start issuing certificates again."
|
||||
: "This action will prevent the CA from issuing new certificates."
|
||||
}
|
||||
onChange={(isOpen) => handlePopUpToggle("caStatus", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemoveMemberSubmit(
|
||||
(popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
|
||||
)
|
||||
onUpdateCaStatus(popUp?.caStatus?.data as { caId: string; status: CaStatus })
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/>
|
||||
<EmailServiceSetupModal
|
||||
isOpen={popUp.setUpEmail?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||
/> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import { faCertificate } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import {
|
||||
faBan,
|
||||
faCertificate,
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
@@ -9,15 +23,29 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useListWorkspaceCas } from "@app/hooks/api";
|
||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
Tooltip,
|
||||
Tr} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { CaStatus, useListWorkspaceCas } from "@app/hooks/api";
|
||||
import { caStatusToNameMap,caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
export const CaTable = () => {
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["installCaCert", "caCert", "ca", "deleteCa", "caStatus"]>,
|
||||
data?: {
|
||||
caId?: string;
|
||||
dn?: string;
|
||||
status?: CaStatus;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const CaTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useListWorkspaceCas(currentWorkspace?.slug ?? "");
|
||||
const { data, isLoading } = useListWorkspaceCas({
|
||||
projectSlug: currentWorkspace?.slug ?? ""
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
@@ -27,6 +55,8 @@ export const CaTable = () => {
|
||||
<Th>Subject</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Type</Th>
|
||||
<Th>Valid Until</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
@@ -38,15 +68,148 @@ export const CaTable = () => {
|
||||
return (
|
||||
<Tr className="h-10" key={`ca-${ca.id}`}>
|
||||
<Td>{ca.dn}</Td>
|
||||
<Td>Pending</Td>
|
||||
<Td>{caStatusToNameMap[ca.status]}</Td>
|
||||
<Td>{caTypeToNameMap[ca.type]}</Td>
|
||||
<Td>{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}</Td>
|
||||
<Td className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{ca.status === CaStatus.PENDING_CERTIFICATE && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
handlePopUpOpen("installCaCert", {
|
||||
caId: ca.id
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faCertificate} />}
|
||||
>
|
||||
Install Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
{ca.status !== CaStatus.PENDING_CERTIFICATE && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
handlePopUpOpen("caCert", {
|
||||
caId: ca.id
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faCertificate} />}
|
||||
>
|
||||
View Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("ca", {
|
||||
caId: ca.id
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEye} />}
|
||||
>
|
||||
View CA
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("caStatus", {
|
||||
caId: ca.id,
|
||||
status:
|
||||
ca.status === CaStatus.ACTIVE
|
||||
? CaStatus.DISABLED
|
||||
: CaStatus.ACTIVE
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faBan} />}
|
||||
>
|
||||
{`${ca.status === CaStatus.ACTIVE ? "Disable" : "Enable"} CA`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("deleteCa", {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete CA
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isLoading && data?.length === 0 && (
|
||||
<EmptyState title="No groups have been added to this project" icon={faCertificate} />
|
||||
<EmptyState title="No certificate authorities have been created" icon={faCertificate} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { CertificatesSection } from "./components";
|
||||
|
||||
export const CertificatesTab = () => {
|
||||
return (
|
||||
<motion.div
|
||||
key="panel-certificates"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<CertificatesSection />
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
import { useEffect } from "react";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { IconButton,Modal, ModalContent } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetCertCert } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["certificateCert"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificateCert"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { data } = useGetCertCert(
|
||||
(popUp?.certificateCert?.data as { certId: string })?.certId || ""
|
||||
);
|
||||
|
||||
const [isSerialNumberCopied, setIsSerialNumberCopied] = useToggle(false);
|
||||
const [isCertificateCopied, setIsCertificateCopied] = useToggle(false);
|
||||
const [isCertificateChainCopied, setIsCertificateChainCopied] = useToggle(false);
|
||||
const [isCertificateSkCopied, setIsCertificateSkCopied] = useToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isSerialNumberCopied) {
|
||||
timer = setTimeout(() => setIsSerialNumberCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isCertificateCopied) {
|
||||
timer = setTimeout(() => setIsCertificateCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isCertificateChainCopied) {
|
||||
timer = setTimeout(() => setIsCertificateChainCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isCertificateSkCopied) {
|
||||
timer = setTimeout(() => setIsCertificateSkCopied.off(), 2000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isSerialNumberCopied, isCertificateCopied, isCertificateChainCopied, isCertificateSkCopied]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificateCert?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("certificateCert", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Export Certificate">
|
||||
{data ? (
|
||||
<div>
|
||||
<h2 className="mb-4">Serial Number</h2>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{data.serialNumber}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.serialNumber);
|
||||
setIsSerialNumberCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSerialNumberCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Click to copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<h2 className="mb-4">Certificate Body</h2>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{data.certificate}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.certificate);
|
||||
setIsCertificateCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCertificateCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Click to copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<h2 className="mb-4">Certificate Chain</h2>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{data.certificateChain}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.certificateChain);
|
||||
setIsCertificateChainCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCertificateChainCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Click to copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<h2 className="mb-4">Certificate Private Key</h2>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{data.privateKey}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.certificateChain);
|
||||
setIsCertificateSkCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCertificateSkCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Click to copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,223 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { format } from "date-fns";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
CaStatus,
|
||||
useCreateCertificate,
|
||||
useGetCertById,
|
||||
useListWorkspaceCas
|
||||
} from "@app/hooks/api";
|
||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const isValidDate = (dateString: string) => {
|
||||
if (dateString === "") return true;
|
||||
const date = new Date(dateString);
|
||||
return !Number.isNaN(date.getTime());
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
caId: z.string(),
|
||||
commonName: z.string().trim().min(1),
|
||||
ttl: z.string().trim().optional(),
|
||||
notAfter: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine(isValidDate, { message: "Invalid date format" })
|
||||
.transform((val) => (val === "" ? undefined : val))
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["certificate"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificate"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: cert } = useGetCertById(
|
||||
(popUp?.certificate?.data as { certId: string })?.certId || ""
|
||||
);
|
||||
|
||||
const { data: cas } = useListWorkspaceCas({
|
||||
projectSlug: currentWorkspace?.slug ?? "",
|
||||
status: CaStatus.ACTIVE
|
||||
});
|
||||
|
||||
const { mutateAsync: createCertificate } = useCreateCertificate();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
setValue
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (cert) {
|
||||
reset({
|
||||
caId: cert.caId,
|
||||
commonName: cert.commonName,
|
||||
ttl: "",
|
||||
notAfter: format(new Date(cert.notAfter), "yyyy-MM-dd")
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
caId: "",
|
||||
commonName: "",
|
||||
ttl: "",
|
||||
notAfter: ""
|
||||
});
|
||||
}
|
||||
}, [cert]);
|
||||
|
||||
const onFormSubmit = async ({ caId, commonName, ttl, notAfter }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await createCertificate({
|
||||
projectSlug: currentWorkspace.slug,
|
||||
caId,
|
||||
commonName,
|
||||
ttl: ttl ? Number(ttl) : undefined,
|
||||
notBefore: new Date().toISOString(),
|
||||
notAfter
|
||||
});
|
||||
|
||||
reset();
|
||||
handlePopUpToggle("certificate", false);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created certificate",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create certificate",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (cas?.length) {
|
||||
setValue("caId", cas[0].id);
|
||||
}
|
||||
}, [cas, setValue]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificate?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("certificate", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${cert ? "View" : "Issue"} Certificate`}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
>
|
||||
{(cas || []).map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="commonName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Common Name (CN)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="Acme Corp" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="ttl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="86400" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="notAfter"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Valid Until" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="YYYY-MM-DD" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!cert && (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useDeleteCert } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateCertModal } from "./CertificateCertModal";
|
||||
import { CertificateModal } from "./CertificateModal";
|
||||
import { CertificatesTable } from "./CertificatesTable";
|
||||
|
||||
export const CertificatesSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync: deleteCert } = useDeleteCert();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"certificate",
|
||||
"certificateCert",
|
||||
"deleteCertificate"
|
||||
] as const);
|
||||
|
||||
const onRemoveCertificateSubmit = async (certId: string) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await deleteCert({ certId, projectSlug: currentWorkspace.slug });
|
||||
|
||||
await createNotification({
|
||||
text: "Successfully deleted certificate",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteCertificate");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete certificate",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Certificates</p>
|
||||
{/* <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => ( */}
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificate")}
|
||||
// isDisabled={!isAllowed}
|
||||
>
|
||||
Issue Certificate
|
||||
</Button>
|
||||
{/* )} */}
|
||||
{/* </OrgPermissionCan> */}
|
||||
</div>
|
||||
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteCertificate.isOpen}
|
||||
title={`Are you sure want to remove the certificate ${
|
||||
(popUp?.deleteCertificate?.data as { commonName: string })?.commonName || ""
|
||||
} from the project?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteCertificate", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemoveCertificateSubmit((popUp?.deleteCertificate?.data as { certId: string })?.certId)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
import {
|
||||
faCertificate,
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faFileExport,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useListWorkspaceCertificates } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["certificate", "deleteCertificate", "certificateCert"]>,
|
||||
data?: {
|
||||
certId?: string;
|
||||
commonName?: string;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useListWorkspaceCertificates(currentWorkspace?.slug ?? "");
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Certificate ID</Th>
|
||||
<Th>Common Name</Th>
|
||||
<Th>Valid Until</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={3} innerKey="project-cas" />}
|
||||
{!isLoading &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map((certificate) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
||||
<Td>{certificate.id}</Td>
|
||||
<Td>{certificate.commonName}</Td>
|
||||
<Td>
|
||||
{certificate.notAfter
|
||||
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
|
||||
: "-"}
|
||||
</Td>
|
||||
<Td className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("certificateCert", {
|
||||
certId: certificate.id
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faFileExport} />}
|
||||
>
|
||||
Export Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("certificate", {
|
||||
certId: certificate.id
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEye} />}
|
||||
>
|
||||
View Details
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("deleteCertificate", {
|
||||
certId: certificate.id,
|
||||
commonName: certificate.commonName
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isLoading && data?.length === 0 && (
|
||||
<EmptyState title="No certificates have been created" icon={faCertificate} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CertificatesSection } from "./CertificatesSection";
|
||||
@@ -0,0 +1 @@
|
||||
export { CertificatesTab } from "./CertificatesTab";
|
||||
@@ -1 +1,2 @@
|
||||
export { CaTab } from "./CaTab";
|
||||
export { CertificatesTab } from "./CertificatesTab";
|
||||
|
||||
Reference in New Issue
Block a user