mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-13 16:29:28 +00:00
Continue cert mgmt mvp
This commit is contained in:
Vendored
+2
@@ -29,6 +29,7 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
|||||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
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 { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
@@ -133,6 +134,7 @@ declare module "fastify" {
|
|||||||
ldap: TLdapConfigServiceFactory;
|
ldap: TLdapConfigServiceFactory;
|
||||||
auditLog: TAuditLogServiceFactory;
|
auditLog: TAuditLogServiceFactory;
|
||||||
auditLogStream: TAuditLogStreamServiceFactory;
|
auditLogStream: TAuditLogStreamServiceFactory;
|
||||||
|
certificate: TCertificateServiceFactory;
|
||||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||||
secretScanning: TSecretScanningServiceFactory;
|
secretScanning: TSecretScanningServiceFactory;
|
||||||
license: TLicenseServiceFactory;
|
license: TLicenseServiceFactory;
|
||||||
|
|||||||
Vendored
+20
@@ -41,6 +41,15 @@ import {
|
|||||||
TCertificateAuthoritySk,
|
TCertificateAuthoritySk,
|
||||||
TCertificateAuthoritySkInsert,
|
TCertificateAuthoritySkInsert,
|
||||||
TCertificateAuthoritySkUpdate,
|
TCertificateAuthoritySkUpdate,
|
||||||
|
TCertificateCerts,
|
||||||
|
TCertificateCertsInsert,
|
||||||
|
TCertificateCertsUpdate,
|
||||||
|
TCertificates,
|
||||||
|
TCertificateSecrets,
|
||||||
|
TCertificateSecretsInsert,
|
||||||
|
TCertificateSecretsUpdate,
|
||||||
|
TCertificatesInsert,
|
||||||
|
TCertificatesUpdate,
|
||||||
TDynamicSecretLeases,
|
TDynamicSecretLeases,
|
||||||
TDynamicSecretLeasesInsert,
|
TDynamicSecretLeasesInsert,
|
||||||
TDynamicSecretLeasesUpdate,
|
TDynamicSecretLeasesUpdate,
|
||||||
@@ -264,6 +273,17 @@ declare module "knex/types/tables" {
|
|||||||
TCertificateAuthoritySkInsert,
|
TCertificateAuthoritySkInsert,
|
||||||
TCertificateAuthoritySkUpdate
|
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<
|
[TableName.UserGroupMembership]: Knex.CompositeTableType<
|
||||||
TUserGroupMembership,
|
TUserGroupMembership,
|
||||||
TUserGroupMembershipInsert,
|
TUserGroupMembershipInsert,
|
||||||
|
|||||||
@@ -14,11 +14,23 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.string("projectId").notNullable();
|
t.string("projectId").notNullable();
|
||||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
t.string("type").notNullable(); // root / intermediate
|
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.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))) {
|
if (!(await knex.schema.hasTable(TableName.CertificateAuthorityCert))) {
|
||||||
|
// table to keep track of certificates belonging to CA
|
||||||
await knex.schema.createTable(TableName.CertificateAuthorityCert, (t) => {
|
await knex.schema.createTable(TableName.CertificateAuthorityCert, (t) => {
|
||||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
t.timestamps(true, true, true);
|
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.CertificateAuthority);
|
||||||
await createOnUpdateTrigger(knex, TableName.CertificateAuthorityCert);
|
await createOnUpdateTrigger(knex, TableName.CertificateAuthorityCert);
|
||||||
await createOnUpdateTrigger(knex, TableName.CertificateAuthoritySk);
|
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> {
|
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 knex.schema.dropTableIfExists(TableName.CertificateAuthoritySk);
|
||||||
await dropOnUpdateTrigger(knex, TableName.CertificateAuthoritySk);
|
await dropOnUpdateTrigger(knex, TableName.CertificateAuthoritySk);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,17 @@ export const CertificateAuthoritiesSchema = z.object({
|
|||||||
parentCaId: z.string().uuid().nullable().optional(),
|
parentCaId: z.string().uuid().nullable().optional(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
type: 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>;
|
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
|
||||||
|
|||||||
@@ -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>>;
|
||||||
@@ -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>>;
|
||||||
@@ -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-authorities";
|
||||||
export * from "./certificate-authority-certs";
|
export * from "./certificate-authority-certs";
|
||||||
export * from "./certificate-authority-sk";
|
export * from "./certificate-authority-sk";
|
||||||
|
export * from "./certificate-certs";
|
||||||
|
export * from "./certificate-secrets";
|
||||||
|
export * from "./certificates";
|
||||||
export * from "./dynamic-secret-leases";
|
export * from "./dynamic-secret-leases";
|
||||||
export * from "./dynamic-secrets";
|
export * from "./dynamic-secrets";
|
||||||
export * from "./git-app-install-sessions";
|
export * from "./git-app-install-sessions";
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export enum TableName {
|
|||||||
CertificateAuthority = "certificate_authorities",
|
CertificateAuthority = "certificate_authorities",
|
||||||
CertificateAuthorityCert = "certificate_authority_certs",
|
CertificateAuthorityCert = "certificate_authority_certs",
|
||||||
CertificateAuthoritySk = "certificate_authority_sk",
|
CertificateAuthoritySk = "certificate_authority_sk",
|
||||||
|
Certificate = "certificates",
|
||||||
|
CertificateCert = "certificate_certs",
|
||||||
|
CertificateSecret = "certificate_secrets",
|
||||||
Groups = "groups",
|
Groups = "groups",
|
||||||
GroupProjectMembership = "group_project_memberships",
|
GroupProjectMembership = "group_project_memberships",
|
||||||
GroupProjectMembershipRole = "group_project_membership_roles",
|
GroupProjectMembershipRole = "group_project_membership_roles",
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ export enum ProjectPermissionSub {
|
|||||||
SecretRollback = "secret-rollback",
|
SecretRollback = "secret-rollback",
|
||||||
SecretApproval = "secret-approval",
|
SecretApproval = "secret-approval",
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
Identity = "identity"
|
Identity = "identity",
|
||||||
|
CertificateAuthorities = "certificate-authorities",
|
||||||
|
Certificates = "certificates"
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubjectFields = {
|
type SubjectFields = {
|
||||||
@@ -53,6 +55,8 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
|
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||||
@@ -139,6 +143,17 @@ const buildAdminPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
|
||||||
can(ProjectPermissionActions.Delete, 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.Edit, ProjectPermissionSub.Project);
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||||
|
|
||||||
@@ -205,6 +220,14 @@ const buildMemberPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
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;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -229,6 +252,8 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
return rules;
|
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 { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
||||||
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
||||||
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { 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";
|
import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
// ca / certs
|
|
||||||
import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||||
import { certificateAuthoritySkDALFactory } from "@app/services/certificate-authority/certificate-authority-sk-dal";
|
import { certificateAuthoritySkDALFactory } from "@app/services/certificate-authority/certificate-authority-sk-dal";
|
||||||
@@ -226,16 +229,6 @@ export const registerRoutes = async (
|
|||||||
const trustedIpDAL = trustedIpDALFactory(db);
|
const trustedIpDAL = trustedIpDALFactory(db);
|
||||||
const telemetryDAL = telemetryDALFactory(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
|
// ee db layer ops
|
||||||
const permissionDAL = permissionDALFactory(db);
|
const permissionDAL = permissionDALFactory(db);
|
||||||
const samlConfigDAL = samlConfigDALFactory(db);
|
const samlConfigDAL = samlConfigDALFactory(db);
|
||||||
@@ -503,6 +496,34 @@ export const registerRoutes = async (
|
|||||||
projectUserMembershipRoleDAL
|
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({
|
const projectService = projectServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -520,6 +541,7 @@ export const registerRoutes = async (
|
|||||||
folderDAL,
|
folderDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
keyStore
|
keyStore
|
||||||
@@ -842,6 +864,7 @@ export const registerRoutes = async (
|
|||||||
ldap: ldapService,
|
ldap: ldapService,
|
||||||
auditLog: auditLogService,
|
auditLog: auditLogService,
|
||||||
auditLogStream: auditLogStreamService,
|
auditLogStream: auditLogStreamService,
|
||||||
|
certificate: certificateService,
|
||||||
certificateAuthority: certificateAuthorityService,
|
certificateAuthority: certificateAuthorityService,
|
||||||
secretScanning: secretScanningService,
|
secretScanning: secretScanningService,
|
||||||
license: licenseService,
|
license: licenseService,
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { CertificateAuthoritiesSchema } from "@app/db/schemas";
|
|||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
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) => {
|
export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -16,18 +17,38 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
description: "Create CA",
|
description: "Create CA",
|
||||||
body: z.object({
|
body: z
|
||||||
projectSlug: z.string().trim(),
|
.object({
|
||||||
type: z.enum([CAType.ROOT, CAType.INTERMEDIATE]),
|
projectSlug: z.string().trim(),
|
||||||
commonName: z.string().trim(),
|
type: z.enum([CaType.ROOT, CaType.INTERMEDIATE]),
|
||||||
organization: z.string().trim(),
|
commonName: z.string().trim(),
|
||||||
ou: z.string().trim(),
|
organization: z.string().trim(),
|
||||||
country: z.string().trim(),
|
ou: z.string().trim(),
|
||||||
province: z.string().trim(),
|
country: z.string().trim(),
|
||||||
locality: 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: {
|
response: {
|
||||||
200: CertificateAuthoritiesSchema
|
200: z.object({
|
||||||
|
ca: CertificateAuthoritiesSchema
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
@@ -38,7 +59,109 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
...req.body
|
...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({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: ":caId/certificate",
|
url: "/:caId/certificate",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
@@ -89,12 +212,13 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificate: z.string(),
|
certificate: z.string(),
|
||||||
certificateChain: z.string()
|
certificateChain: z.string(),
|
||||||
|
serialNumber: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { certificate, certificateChain } = await server.services.certificateAuthority.getCaCert({
|
const { certificate, certificateChain, serialNumber } = await server.services.certificateAuthority.getCaCert({
|
||||||
caId: req.params.caId,
|
caId: req.params.caId,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -103,49 +227,155 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
certificate,
|
certificate,
|
||||||
certificateChain
|
certificateChain,
|
||||||
|
serialNumber
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: ":caId/issue-certificate",
|
url: "/:caId/sign-intermediate",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
description: "Issue (leaf) certificate from CA",
|
description: "Create intermediate CA certificate from parent CA",
|
||||||
params: z.object({
|
params: z.object({
|
||||||
caId: z.string().trim()
|
caId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
csr: z.string().trim(),
|
csr: z.string().trim(),
|
||||||
notBefore: z.string().trim(),
|
notBefore: validateCaDateField.optional(),
|
||||||
notAfter: z.string().trim()
|
notAfter: validateCaDateField,
|
||||||
|
maxPathLength: z.number().min(-1).default(-1)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
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 () => {
|
handler: async (req) => {
|
||||||
// await server.services.certificateAuthority.issueCertFromCa({
|
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
||||||
// caId: req.params.caId,
|
await server.services.certificateAuthority.signIntermediate({
|
||||||
// actor: req.permission.type,
|
caId: req.params.caId,
|
||||||
// actorId: req.permission.id,
|
actor: req.permission.type,
|
||||||
// actorAuthMethod: req.permission.authMethod,
|
actorId: req.permission.id,
|
||||||
// actorOrgId: req.permission.orgId,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
// ...req.body
|
actorOrgId: req.permission.orgId,
|
||||||
// });
|
...req.body
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
certificateId: "111"
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
issuingCaCertificate,
|
||||||
|
serialNumber
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO 2: logic for creating intermediary ca
|
server.route({
|
||||||
// TODO 1: get certificate + certificate chain for the root and intermediary CA GET /ca/:caId/certificate
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 { registerAuthRoutes } from "./auth-router";
|
||||||
import { registerProjectBotRouter } from "./bot-router";
|
import { registerProjectBotRouter } from "./bot-router";
|
||||||
import { registerCaRouter } from "./certificate-authority-router";
|
import { registerCaRouter } from "./certificate-authority-router";
|
||||||
|
import { registerCertRouter } from "./certificate-router";
|
||||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||||
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
|
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
|
||||||
@@ -59,12 +60,8 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
);
|
);
|
||||||
|
|
||||||
await server.register(
|
await server.register(registerCaRouter, { prefix: "/ca" });
|
||||||
async (certRouter) => {
|
await server.register(registerCertRouter, { prefix: "/certificates" });
|
||||||
await certRouter.register(registerCaRouter);
|
|
||||||
},
|
|
||||||
{ prefix: "/ca" }
|
|
||||||
);
|
|
||||||
|
|
||||||
await server.register(registerProjectBotRouter, { prefix: "/bot" });
|
await server.register(registerProjectBotRouter, { prefix: "/bot" });
|
||||||
await server.register(registerIntegrationRouter, { prefix: "/integration" });
|
await server.register(registerIntegrationRouter, { prefix: "/integration" });
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { z } from "zod";
|
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 { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
import { ProjectFilterType } from "@app/services/project/project-types";
|
import { ProjectFilterType } from "@app/services/project/project-types";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
@@ -312,22 +313,58 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:slug/cas",
|
url: "/:slug/cas",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
slug: slugSchema.describe("The slug of the project to list CAs.")
|
slug: slugSchema.describe("The slug of the project to list CAs.")
|
||||||
}),
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional()
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
cas: z.array(CertificateAuthoritiesSchema)
|
cas: z.array(CertificateAuthoritiesSchema)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const cas = await server.services.project.listProjectCas({
|
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: {
|
filter: {
|
||||||
slug: req.params.slug,
|
slug: req.params.slug,
|
||||||
orgId: req.permission.orgId,
|
orgId: req.permission.orgId,
|
||||||
@@ -338,7 +375,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actor: req.permission.type
|
actor: req.permission.type
|
||||||
});
|
});
|
||||||
return { cas };
|
return { certificates };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,47 @@
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TCertificateAuthorityDALFactory = ReturnType<typeof certificateAuthorityDALFactory>;
|
export type TCertificateAuthorityDALFactory = ReturnType<typeof certificateAuthorityDALFactory>;
|
||||||
|
|
||||||
export const certificateAuthorityDALFactory = (db: TDbClient) => {
|
export const certificateAuthorityDALFactory = (db: TDbClient) => {
|
||||||
const caOrm = ormify(db, TableName.CertificateAuthority);
|
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 * as x509 from "@peculiar/x509";
|
||||||
import crypto, { KeyObject } from "crypto";
|
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 { 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 { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||||
|
import { createDistinguishedName } from "./certificate-authority-fns";
|
||||||
import { TCertificateAuthoritySkDALFactory } from "./certificate-authority-sk-dal";
|
import { TCertificateAuthoritySkDALFactory } from "./certificate-authority-sk-dal";
|
||||||
import {
|
import {
|
||||||
// TIssueCertFromCaDTO,
|
CaStatus,
|
||||||
CAType,
|
CaType,
|
||||||
TCreateCaDTO,
|
TCreateCaDTO,
|
||||||
|
TDeleteCaDTO,
|
||||||
TGetCaCertDTO,
|
TGetCaCertDTO,
|
||||||
TGetCaCsrDTO
|
TGetCaCsrDTO,
|
||||||
|
TGetCaDTO,
|
||||||
|
TImportCertToCaDTO,
|
||||||
|
TIssueCertFromCaDTO,
|
||||||
|
TSignIntermediateDTO,
|
||||||
|
TUpdateCaDTO
|
||||||
} from "./certificate-authority-types";
|
} from "./certificate-authority-types";
|
||||||
|
|
||||||
type TCertificateAuthorityServiceFactoryDep = {
|
type TCertificateAuthorityServiceFactoryDep = {
|
||||||
@@ -20,41 +33,27 @@ type TCertificateAuthorityServiceFactoryDep = {
|
|||||||
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
||||||
certificateAuthorityCertDAL: TCertificateAuthorityCertDALFactory;
|
certificateAuthorityCertDAL: TCertificateAuthorityCertDALFactory;
|
||||||
certificateAuthoritySkDAL: TCertificateAuthoritySkDALFactory;
|
certificateAuthoritySkDAL: TCertificateAuthoritySkDALFactory;
|
||||||
|
certificateDAL: TCertificateDALFactory;
|
||||||
|
certificateCertDAL: TCertificateCertDALFactory;
|
||||||
|
certificateSecretDAL: TCertificateSecretDALFactory;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
|
permissionService: TPermissionServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateAuthorityServiceFactory = ReturnType<typeof certificateAuthorityServiceFactory>;
|
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 = ({
|
export const certificateAuthorityServiceFactory = ({
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
certificateAuthoritySkDAL,
|
certificateAuthoritySkDAL,
|
||||||
projectDAL
|
certificateDAL,
|
||||||
|
certificateCertDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
permissionService
|
||||||
}: TCertificateAuthorityServiceFactoryDep) => {
|
}: TCertificateAuthorityServiceFactoryDep) => {
|
||||||
/**
|
/**
|
||||||
* Generates a new root or intermediate CA
|
* Generates a new root or intermediate CA
|
||||||
* @param param0
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
const createCa = async ({
|
const createCa = async ({
|
||||||
projectSlug,
|
projectSlug,
|
||||||
@@ -65,14 +64,30 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
country,
|
country,
|
||||||
province,
|
province,
|
||||||
locality,
|
locality,
|
||||||
// actorId,
|
notBefore,
|
||||||
// actorAuthMethod,
|
notAfter,
|
||||||
// actor,
|
maxPathLength,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
}: TCreateCaDTO) => {
|
}: TCreateCaDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
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({
|
const dn = createDistinguishedName({
|
||||||
commonName,
|
commonName,
|
||||||
organization,
|
organization,
|
||||||
@@ -97,25 +112,41 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
const pk = pkObj.export({ format: "pem", type: "spki" }) as string;
|
const pk = pkObj.export({ format: "pem", type: "spki" }) as string;
|
||||||
|
|
||||||
const newCa = await certificateAuthorityDAL.transaction(async (tx) => {
|
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(
|
const ca = await certificateAuthorityDAL.create(
|
||||||
{
|
{
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
type,
|
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
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
if (type === CAType.ROOT) {
|
if (type === CaType.ROOT) {
|
||||||
// note: self-signed cert only applicable for root CA
|
// note: self-signed cert only applicable for root CA
|
||||||
|
|
||||||
const cert = await x509.X509CertificateGenerator.createSelfSigned({
|
const cert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||||
name: dn,
|
name: dn,
|
||||||
notBefore: new Date("2020/01/01"),
|
notBefore: notBeforeDate,
|
||||||
notAfter: new Date("2020/01/02"),
|
notAfter: notAfterDate,
|
||||||
signingAlgorithm: alg,
|
signingAlgorithm: alg,
|
||||||
keys,
|
keys,
|
||||||
extensions: [
|
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),
|
new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
|
||||||
// eslint-disable-next-line no-bitwise
|
// eslint-disable-next-line no-bitwise
|
||||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
|
||||||
@@ -148,15 +179,94 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
return newCa;
|
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
|
* 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);
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
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 });
|
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 caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id });
|
||||||
|
|
||||||
const alg = {
|
const alg = {
|
||||||
@@ -176,10 +286,8 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
"verify"
|
"verify"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const cert = new x509.X509Certificate(caCert.certificate);
|
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||||
|
name: ca.dn,
|
||||||
const csr = await x509.Pkcs10CertificateRequestGenerator.create({
|
|
||||||
name: cert.subject || ca.dn,
|
|
||||||
keys: { privateKey: sk, publicKey: pk },
|
keys: { privateKey: sk, publicKey: pk },
|
||||||
signingAlgorithm: alg,
|
signingAlgorithm: alg,
|
||||||
extensions: [
|
extensions: [
|
||||||
@@ -189,80 +297,389 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
attributes: [new x509.ChallengePasswordAttribute("password")]
|
attributes: [new x509.ChallengePasswordAttribute("password")]
|
||||||
});
|
});
|
||||||
|
|
||||||
return csr.toString("base64");
|
return csrObj.toString("pem");
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return certificate and certificate chain for CA
|
* 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);
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
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 caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||||
|
const certObj = new x509.X509Certificate(caCert.certificate);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate: caCert.certificate,
|
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 () => {
|
const signIntermediate = async ({
|
||||||
// WIP: parse publicKey from CSR
|
caId,
|
||||||
// const csrArrayBuffer = Uint8Array.from(atob(csr), (c) => c.charCodeAt(0));
|
actorId,
|
||||||
// const csrR = new x509.Pkcs10CertificateRequest(csrArrayBuffer);
|
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);
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
// if (!ca) throw new BadRequestError({ message: "CA not found" });
|
actor,
|
||||||
// // TODO: permissioning
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
// const alg = {
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
// name: "RSASSA-PKCS1-v1_5",
|
ProjectPermissionActions.Create,
|
||||||
// hash: "SHA-256",
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
// publicExponent: new Uint8Array([1, 0, 1]),
|
);
|
||||||
// modulusLength: 2048
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||||
// const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id });
|
|
||||||
|
|
||||||
// const skObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" });
|
const alg = {
|
||||||
// const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
// "sign"
|
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({
|
const skObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" });
|
||||||
// // serialNumber: "03",
|
const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
|
||||||
// subject: "CN=Leaf", // TODO: make dynamic
|
"sign"
|
||||||
// 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)
|
|
||||||
// ]
|
|
||||||
// });
|
|
||||||
|
|
||||||
// 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 {
|
return {
|
||||||
createCa,
|
createCa,
|
||||||
|
getCaById,
|
||||||
|
updateCaById,
|
||||||
|
deleteCaById,
|
||||||
getCaCsr,
|
getCaCsr,
|
||||||
getCaCert,
|
getCaCert,
|
||||||
|
signIntermediate,
|
||||||
|
importCertToCa,
|
||||||
issueCertFromCa
|
issueCertFromCa
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,43 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export enum CAType {
|
export enum CaType {
|
||||||
ROOT = "root",
|
ROOT = "root",
|
||||||
INTERMEDIATE = "intermediate"
|
INTERMEDIATE = "intermediate"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CaStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
DISABLED = "disabled",
|
||||||
|
PENDING_CERTIFICATE = "pending-certificate"
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: attach permissions after draft impl
|
// TODO: attach permissions after draft impl
|
||||||
|
|
||||||
export type TCreateCaDTO = {
|
export type TCreateCaDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
type: CAType;
|
type: CaType;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
ou: string;
|
ou: string;
|
||||||
country: string;
|
country: string;
|
||||||
province: string;
|
province: string;
|
||||||
locality: 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">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TGetCaCsrDTO = {
|
export type TGetCaCsrDTO = {
|
||||||
@@ -26,9 +48,33 @@ export type TGetCaCertDTO = {
|
|||||||
caId: string;
|
caId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TIssueCertFromCaDTO = {
|
export type TSignIntermediateDTO = {
|
||||||
caId: string;
|
caId: string;
|
||||||
csr: string;
|
csr: string;
|
||||||
notBefore: string;
|
notBefore?: string;
|
||||||
notAfter: string;
|
notAfter: string;
|
||||||
|
maxPathLength: number;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & 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" });
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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 { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||||
@@ -51,6 +52,7 @@ export const DEFAULT_PROJECT_ENVS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
type TProjectServiceFactoryDep = {
|
type TProjectServiceFactoryDep = {
|
||||||
|
// TODO: Pick
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
projectQueue: TProjectQueueFactory;
|
projectQueue: TProjectQueueFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
@@ -65,6 +67,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||||
|
certificateDAL: TCertificateDALFactory;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
@@ -93,6 +96,7 @@ export const projectServiceFactory = ({
|
|||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
keyStore
|
keyStore
|
||||||
}: TProjectServiceFactoryDep) => {
|
}: TProjectServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
@@ -500,24 +504,66 @@ export const projectServiceFactory = ({
|
|||||||
* Return list of CAs for project
|
* Return list of CAs for project
|
||||||
*/
|
*/
|
||||||
const listProjectCas = async ({
|
const listProjectCas = async ({
|
||||||
// actorId,
|
status,
|
||||||
// actorOrgId,
|
actorId,
|
||||||
// actorAuthMethod,
|
actorOrgId,
|
||||||
filter // actor
|
actorAuthMethod,
|
||||||
|
filter,
|
||||||
|
actor
|
||||||
}: TListProjectCasDTO) => {
|
}: TListProjectCasDTO) => {
|
||||||
const project = await projectDAL.findProjectByFilter(filter);
|
const project = await projectDAL.findProjectByFilter(filter);
|
||||||
|
|
||||||
// const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
// actor,
|
actor,
|
||||||
// actorId,
|
actorId,
|
||||||
// project.id,
|
project.id,
|
||||||
// actorAuthMethod,
|
actorAuthMethod,
|
||||||
// actorOrgId
|
actorOrgId
|
||||||
// );
|
);
|
||||||
// TODO: add permissioning
|
|
||||||
|
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 });
|
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 {
|
return {
|
||||||
@@ -530,6 +576,7 @@ export const projectServiceFactory = ({
|
|||||||
toggleAutoCapitalization,
|
toggleAutoCapitalization,
|
||||||
updateName,
|
updateName,
|
||||||
upgradeProject,
|
upgradeProject,
|
||||||
listProjectCas
|
listProjectCas,
|
||||||
|
listProjectCertificates
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
import { CaStatus } from "../certificate-authority/certificate-authority-types";
|
||||||
|
|
||||||
export enum ProjectFilterType {
|
export enum ProjectFilterType {
|
||||||
ID = "id",
|
ID = "id",
|
||||||
@@ -77,5 +78,6 @@ export type AddUserToWsDTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TListProjectCasDTO = {
|
export type TListProjectCasDTO = {
|
||||||
|
status?: CaStatus;
|
||||||
filter: Filter;
|
filter: Filter;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export enum ProjectPermissionSub {
|
|||||||
SecretRollback = "secret-rollback",
|
SecretRollback = "secret-rollback",
|
||||||
SecretApproval = "secret-approval",
|
SecretApproval = "secret-approval",
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
Identity = "identity"
|
Identity = "identity",
|
||||||
|
CertificateAuthorities = "certificate-authorities",
|
||||||
|
Certificates = "certificates"
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubjectFields = {
|
type SubjectFields = {
|
||||||
@@ -51,6 +53,8 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
|
||||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { CertificateAuthorityType } from "./enums";
|
import { CaStatus,CaType } from "./enums";
|
||||||
|
|
||||||
export const caTypeToNameMap: { [K in CertificateAuthorityType]: string } = {
|
export const caTypeToNameMap: { [K in CaType]: string } = {
|
||||||
[CertificateAuthorityType.ROOT]: "Root",
|
[CaType.ROOT]: "Root",
|
||||||
[CertificateAuthorityType.INTERMEDIATE]: "Intermediate"
|
[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",
|
ROOT = "root",
|
||||||
INTERMEDIATE = "intermediate"
|
INTERMEDIATE = "intermediate"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CaStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
DISABLED = "disabled",
|
||||||
|
PENDING_CERTIFICATE = "pending-certificate"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
export { CertificateAuthorityType } from "./enums";
|
export { CaStatus,CaType } from "./enums";
|
||||||
export { useCreateCa } from "./mutations";
|
export {
|
||||||
|
useCreateCa,
|
||||||
|
useCreateCertificate,
|
||||||
|
useDeleteCa,
|
||||||
|
useImportCaCertificate,
|
||||||
|
useSignIntermediate,
|
||||||
|
useUpdateCa} from "./mutations";
|
||||||
|
export { useGetCaById, useGetCaCert, useGetCaCsr } from "./queries";
|
||||||
|
|||||||
@@ -1,23 +1,107 @@
|
|||||||
import {
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
useMutation
|
|
||||||
// useQueryClient
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
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 = () => {
|
export const useCreateCa = () => {
|
||||||
// const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<TCertificateAuthority, {}, TCreateCaDTO>({
|
return useMutation<TCertificateAuthority, {}, TCreateCaDTO>({
|
||||||
mutationFn: async (body) => {
|
mutationFn: async (body) => {
|
||||||
const {
|
const {
|
||||||
data: { identity }
|
data: { ca }
|
||||||
} = await apiRequest.post("/api/v1/ca/", body);
|
} = await apiRequest.post<{ ca: TCertificateAuthority }>("/api/v1/ca/", body);
|
||||||
return identity;
|
return ca;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: (_, { projectSlug }) => {
|
||||||
// queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
|
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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = {
|
export type TCertificateAuthority = {
|
||||||
id: string;
|
id: string;
|
||||||
parentCaId?: string;
|
parentCaId?: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
type: CertificateAuthorityType;
|
type: CaType;
|
||||||
dn: string;
|
status: CaStatus;
|
||||||
|
organization: string;
|
||||||
|
ou: string;
|
||||||
|
country: string;
|
||||||
|
province: string;
|
||||||
|
locality: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
|
dn: string;
|
||||||
|
maxPathLength?: number;
|
||||||
|
notAfter?: string;
|
||||||
|
notBefore?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
@@ -20,4 +29,62 @@ export type TCreateCaDTO = {
|
|||||||
province: string;
|
province: string;
|
||||||
locality: string;
|
locality: string;
|
||||||
commonName: 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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { useDeleteCert } from "./mutations";
|
||||||
|
export { useGetCertById, useGetCertCert } from "./queries";
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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)
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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 "./auth";
|
||||||
export * from "./bots";
|
export * from "./bots";
|
||||||
export * from "./ca";
|
export * from "./ca";
|
||||||
|
export * from "./certificates";
|
||||||
export * from "./dynamicSecret";
|
export * from "./dynamicSecret";
|
||||||
export * from "./dynamicSecretLease";
|
export * from "./dynamicSecretLease";
|
||||||
export * from "./groups";
|
export * from "./groups";
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export {
|
|||||||
useGetWorkspaceSecrets,
|
useGetWorkspaceSecrets,
|
||||||
useGetWorkspaceUsers,
|
useGetWorkspaceUsers,
|
||||||
useListWorkspaceCas,
|
useListWorkspaceCas,
|
||||||
|
useListWorkspaceCertificates,
|
||||||
useListWorkspaceGroups,
|
useListWorkspaceGroups,
|
||||||
useNameWorkspaceSecrets,
|
useNameWorkspaceSecrets,
|
||||||
useRenameWorkspace,
|
useRenameWorkspace,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { CaStatus } from "../ca/enums";
|
||||||
import { TCertificateAuthority } from "../ca/types";
|
import { TCertificateAuthority } from "../ca/types";
|
||||||
|
import { TCertificate } from "../certificates/types";
|
||||||
import { TGroupMembership } from "../groups/types";
|
import { TGroupMembership } from "../groups/types";
|
||||||
import { IdentityMembership } from "../identities/types";
|
import { IdentityMembership } from "../identities/types";
|
||||||
import { IntegrationAuth } from "../integrationAuth/types";
|
import { IntegrationAuth } from "../integrationAuth/types";
|
||||||
@@ -41,7 +43,12 @@ export const workspaceKeys = {
|
|||||||
[{ workspaceId }, "workspace-identity-memberships"] as const,
|
[{ workspaceId }, "workspace-identity-memberships"] as const,
|
||||||
getWorkspaceGroupMemberships: (workspaceId: string) =>
|
getWorkspaceGroupMemberships: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-groups"] as const,
|
[{ 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) => {
|
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({
|
return useQuery({
|
||||||
queryKey: workspaceKeys.getWorkspaceCas(projectSlug),
|
queryKey: workspaceKeys.specificWorkspaceCas({
|
||||||
|
projectSlug,
|
||||||
|
status
|
||||||
|
}),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
...(status && { status })
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { cas }
|
data: { cas }
|
||||||
} = await apiRequest.get<{ cas: TCertificateAuthority[] }>(
|
} = await apiRequest.get<{ cas: TCertificateAuthority[] }>(
|
||||||
`/api/v2/workspace/${projectSlug}/cas`
|
`/api/v2/workspace/${projectSlug}/cas`,
|
||||||
|
{
|
||||||
|
params
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return cas;
|
return cas;
|
||||||
},
|
},
|
||||||
enabled: Boolean(projectSlug)
|
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 (
|
return (
|
||||||
<div className="h-full bg-bunker-800">
|
<div className="h-full bg-bunker-800">
|
||||||
<Head>
|
<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" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
<meta property="og:image" content="/images/message.png" />
|
<meta property="og:image" content="/images/message.png" />
|
||||||
</Head>
|
</Head>
|
||||||
<div>Test CA page</div>
|
|
||||||
<CertificatesPage />
|
<CertificatesPage />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,15 +2,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
|||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
import { withProjectPermission } from "@app/hoc";
|
||||||
|
|
||||||
import { CaTab } from "./components";
|
import { CaTab, CertificatesTab } from "./components";
|
||||||
// import { CaTable } from "./components";
|
|
||||||
|
|
||||||
// TODO: fix permission
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO 1: CA section
|
|
||||||
* TODO 2: Certificates section
|
|
||||||
*/
|
|
||||||
|
|
||||||
enum TabSections {
|
enum TabSections {
|
||||||
Ca = "certificate-authorities",
|
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="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">
|
<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>
|
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Certificates</p>
|
||||||
<Tabs defaultValue={TabSections.Ca}>
|
<Tabs defaultValue={TabSections.Certificates}>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab value={TabSections.Ca}>Certificate Authorities</Tab>
|
|
||||||
<Tab value={TabSections.Certificates}>Certificates</Tab>
|
<Tab value={TabSections.Certificates}>Certificates</Tab>
|
||||||
|
<Tab value={TabSections.Ca}>Certificate Authorities</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
<TabPanel value={TabSections.Certificates}>
|
||||||
|
<CertificatesTab />
|
||||||
|
</TabPanel>
|
||||||
<TabPanel value={TabSections.Ca}>
|
<TabPanel value={TabSections.Ca}>
|
||||||
<CaTab />
|
<CaTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value={TabSections.Certificates}>
|
|
||||||
<div>Certs</div>
|
|
||||||
{/* <OrgGroupsTab /> */}
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+109
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+328
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { CaInstallCertModal } from "./CaInstallCertModal";
|
||||||
+181
-147
@@ -1,8 +1,7 @@
|
|||||||
// import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
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 { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { format } from "date-fns";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
@@ -14,21 +13,34 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem
|
||||||
|
// DatePicker
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { CertificateAuthorityType, useCreateCa } from "@app/hooks/api/ca";
|
import { CaType, useCreateCa, useGetCaById } from "@app/hooks/api/ca";
|
||||||
// import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
|
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
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
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
type: z.enum(["root", "intermediate"]), // move to ref enum of hooks/api
|
type: z.enum([CaType.ROOT, CaType.INTERMEDIATE]),
|
||||||
organization: z.string(),
|
organization: z.string(),
|
||||||
ou: z.string(),
|
ou: z.string(),
|
||||||
country: z.string(),
|
country: z.string(),
|
||||||
province: z.string(),
|
province: z.string(),
|
||||||
locality: 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();
|
.required();
|
||||||
|
|
||||||
@@ -36,75 +48,71 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["ca"]>;
|
popUp: UsePopUpState<["ca"]>;
|
||||||
// handlePopUpOpen: (
|
|
||||||
// popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
|
||||||
// data: {
|
|
||||||
// identityId: string;
|
|
||||||
// name: string;
|
|
||||||
// authMethod?: IdentityAuthMethod;
|
|
||||||
// }
|
|
||||||
// ) => void;
|
|
||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void;
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const caTypes = [
|
const caTypes = [
|
||||||
{ label: "Root", value: CertificateAuthorityType.ROOT },
|
{ label: "Root", value: CaType.ROOT },
|
||||||
{ label: "intermediate", value: CertificateAuthorityType.INTERMEDIATE }
|
{ label: "Intermediate", value: CaType.INTERMEDIATE }
|
||||||
];
|
];
|
||||||
|
|
||||||
export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
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 { mutateAsync: createMutateAsync } = useCreateCa();
|
||||||
// const { data: roles } = useGetOrgRoles(orgId);
|
|
||||||
// const { mutateAsync: createMutateAsync } = useCreateIdentity();
|
|
||||||
// const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
|
||||||
// const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting },
|
||||||
|
watch
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: CertificateAuthorityType.ROOT,
|
type: CaType.ROOT,
|
||||||
organization: "",
|
organization: "",
|
||||||
ou: "",
|
ou: "",
|
||||||
country: "",
|
country: "",
|
||||||
province: "",
|
province: "",
|
||||||
locality: "",
|
locality: "",
|
||||||
commonName: ""
|
commonName: "",
|
||||||
|
notAfter: getDateTenYearsFromToday(),
|
||||||
|
maxPathLength: "-1"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// useEffect(() => {
|
const caType = watch("type");
|
||||||
// const identity = popUp?.identity?.data as {
|
|
||||||
// identityId: string;
|
|
||||||
// name: string;
|
|
||||||
// role: string;
|
|
||||||
// customRole: {
|
|
||||||
// name: string;
|
|
||||||
// slug: string;
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (!roles?.length) return;
|
useEffect(() => {
|
||||||
|
if (ca) {
|
||||||
// if (identity) {
|
reset({
|
||||||
// reset({
|
type: ca.type,
|
||||||
// name: identity.name,
|
organization: ca.organization,
|
||||||
// role: identity?.customRole?.slug ?? identity.role
|
ou: ca.ou,
|
||||||
// });
|
country: ca.country,
|
||||||
// } else {
|
province: ca.province,
|
||||||
// reset({
|
locality: ca.locality,
|
||||||
// name: "",
|
commonName: ca.commonName,
|
||||||
// role: roles[0].slug
|
notAfter: ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "",
|
||||||
// });
|
maxPathLength: ca.maxPathLength ? String(ca.maxPathLength) : ""
|
||||||
// }
|
});
|
||||||
// }, [popUp?.identity?.data, roles]);
|
} else {
|
||||||
|
reset({
|
||||||
|
type: CaType.ROOT,
|
||||||
|
organization: "",
|
||||||
|
ou: "",
|
||||||
|
country: "",
|
||||||
|
province: "",
|
||||||
|
locality: "",
|
||||||
|
commonName: "",
|
||||||
|
notAfter: getDateTenYearsFromToday(),
|
||||||
|
maxPathLength: "-1"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [ca]);
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
const onFormSubmit = async ({
|
||||||
type,
|
type,
|
||||||
@@ -113,18 +121,11 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
ou,
|
ou,
|
||||||
country,
|
country,
|
||||||
locality,
|
locality,
|
||||||
province
|
province,
|
||||||
|
notAfter,
|
||||||
|
maxPathLength
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
console.log("onFormSubmit args: ", {
|
|
||||||
commonName,
|
|
||||||
organization,
|
|
||||||
ou,
|
|
||||||
country,
|
|
||||||
locality,
|
|
||||||
province
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!currentWorkspace?.slug) return;
|
if (!currentWorkspace?.slug) return;
|
||||||
|
|
||||||
await createMutateAsync({
|
await createMutateAsync({
|
||||||
@@ -135,90 +136,47 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
ou,
|
ou,
|
||||||
country,
|
country,
|
||||||
province,
|
province,
|
||||||
locality
|
locality,
|
||||||
});
|
notAfter,
|
||||||
|
maxPathLength: Number(maxPathLength)
|
||||||
// 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"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
} catch (err) {
|
handlePopUpToggle("ca", false);
|
||||||
console.error(err);
|
|
||||||
const error = err as any;
|
|
||||||
const text =
|
|
||||||
error?.response?.data?.message ?? `Failed to ${popUp?.ca?.data ? "update" : "create"} CA`;
|
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text,
|
text: "Successfully created CA",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create CA",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const getDefaultNotAfterDate = () => {
|
||||||
|
// const date = new Date();
|
||||||
|
// date.setFullYear(date.getFullYear() + 10);
|
||||||
|
// return date;
|
||||||
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.ca?.isOpen}
|
isOpen={popUp?.ca?.isOpen}
|
||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
handlePopUpToggle("ca", isOpen);
|
|
||||||
reset();
|
reset();
|
||||||
|
handlePopUpToggle("ca", isOpen);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent title={`${popUp?.ca?.data ? "Update" : "Create"} CA`}>
|
<ModalContent title={`${ca ? "View" : "Create"} Private CA`}>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="type"
|
name="type"
|
||||||
defaultValue={CertificateAuthorityType.ROOT}
|
defaultValue={CaType.ROOT}
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
|
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
|
||||||
<Select
|
<Select
|
||||||
@@ -226,7 +184,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onValueChange={(e) => onChange(e)}
|
onValueChange={(e) => onChange(e)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
// isDisabled={!!identityAuthMethodData?.authMethod}
|
isDisabled={Boolean(ca)}
|
||||||
>
|
>
|
||||||
{caTypes.map(({ label, value }) => (
|
{caTypes.map(({ label, value }) => (
|
||||||
<SelectItem value={String(value || "")} key={label}>
|
<SelectItem value={String(value || "")} key={label}>
|
||||||
@@ -237,6 +195,80 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
</FormControl>
|
</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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
@@ -247,7 +279,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="Acme Corp" />
|
<Input {...field} placeholder="Acme Corp" isDisabled={Boolean(ca)} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -261,7 +293,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="Engineering" />
|
<Input {...field} placeholder="Engineering" isDisabled={Boolean(ca)} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -275,7 +307,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="United States (US)" />
|
<Input {...field} placeholder="United States (US)" isDisabled={Boolean(ca)} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -289,7 +321,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="California" />
|
<Input {...field} placeholder="California" isDisabled={Boolean(ca)} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -303,7 +335,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="San Francisco" />
|
<Input {...field} placeholder="San Francisco" isDisabled={Boolean(ca)} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -317,28 +349,30 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="Example CA" />
|
<Input {...field} placeholder="Example CA" isDisabled={Boolean(ca)} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center">
|
{!ca && (
|
||||||
<Button
|
<div className="flex items-center">
|
||||||
className="mr-4"
|
<Button
|
||||||
size="sm"
|
className="mr-4"
|
||||||
type="submit"
|
size="sm"
|
||||||
isLoading={isSubmitting}
|
type="submit"
|
||||||
isDisabled={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
>
|
isDisabled={isSubmitting}
|
||||||
{popUp?.ca?.data ? "Update" : "Create"}
|
>
|
||||||
</Button>
|
{popUp?.ca?.data ? "Update" : "Create"}
|
||||||
<Button
|
</Button>
|
||||||
colorSchema="secondary"
|
<Button
|
||||||
variant="plain"
|
colorSchema="secondary"
|
||||||
onClick={() => handlePopUpToggle("ca", false)}
|
variant="plain"
|
||||||
>
|
onClick={() => handlePopUpToggle("ca", false)}
|
||||||
Cancel
|
>
|
||||||
</Button>
|
Cancel
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
+91
-106
@@ -1,141 +1,126 @@
|
|||||||
// import { useState } from "react";
|
|
||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
// import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
// import { OrgPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
Button
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
// DeleteActionModal,
|
import { CaStatus,useDeleteCa, useUpdateCa } from "@app/hooks/api";
|
||||||
// EmailServiceSetupModal,
|
|
||||||
// UpgradePlanModal
|
|
||||||
} from "@app/components/v2";
|
|
||||||
// import {
|
|
||||||
// OrgPermissionActions,
|
|
||||||
// OrgPermissionSubjects,
|
|
||||||
// useOrganization,
|
|
||||||
// useSubscription
|
|
||||||
// } from "@app/context";
|
|
||||||
// import { useDeleteOrgMembership } from "@app/hooks/api";
|
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { CaCertModal } from "./CaCertModal";
|
||||||
|
import { CaInstallCertModal } from "./CaInstallCertModal";
|
||||||
import { CaModal } from "./CaModal";
|
import { CaModal } from "./CaModal";
|
||||||
import { CaTable } from "./CaTable";
|
import { CaTable } from "./CaTable";
|
||||||
// import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
|
||||||
// import { OrgMembersTable } from "./OrgMembersTable";
|
|
||||||
|
|
||||||
export const CaSection = () => {
|
export const CaSection = () => {
|
||||||
// const { subscription } = useSubscription();
|
const { currentWorkspace } = useWorkspace();
|
||||||
// const { currentOrg } = useOrganization();
|
const { mutateAsync: deleteCa } = useDeleteCa();
|
||||||
// const orgId = currentOrg?.id ?? "";
|
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 {
|
const onRemoveCaSubmit = async (caId: string) => {
|
||||||
popUp,
|
try {
|
||||||
handlePopUpOpen,
|
if (!currentWorkspace?.slug) return;
|
||||||
// handlePopUpClose,
|
|
||||||
handlePopUpToggle
|
|
||||||
} = usePopUp(["ca"] as const);
|
|
||||||
|
|
||||||
// const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
|
await deleteCa({ caId, projectSlug: currentWorkspace.slug });
|
||||||
|
|
||||||
// const isMoreUsersNotAllowed = subscription?.memberLimit
|
await createNotification({
|
||||||
// ? subscription.membersUsed >= subscription.memberLimit
|
text: "Successfully deleted CA",
|
||||||
// : false;
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
// const handleAddMemberModal = () => {
|
handlePopUpClose("deleteCa");
|
||||||
// if (currentOrg?.authEnforced) {
|
} catch (err) {
|
||||||
// createNotification({
|
console.error(err);
|
||||||
// text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
createNotification({
|
||||||
// type: "error"
|
text: "Failed to delete CA",
|
||||||
// });
|
type: "error"
|
||||||
// return;
|
});
|
||||||
// }
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// if (isMoreUsersNotAllowed) {
|
const onUpdateCaStatus = async ({ caId, status }: { caId: string; status: CaStatus }) => {
|
||||||
// handlePopUpOpen("upgradePlan", {
|
try {
|
||||||
// description: "You can add more members if you upgrade your Infisical plan."
|
if (!currentWorkspace?.slug) return;
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// handlePopUpOpen("addMember");
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const onRemoveMemberSubmit = async (orgMembershipId: string) => {
|
await updateCa({ caId, projectSlug: currentWorkspace.slug, status });
|
||||||
// try {
|
|
||||||
// await deleteMutateAsync({
|
|
||||||
// orgId,
|
|
||||||
// membershipId: orgMembershipId
|
|
||||||
// });
|
|
||||||
|
|
||||||
// createNotification({
|
await createNotification({
|
||||||
// text: "Successfully removed user from org",
|
text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
|
||||||
// type: "success"
|
type: "success"
|
||||||
// });
|
});
|
||||||
// } catch (err) {
|
|
||||||
// console.error(err);
|
|
||||||
// createNotification({
|
|
||||||
// text: "Failed to remove user from the organization",
|
|
||||||
// type: "error"
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// handlePopUpClose("removeMember");
|
handlePopUpClose("caStatus");
|
||||||
// };
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Certificate Authorities</p>
|
<p className="text-xl font-semibold text-mineshaft-100">Certificate Authorities</p>
|
||||||
{/* <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
<ProjectPermissionCan
|
||||||
{(isAllowed) => ( */}
|
I={ProjectPermissionActions.Create}
|
||||||
<Button
|
a={ProjectPermissionSub.CertificateAuthorities}
|
||||||
colorSchema="primary"
|
|
||||||
type="submit"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => handlePopUpOpen("ca")}
|
|
||||||
// isDisabled={!isAllowed}
|
|
||||||
>
|
>
|
||||||
Create CA
|
{(isAllowed) => (
|
||||||
</Button>
|
<Button
|
||||||
{/* )} */}
|
colorSchema="primary"
|
||||||
{/* </OrgPermissionCan> */}
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen("ca")}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Create CA
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
<CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<CaTable />
|
<CaInstallCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
{/* <OrgMembersTable
|
<CaCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
<CaTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
setCompleteInviteLink={setCompleteInviteLink}
|
<DeleteActionModal
|
||||||
/>
|
isOpen={popUp.deleteCa.isOpen}
|
||||||
<AddOrgMemberModal
|
title={`Are you sure want to remove the CA ${
|
||||||
popUp={popUp}
|
(popUp?.deleteCa?.data as { dn: string })?.dn || ""
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
} from the project?`}
|
||||||
completeInviteLink={completeInviteLink}
|
subTitle="This action will delete other CAs and certificates below it in your CA hierarchy."
|
||||||
setCompleteInviteLink={setCompleteInviteLink}
|
onChange={(isOpen) => handlePopUpToggle("deleteCa", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() => onRemoveCaSubmit((popUp?.deleteCa?.data as { caId: string })?.caId)}
|
||||||
/>
|
/>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.removeMember.isOpen}
|
isOpen={popUp.caStatus.isOpen}
|
||||||
title={`Are you sure want to remove member with username ${
|
title={`Are you sure want to ${
|
||||||
(popUp?.removeMember?.data as { username: string })?.username || ""
|
(popUp?.caStatus?.data as { status: string })?.status === CaStatus.ACTIVE
|
||||||
}?`}
|
? "enable"
|
||||||
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
: "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"
|
deleteKey="confirm"
|
||||||
onDeleteApproved={() =>
|
onDeleteApproved={() =>
|
||||||
onRemoveMemberSubmit(
|
onUpdateCaStatus(popUp?.caStatus?.data as { caId: string; status: CaStatus })
|
||||||
(popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+174
-11
@@ -1,6 +1,20 @@
|
|||||||
import { faCertificate } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
|
|
||||||
import {
|
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,
|
EmptyState,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
@@ -9,15 +23,29 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr
|
Tooltip,
|
||||||
} from "@app/components/v2";
|
Tr} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { useListWorkspaceCas } from "@app/hooks/api";
|
import { CaStatus, useListWorkspaceCas } from "@app/hooks/api";
|
||||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
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 { currentWorkspace } = useWorkspace();
|
||||||
const { data, isLoading } = useListWorkspaceCas(currentWorkspace?.slug ?? "");
|
const { data, isLoading } = useListWorkspaceCas({
|
||||||
|
projectSlug: currentWorkspace?.slug ?? ""
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
@@ -27,6 +55,8 @@ export const CaTable = () => {
|
|||||||
<Th>Subject</Th>
|
<Th>Subject</Th>
|
||||||
<Th>Status</Th>
|
<Th>Status</Th>
|
||||||
<Th>Type</Th>
|
<Th>Type</Th>
|
||||||
|
<Th>Valid Until</Th>
|
||||||
|
<Th />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
@@ -38,15 +68,148 @@ export const CaTable = () => {
|
|||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`ca-${ca.id}`}>
|
<Tr className="h-10" key={`ca-${ca.id}`}>
|
||||||
<Td>{ca.dn}</Td>
|
<Td>{ca.dn}</Td>
|
||||||
<Td>Pending</Td>
|
<Td>{caStatusToNameMap[ca.status]}</Td>
|
||||||
<Td>{caTypeToNameMap[ca.type]}</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>
|
</Tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
{!isLoading && data?.length === 0 && (
|
{!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>
|
</TableContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+17
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+135
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+223
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+79
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+160
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { CertificatesSection } from "./CertificatesSection";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { CertificatesTab } from "./CertificatesTab";
|
||||||
@@ -1 +1,2 @@
|
|||||||
export { CaTab } from "./CaTab";
|
export { CaTab } from "./CaTab";
|
||||||
|
export { CertificatesTab } from "./CertificatesTab";
|
||||||
|
|||||||
Reference in New Issue
Block a user