Rename pki alerting structures

This commit is contained in:
Tuan Dang
2024-08-09 08:27:53 -07:00
parent 556e4d62c4
commit fc4a20caf2
54 changed files with 1443 additions and 161 deletions
+2 -2
View File
@@ -27,7 +27,6 @@ import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
import { TAlertServiceFactory } from "@app/services/alert/alert-service";
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
@@ -52,6 +51,7 @@ import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/i
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
import { TOrgServiceFactory } from "@app/services/org/org-service";
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
import { TProjectServiceFactory } from "@app/services/project/project-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@@ -116,7 +116,7 @@ declare module "fastify" {
group: TGroupServiceFactory;
groupProject: TGroupProjectServiceFactory;
apiKey: TApiKeyServiceFactory;
alert: TAlertServiceFactory;
pkiAlert: TPkiAlertServiceFactory;
project: TProjectServiceFactory;
projectMembership: TProjectMembershipServiceFactory;
projectEnv: TProjectEnvServiceFactory;
+12 -4
View File
@@ -14,9 +14,6 @@ import {
TAccessApprovalRequestsReviewersInsert,
TAccessApprovalRequestsReviewersUpdate,
TAccessApprovalRequestsUpdate,
TAlerts,
TAlertsInsert,
TAlertsUpdate,
TApiKeys,
TApiKeysInsert,
TApiKeysUpdate,
@@ -164,6 +161,12 @@ import {
TOrgRoles,
TOrgRolesInsert,
TOrgRolesUpdate,
TPkiAlerts,
TPkiAlertsInsert,
TPkiAlertsUpdate,
TPkiCollectionItems,
TPkiCollectionItemsInsert,
TPkiCollectionItemsUpdate,
TPkiCollections,
TPkiCollectionsInsert,
TPkiCollectionsUpdate,
@@ -371,11 +374,17 @@ declare module "knex/types/tables" {
TCertificateSecretsInsert,
TCertificateSecretsUpdate
>;
[TableName.PkiAlert]: KnexOriginal.CompositeTableType<TPkiAlerts, TPkiAlertsInsert, TPkiAlertsUpdate>;
[TableName.PkiCollection]: KnexOriginal.CompositeTableType<
TPkiCollections,
TPkiCollectionsInsert,
TPkiCollectionsUpdate
>;
[TableName.PkiCollectionItem]: KnexOriginal.CompositeTableType<
TPkiCollectionItems,
TPkiCollectionItemsInsert,
TPkiCollectionItemsUpdate
>;
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
TUserGroupMembership,
TUserGroupMembershipInsert,
@@ -427,7 +436,6 @@ declare module "knex/types/tables" {
[TableName.UserAction]: KnexOriginal.CompositeTableType<TUserActions, TUserActionsInsert, TUserActionsUpdate>;
[TableName.SuperAdmin]: KnexOriginal.CompositeTableType<TSuperAdmin, TSuperAdminInsert, TSuperAdminUpdate>;
[TableName.ApiKey]: KnexOriginal.CompositeTableType<TApiKeys, TApiKeysInsert, TApiKeysUpdate>;
[TableName.Alert]: KnexOriginal.CompositeTableType<TAlerts, TAlertsInsert, TAlertsUpdate>;
[TableName.Project]: KnexOriginal.CompositeTableType<TProjects, TProjectsInsert, TProjectsUpdate>;
[TableName.ProjectMembership]: KnexOriginal.CompositeTableType<
TProjectMemberships,
@@ -14,9 +14,21 @@ export async function up(knex: Knex): Promise<void> {
});
}
if (!(await knex.schema.hasTable(TableName.Alert))) {
// TODO: rename to pki alert
await knex.schema.createTable(TableName.Alert, (t) => {
if (!(await knex.schema.hasTable(TableName.PkiCollectionItem))) {
await knex.schema.createTable(TableName.PkiCollectionItem, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.uuid("pkiCollectionId").notNullable();
t.foreign("pkiCollectionId").references("id").inTable(TableName.PkiCollection).onDelete("CASCADE");
t.uuid("caId").nullable();
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
t.uuid("certId").nullable();
t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
});
}
if (!(await knex.schema.hasTable(TableName.PkiAlert))) {
await knex.schema.createTable(TableName.PkiAlert, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.string("projectId").notNullable();
@@ -30,13 +42,16 @@ export async function up(knex: Knex): Promise<void> {
}
await createOnUpdateTrigger(knex, TableName.PkiCollection);
await createOnUpdateTrigger(knex, TableName.Alert);
await createOnUpdateTrigger(knex, TableName.PkiAlert);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.PkiAlert);
await dropOnUpdateTrigger(knex, TableName.PkiAlert);
await knex.schema.dropTableIfExists(TableName.PkiCollectionItem);
await dropOnUpdateTrigger(knex, TableName.PkiCollectionItem);
await knex.schema.dropTableIfExists(TableName.PkiCollection);
await dropOnUpdateTrigger(knex, TableName.PkiCollection);
await knex.schema.dropTableIfExists(TableName.Alert);
await dropOnUpdateTrigger(knex, TableName.Alert);
}
+6 -4
View File
@@ -5,8 +5,6 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const DynamicSecretsSchema = z.object({
@@ -16,12 +14,16 @@ export const DynamicSecretsSchema = z.object({
type: z.string(),
defaultTTL: z.string(),
maxTTL: z.string().nullable().optional(),
inputIV: z.string(),
inputCiphertext: z.string(),
inputTag: z.string(),
algorithm: z.string().default("aes-256-gcm"),
keyEncoding: z.string().default("utf8"),
folderId: z.string().uuid(),
status: z.string().nullable().optional(),
statusDetails: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
encryptedConfig: zodBuffer
updatedAt: z.date()
});
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
+2 -1
View File
@@ -2,7 +2,6 @@ export * from "./access-approval-policies";
export * from "./access-approval-policies-approvers";
export * from "./access-approval-requests";
export * from "./access-approval-requests-reviewers";
export * from "./alerts";
export * from "./api-keys";
export * from "./audit-log-streams";
export * from "./audit-logs";
@@ -53,6 +52,8 @@ export * from "./org-bots";
export * from "./org-memberships";
export * from "./org-roles";
export * from "./organizations";
export * from "./pki-alerts";
export * from "./pki-collection-items";
export * from "./pki-collections";
export * from "./project-bots";
export * from "./project-environments";
+2 -1
View File
@@ -9,8 +9,9 @@ export enum TableName {
Certificate = "certificates",
CertificateBody = "certificate_bodies",
CertificateSecret = "certificate_secrets",
Alert = "alerts", // TODO: rename
PkiAlert = "pki_alerts",
PkiCollection = "pki_collections",
PkiCollectionItem = "pki_collection_items",
Groups = "groups",
GroupProjectMembership = "group_project_memberships",
GroupProjectMembershipRole = "group_project_membership_roles",
@@ -7,7 +7,7 @@ import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const AlertsSchema = z.object({
export const PkiAlertsSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
@@ -18,6 +18,6 @@ export const AlertsSchema = z.object({
recipientEmails: z.string()
});
export type TAlerts = z.infer<typeof AlertsSchema>;
export type TAlertsInsert = Omit<z.input<typeof AlertsSchema>, TImmutableDBKeys>;
export type TAlertsUpdate = Partial<Omit<z.input<typeof AlertsSchema>, TImmutableDBKeys>>;
export type TPkiAlerts = z.infer<typeof PkiAlertsSchema>;
export type TPkiAlertsInsert = Omit<z.input<typeof PkiAlertsSchema>, TImmutableDBKeys>;
export type TPkiAlertsUpdate = Partial<Omit<z.input<typeof PkiAlertsSchema>, 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 PkiCollectionItemsSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
pkiCollectionId: z.string().uuid(),
caId: z.string().uuid().nullable().optional(),
certId: z.string().uuid().nullable().optional()
});
export type TPkiCollectionItems = z.infer<typeof PkiCollectionItemsSchema>;
export type TPkiCollectionItemsInsert = Omit<z.input<typeof PkiCollectionItemsSchema>, TImmutableDBKeys>;
export type TPkiCollectionItemsUpdate = Partial<Omit<z.input<typeof PkiCollectionItemsSchema>, TImmutableDBKeys>>;
+10 -5
View File
@@ -5,22 +5,27 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const WebhooksSchema = z.object({
id: z.string().uuid(),
secretPath: z.string().default("/"),
url: z.string(),
lastStatus: z.string().nullable().optional(),
lastRunErrorMessage: z.string().nullable().optional(),
isDisabled: z.boolean().default(false),
encryptedSecretKey: z.string().nullable().optional(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
algorithm: z.string().nullable().optional(),
keyEncoding: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
envId: z.string().uuid(),
type: z.string().default("general").nullable().optional(),
encryptedSecretKeyWithKms: zodBuffer.nullable().optional(),
encryptedUrl: zodBuffer
urlCipherText: z.string().nullable().optional(),
urlIV: z.string().nullable().optional(),
urlTag: z.string().nullable().optional(),
type: z.string().default("general").nullable().optional()
});
export type TWebhooks = z.infer<typeof WebhooksSchema>;
+12 -7
View File
@@ -73,8 +73,6 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { TQueueServiceFactory } from "@app/queue";
import { readLimit } from "@app/server/config/rateLimiter";
import { alertDALFactory } from "@app/services/alert/alert-dal";
import { alertServiceFactory } from "@app/services/alert/alert-service";
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { authDALFactory } from "@app/services/auth/auth-dal";
@@ -133,7 +131,10 @@ import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
import { orgServiceFactory } from "@app/services/org/org-service";
import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal";
import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
import { projectDALFactory } from "@app/services/project/project-dal";
import { projectQueueFactory } from "@app/services/project/project-queue";
@@ -220,7 +221,6 @@ export const registerRoutes = async (
const superAdminDAL = superAdminDALFactory(db);
const rateLimitDAL = rateLimitDALFactory(db);
const apiKeyDAL = apiKeyDALFactory(db);
const alertDAL = alertDALFactory(db);
const projectDAL = projectDALFactory(db);
const projectMembershipDAL = projectMembershipDALFactory(db);
@@ -588,7 +588,9 @@ export const registerRoutes = async (
const certificateDAL = certificateDALFactory(db);
const certificateBodyDAL = certificateBodyDALFactory(db);
const pkiAlertDAL = pkiAlertDALFactory(db);
const pkiCollectionDAL = pkiCollectionDALFactory(db);
const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db);
const certificateService = certificateServiceFactory({
certificateDAL,
@@ -634,14 +636,17 @@ export const registerRoutes = async (
licenseService
});
const alertService = alertServiceFactory({
alertDAL,
const pkiAlertService = pkiAlertServiceFactory({
pkiAlertDAL,
pkiCollectionDAL,
permissionService
});
const pkiCollectionService = pkiCollectionServiceFactory({
pkiCollectionDAL,
pkiCollectionItemDAL,
certificateAuthorityDAL,
certificateDAL,
permissionService
});
@@ -661,7 +666,7 @@ export const registerRoutes = async (
licenseService,
certificateAuthorityDAL,
certificateDAL,
alertDAL,
pkiAlertDAL,
pkiCollectionDAL,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL,
@@ -1091,7 +1096,6 @@ export const registerRoutes = async (
orgRole: orgRoleService,
oidc: oidcService,
apiKey: apiKeyService,
alert: alertService,
authToken: tokenService,
superAdmin: superAdminService,
project: projectService,
@@ -1135,6 +1139,7 @@ export const registerRoutes = async (
certificate: certificateService,
certificateAuthority: certificateAuthorityService,
certificateAuthorityCrl: certificateAuthorityCrlService,
pkiAlert: pkiAlertService,
pkiCollection: pkiCollectionService,
secretScanning: secretScanningService,
license: licenseService,
@@ -1,6 +1,6 @@
import { z } from "zod";
import { AlertsSchema } from "@app/db/schemas";
import { PkiAlertsSchema } 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";
@@ -23,11 +23,11 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
emails: z.array(z.string())
}),
response: {
200: AlertsSchema
200: PkiAlertsSchema
}
},
handler: async (req) => {
const alert = await server.services.alert.createPkiAlert({
const alert = await server.services.pkiAlert.createPkiAlert({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -67,11 +67,11 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
alertId: z.string().trim()
}),
response: {
200: AlertsSchema
200: PkiAlertsSchema
}
},
handler: async (req) => {
const alert = await server.services.alert.getPkiAlertById({
const alert = await server.services.pkiAlert.getPkiAlertById({
alertId: req.params.alertId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -116,11 +116,11 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
emails: z.array(z.string()).optional()
}),
response: {
200: AlertsSchema
200: PkiAlertsSchema
}
},
handler: async (req) => {
const alert = await server.services.alert.updatePkiAlert({
const alert = await server.services.pkiAlert.updatePkiAlert({
alertId: req.params.alertId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -160,11 +160,11 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
alertId: z.string().trim()
}),
response: {
200: AlertsSchema
200: PkiAlertsSchema
}
},
handler: async (req) => {
const alert = await server.services.alert.deletePkiAlert({
const alert = await server.services.pkiAlert.deletePkiAlert({
alertId: req.params.alertId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -1,9 +1,10 @@
import { z } from "zod";
import { PkiCollectionsSchema } from "@app/db/schemas";
import { PkiCollectionItemsSchema, PkiCollectionsSchema } from "@app/db/schemas";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -184,4 +185,165 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) =>
return pkiCollection;
}
});
server.route({
method: "GET",
url: "/:collectionId/items",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get items in PKI collection",
params: z.object({
collectionId: z.string().trim()
}),
querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0),
limit: z.coerce.number().min(1).max(100).default(25)
}),
response: {
200: z.object({
collectionItems: z.array(
PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({
type: z.nativeEnum(PkiItemType),
itemId: z.string().trim()
})
),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { pkiCollectionItems, totalCount } = await server.services.pkiCollection.getPkiCollectionItems({
collectionId: req.params.collectionId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
// TODO: audit logging
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// projectId: ca.projectId,
// event: {
// type: EventType.REVOKE_CERT,
// metadata: {
// certId: cert.id,
// cn: cert.commonName,
// serialNumber: cert.serialNumber
// }
// }
// });
return {
collectionItems: pkiCollectionItems,
totalCount
};
}
});
server.route({
method: "POST",
url: "/:collectionId/items",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Add item to PKI collection",
params: z.object({
collectionId: z.string().trim()
}),
body: z.object({
type: z.nativeEnum(PkiItemType),
itemId: z.string().trim()
}),
response: {
200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({
type: z.nativeEnum(PkiItemType),
itemId: z.string().trim()
})
}
},
handler: async (req) => {
const pkiCollectionItem = await server.services.pkiCollection.addItemToPkiCollection({
collectionId: req.params.collectionId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
// TODO: audit logging
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// projectId: ca.projectId,
// event: {
// type: EventType.REVOKE_CERT,
// metadata: {
// certId: cert.id,
// cn: cert.commonName,
// serialNumber: cert.serialNumber
// }
// }
// });
return pkiCollectionItem;
}
});
server.route({
method: "DELETE",
url: "/:collectionId/items/:itemId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Remove item from PKI collection",
params: z.object({
collectionId: z.string().trim(),
itemId: z.string().trim()
}),
response: {
200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({
type: z.nativeEnum(PkiItemType),
itemId: z.string().trim()
})
}
},
handler: async (req) => {
const pkiCollectionItem = await server.services.pkiCollection.removeItemFromPkiCollection({
collectionId: req.params.collectionId,
itemId: req.params.itemId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
// TODO: audit logging
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// projectId: ca.projectId,
// event: {
// type: EventType.DELETE_CERT,
// metadata: {
// certId: deletedCert.id,
// cn: deletedCert.commonName,
// serialNumber: deletedCert.serialNumber
// }
// }
// });
return pkiCollectionItem;
}
});
};
@@ -2,9 +2,9 @@ import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import {
AlertsSchema,
CertificateAuthoritiesSchema,
CertificatesSchema,
PkiAlertsSchema,
PkiCollectionsSchema,
ProjectKeysSchema
} from "@app/db/schemas";
@@ -411,7 +411,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
alerts: z.array(AlertsSchema)
alerts: z.array(PkiAlertsSchema)
})
}
},
-12
View File
@@ -1,12 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TAlertDALFactory = ReturnType<typeof alertDALFactory>;
export const alertDALFactory = (db: TDbClient) => {
const alertOrm = ormify(db, TableName.Alert);
return {
...alertOrm
};
};
@@ -0,0 +1,12 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TPkiAlertDALFactory = ReturnType<typeof pkiAlertDALFactory>;
export const pkiAlertDALFactory = (db: TDbClient) => {
const pkiAlertOrm = ormify(db, TableName.PkiAlert);
return {
...pkiAlertOrm
};
};
@@ -5,18 +5,22 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
import { TAlertDALFactory } from "./alert-dal";
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./alert-types";
import { TPkiAlertDALFactory } from "./pki-alert-dal";
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types";
type TAlertServiceFactoryDep = {
alertDAL: TAlertDALFactory;
type TPkiAlertServiceFactoryDep = {
pkiAlertDAL: TPkiAlertDALFactory;
pkiCollectionDAL: TPkiCollectionDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TAlertServiceFactory = ReturnType<typeof alertServiceFactory>;
export type TPkiAlertServiceFactory = ReturnType<typeof pkiAlertServiceFactory>;
export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionService }: TAlertServiceFactoryDep) => {
export const pkiAlertServiceFactory = ({
pkiAlertDAL,
pkiCollectionDAL,
permissionService
}: TPkiAlertServiceFactoryDep) => {
const createPkiAlert = async ({
projectId,
name,
@@ -43,7 +47,7 @@ export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionServ
if (pkiCollection.projectId !== projectId)
throw new UnauthorizedError({ message: "PKI collection not found in project" });
const alert = await alertDAL.create({
const alert = await pkiAlertDAL.create({
projectId,
pkiCollectionId,
name,
@@ -54,7 +58,7 @@ export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionServ
};
const getPkiAlertById = async ({ alertId, actorId, actorAuthMethod, actor, actorOrgId }: TGetAlertByIdDTO) => {
const alert = await alertDAL.findById(alertId);
const alert = await pkiAlertDAL.findById(alertId);
if (!alert) throw new NotFoundError({ message: "Alert not found" });
const { permission } = await permissionService.getProjectPermission(
@@ -80,7 +84,7 @@ export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionServ
actor,
actorOrgId
}: TUpdateAlertDTO) => {
let alert = await alertDAL.findById(alertId);
let alert = await pkiAlertDAL.findById(alertId);
if (!alert) throw new NotFoundError({ message: "Alert not found" });
const { permission } = await permissionService.getProjectPermission(
@@ -100,7 +104,7 @@ export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionServ
throw new UnauthorizedError({ message: "PKI collection not found in project" });
}
alert = await alertDAL.updateById(alertId, {
alert = await pkiAlertDAL.updateById(alertId, {
name,
alertBeforeDays,
...(pkiCollectionId && { pkiCollectionId }),
@@ -111,7 +115,7 @@ export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionServ
};
const deletePkiAlert = async ({ alertId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteAlertDTO) => {
let alert = await alertDAL.findById(alertId);
let alert = await pkiAlertDAL.findById(alertId);
if (!alert) throw new NotFoundError({ message: "Alert not found" });
const { permission } = await permissionService.getProjectPermission(
@@ -123,7 +127,7 @@ export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionServ
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts);
alert = await alertDAL.deleteById(alertId);
alert = await pkiAlertDAL.deleteById(alertId);
return alert;
};
@@ -0,0 +1,30 @@
import { TPkiCollectionItems } from "@app/db/schemas";
import { PkiItemType } from "./pki-collection-types";
/**
* Transforms a PKI Collection Item from the database to the expected API response format
*/
export const transformPkiCollectionItem = (pkiCollectionItem: TPkiCollectionItems) => {
let type: PkiItemType;
let itemId: string;
if (pkiCollectionItem.caId) {
type = PkiItemType.CA;
itemId = pkiCollectionItem.caId;
} else if (pkiCollectionItem.certId) {
type = PkiItemType.CERTIFICATE;
itemId = pkiCollectionItem.certId;
} else {
throw new Error("Invalid PKI Collection Item: must have either caId or certId");
}
return {
id: pkiCollectionItem.id,
pkiCollectionId: pkiCollectionItem.pkiCollectionId,
type,
itemId,
createdAt: pkiCollectionItem.createdAt,
updatedAt: pkiCollectionItem.updatedAt
};
};
@@ -0,0 +1,33 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TPkiCollectionItemDALFactory = ReturnType<typeof pkiCollectionItemDALFactory>;
export const pkiCollectionItemDALFactory = (db: TDbClient) => {
const pkiCollectionItemOrm = ormify(db, TableName.PkiCollectionItem);
const countItemsInPkiCollection = async (collectionId: string) => {
try {
interface CountResult {
count: string;
}
const query = db
.replicaNode()(TableName.PkiCollectionItem)
.where(`${TableName.PkiCollectionItem}.pkiCollectionId`, collectionId);
const count = await query.count("*").first();
return parseInt((count as unknown as CountResult).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Count all project certificates" });
}
};
return {
...pkiCollectionItemOrm,
countItemsInPkiCollection
};
};
@@ -1,19 +1,31 @@
import { ForbiddenError } from "@casl/ability";
import { TPkiCollectionItems } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { TPkiCollectionDALFactory } from "./pki-collection-dal";
import { transformPkiCollectionItem } from "./pki-collection-fns";
import { TPkiCollectionItemDALFactory } from "./pki-collection-item-dal";
import {
PkiItemType,
TAddItemToPkiCollectionDTO,
TCreatePkiCollectionDTO,
TDeletePkiCollectionDTO,
TGetPkiCollectionByIdDTO,
TGetPkiCollectionItems,
TRemoveItemFromPkiCollectionDTO,
TUpdatePkiCollectionDTO
} from "./pki-collection-types";
type TPkiCollectionServiceFactoryDep = {
pkiCollectionDAL: TPkiCollectionDALFactory;
pkiCollectionDAL: TPkiCollectionDALFactory; // TODO: Pick
pkiCollectionItemDAL: TPkiCollectionItemDALFactory;
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
certificateDAL: TCertificateDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
@@ -21,6 +33,9 @@ export type TPkiCollectionServiceFactory = ReturnType<typeof pkiCollectionServic
export const pkiCollectionServiceFactory = ({
pkiCollectionDAL,
pkiCollectionItemDAL,
certificateAuthorityDAL,
certificateDAL,
permissionService
}: TPkiCollectionServiceFactoryDep) => {
const createPkiCollection = async ({
@@ -127,12 +142,168 @@ export const pkiCollectionServiceFactory = ({
return pkiCollection;
};
// TODO: add/remove pki collection items
const getPkiCollectionItems = async ({
collectionId,
offset = 0,
limit = 25,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetPkiCollectionItems) => {
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
const pkiCollectionItems = await pkiCollectionItemDAL.find(
{ pkiCollectionId: collectionId },
{ offset, limit, sort: [["createdAt", "desc"]] }
);
const count = await pkiCollectionItemDAL.countItemsInPkiCollection(collectionId);
return {
pkiCollectionItems: pkiCollectionItems.map(transformPkiCollectionItem),
totalCount: count
};
};
const addItemToPkiCollection = async ({
collectionId,
actorId,
actorAuthMethod,
actor,
actorOrgId,
type,
itemId
}: TAddItemToPkiCollectionDTO) => {
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.PkiCollections
);
let pkiCollectionItem: TPkiCollectionItems;
switch (type) {
case PkiItemType.CA: {
// validate that CA has not already been added to PKI collection
const isCaAdded = await pkiCollectionItemDAL.findOne({
pkiCollectionId: collectionId,
caId: itemId
});
if (isCaAdded) throw new BadRequestError({ message: "CA is already part of the PKI collection" });
// validate that there exists a CA in same project as PKI collection
const ca = await certificateAuthorityDAL.findOne({
id: itemId,
projectId: pkiCollection.projectId
});
if (!ca) throw new NotFoundError({ message: "CA not found" });
pkiCollectionItem = await pkiCollectionItemDAL.create({
pkiCollectionId: collectionId,
caId: itemId
});
break;
}
case PkiItemType.CERTIFICATE: {
// validate that certificate has not already been added to PKI collection
const isCertAdded = await pkiCollectionItemDAL.findOne({
pkiCollectionId: collectionId,
certId: itemId
});
if (isCertAdded) throw new BadRequestError({ message: "Certificate already part of the PKI collection" });
// validate that there exists a certificate in same project as PKI collection
const cas = await certificateAuthorityDAL.find({ projectId: pkiCollection.projectId });
// TODO: consider making this more efficient
const [certificate] = await certificateDAL.find({
$in: {
caId: cas.map((ca) => ca.id)
},
id: itemId
});
if (!certificate) throw new NotFoundError({ message: "Certificate not found" });
pkiCollectionItem = await pkiCollectionItemDAL.create({
pkiCollectionId: collectionId,
certId: itemId
});
break;
}
default: {
throw new BadRequestError({ message: "Invalid PKI item type" });
}
}
return transformPkiCollectionItem(pkiCollectionItem);
};
const removeItemFromPkiCollection = async ({
collectionId,
actorId,
actorAuthMethod,
actor,
actorOrgId,
itemId
}: TRemoveItemFromPkiCollectionDTO) => {
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
let pkiCollectionItem = await pkiCollectionItemDAL.findOne({
pkiCollectionId: collectionId,
id: itemId
});
if (!pkiCollectionItem) throw new NotFoundError({ message: "PKI collection item not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.PkiCollections
);
pkiCollectionItem = await pkiCollectionItemDAL.deleteById(itemId);
return transformPkiCollectionItem(pkiCollectionItem);
};
return {
createPkiCollection,
getPkiCollectionById,
updatePkiCollection,
deletePkiCollection
deletePkiCollection,
getPkiCollectionItems,
addItemToPkiCollection,
removeItemFromPkiCollection
};
};
@@ -16,3 +16,25 @@ export type TUpdatePkiCollectionDTO = {
export type TDeletePkiCollectionDTO = {
collectionId: string;
} & Omit<TProjectPermission, "projectId">;
export enum PkiItemType {
CERTIFICATE = "certificate",
CA = "ca"
}
export type TGetPkiCollectionItems = {
collectionId: string;
offset: number;
limit: number;
} & Omit<TProjectPermission, "projectId">;
export type TAddItemToPkiCollectionDTO = {
collectionId: string;
type: PkiItemType;
itemId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRemoveItemFromPkiCollectionDTO = {
collectionId: string;
itemId: string;
} & Omit<TProjectPermission, "projectId">;
@@ -13,7 +13,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectPermission } from "@app/lib/types";
import { TAlertDALFactory } from "../alert/alert-dal";
import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
@@ -23,6 +22,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
@@ -73,7 +73,7 @@ type TProjectServiceFactoryDep = {
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
alertDAL: Pick<TAlertDALFactory, "find">;
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
@@ -113,7 +113,7 @@ export const projectServiceFactory = ({
certificateAuthorityDAL,
certificateDAL,
pkiCollectionDAL,
alertDAL,
pkiAlertDAL,
keyStore,
kmsService,
projectBotDAL
@@ -684,7 +684,7 @@ export const projectServiceFactory = ({
};
/**
* Return list of alerts configured for project
* Return list of (PKI) alerts configured for project
*/
const listProjectAlerts = async ({
projectId,
@@ -703,7 +703,7 @@ export const projectServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
const alerts = await alertDAL.find({ projectId });
const alerts = await pkiAlertDAL.find({ projectId });
return {
alerts
@@ -43,7 +43,6 @@ export type ProjectPermissionSet =
ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]
@@ -60,6 +59,8 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
+2 -2
View File
@@ -1,2 +1,2 @@
export { useCreateAlert, useDeleteAlert,useUpdateAlert } from "./mutations";
export { useGetAlertById } from "./queries";
export { useCreatePkiAlert, useDeletePkiAlert, useUpdatePkiAlert } from "./mutations";
export { useGetPkiAlertById } from "./queries";
@@ -6,7 +6,7 @@ import { workspaceKeys } from "../workspace/queries";
import { pkiAlertKeys } from "./queries";
import { TCreatePkiAlertDTO, TDeletePkiAlertDTO, TPkiAlert, TUpdatePkiAlertDTO } from "./types";
export const useCreateAlert = () => {
export const useCreatePkiAlert = () => {
const queryClient = useQueryClient();
return useMutation<TPkiAlert, {}, TCreatePkiAlertDTO>({
mutationFn: async (body) => {
@@ -19,7 +19,7 @@ export const useCreateAlert = () => {
});
};
export const useUpdateAlert = () => {
export const useUpdatePkiAlert = () => {
const queryClient = useQueryClient();
return useMutation<TPkiAlert, {}, TUpdatePkiAlertDTO>({
mutationFn: async ({ alertId, ...body }) => {
@@ -36,7 +36,7 @@ export const useUpdateAlert = () => {
});
};
export const useDeleteAlert = () => {
export const useDeletePkiAlert = () => {
const queryClient = useQueryClient();
return useMutation<TPkiAlert, {}, TDeletePkiAlertDTO>({
mutationFn: async ({ alertId }) => {
+1 -1
View File
@@ -8,7 +8,7 @@ export const pkiAlertKeys = {
getPkiAlertById: (alertId: string) => [{ alertId }, "alert"]
};
export const useGetAlertById = (alertId: string) => {
export const useGetPkiAlertById = (alertId: string) => {
return useQuery({
queryKey: pkiAlertKeys.getPkiAlertById(alertId),
queryFn: async () => {
@@ -0,0 +1,9 @@
export enum PkiItemType {
CERTIFICATE = "certificate",
CA = "ca"
}
export const pkiItemTypeToNameMap: { [K in PkiItemType]: string } = {
[PkiItemType.CA]: "CA",
[PkiItemType.CERTIFICATE]: "Certificate"
};
@@ -1,5 +1,7 @@
export {
useAddItemToPkiCollection,
useCreatePkiCollection,
useDeletePkiCollection,
useRemoveItemFromPkiCollection,
useUpdatePkiCollection} from "./mutations";
export { useGetPkiCollectionById } from "./queries";
export { useGetPkiCollectionById, useListPkiCollectionItems } from "./queries";
@@ -3,13 +3,14 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
// import { alertKeys } from "./queries";
// import { TAlert, TCreateAlertDTO, TDeleteAlertDTO, TUpdateAlertDTO } from "./types";
import { pkiCollectionKeys } from "./queries";
import {
TAddItemToPkiCollectionDTO,
TCreatePkiCollectionDTO,
TDeletePkiCollectionDTO,
TPkiCollection,
TPkiCollectionItem,
TRemoveItemFromPkiCollectionDTO,
TUpdatePkiCollectionTO} from "./types";
export const useCreatePkiCollection = () => {
@@ -61,4 +62,36 @@ export const useDeletePkiCollection = () => {
});
};
// TODO: add PKI Collection Item
export const useAddItemToPkiCollection = () => {
const queryClient = useQueryClient();
return useMutation<TPkiCollectionItem, {}, TAddItemToPkiCollectionDTO>({
mutationFn: async ({ collectionId, type, itemId }) => {
const { data: pkiCollectionItem } = await apiRequest.post<TPkiCollectionItem>(
`/api/v1/pki/collections/${collectionId}/items`,
{
type,
itemId
}
);
return pkiCollectionItem;
},
onSuccess: (_, { collectionId }) => {
queryClient.invalidateQueries(pkiCollectionKeys.getPkiCollectionItems(collectionId));
}
});
};
export const useRemoveItemFromPkiCollection = () => {
const queryClient = useQueryClient();
return useMutation<TPkiCollectionItem, {}, TRemoveItemFromPkiCollectionDTO>({
mutationFn: async ({ collectionId, itemId }) => {
const { data: pkiCollectionItem } = await apiRequest.delete<TPkiCollectionItem>(
`/api/v1/pki/collections/${collectionId}/items/${itemId}`
);
return pkiCollectionItem;
},
onSuccess: (_, { collectionId }) => {
queryClient.invalidateQueries(pkiCollectionKeys.getPkiCollectionItems(collectionId));
}
});
};
@@ -2,10 +2,26 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TPkiCollection } from "./types";
import { TPkiCollection, TPkiCollectionItem } from "./types";
export const pkiCollectionKeys = {
getPkiCollectionById: (collectionId: string) => [{ collectionId }, "pki-collection"]
getPkiCollectionById: (collectionId: string) => [{ collectionId }, "pki-collection"] as const,
getPkiCollectionItems: (collectionId: string) =>
[{ collectionId }, "pki-collection-items"] as const,
specificPkiCollectionItems: ({
collectionId,
offset,
limit
}: {
collectionId: string;
offset: number;
limit: number;
}) =>
[
...pkiCollectionKeys.getPkiCollectionItems(collectionId),
{ offset, limit },
"pki-collection-items-2"
] as const
};
export const useGetPkiCollectionById = (collectionId: string) => {
@@ -20,3 +36,39 @@ export const useGetPkiCollectionById = (collectionId: string) => {
enabled: Boolean(collectionId)
});
};
export const useListPkiCollectionItems = ({
collectionId,
offset,
limit
}: {
collectionId: string;
offset: number;
limit: number;
}) => {
return useQuery({
queryKey: pkiCollectionKeys.specificPkiCollectionItems({
collectionId,
offset,
limit
}),
queryFn: async () => {
const params = new URLSearchParams({
offset: String(offset),
limit: String(limit)
});
const {
data: { collectionItems, totalCount }
} = await apiRequest.get<{
collectionItems: TPkiCollectionItem[];
totalCount: number;
}>(`/api/v1/pki/collections/${collectionId}/items`, {
params
});
return { collectionItems, totalCount };
},
enabled: Boolean(collectionId)
});
};
@@ -21,3 +21,23 @@ export type TDeletePkiCollectionDTO = {
collectionId: string;
projectId: string;
};
export type TPkiCollectionItem = {
id: string;
collectionId: string;
type: string;
itemId: string;
createdAt: string;
updatedAt: string;
};
export type TAddItemToPkiCollectionDTO = {
collectionId: string;
type: string;
itemId: string;
};
export type TRemoveItemFromPkiCollectionDTO = {
collectionId: string;
itemId: string;
};
+1 -1
View File
@@ -23,10 +23,10 @@ export {
useGetWorkspaceIntegrations,
useGetWorkspaceSecrets,
useGetWorkspaceUsers,
useListWorkspaceAlerts,
useListWorkspaceCas,
useListWorkspaceCertificates,
useListWorkspaceGroups,
useListWorkspacePkiAlerts,
useListWorkspacePkiCollections,
useNameWorkspaceSecrets,
useRenameWorkspace,
+3 -2
View File
@@ -65,7 +65,8 @@ export const workspaceKeys = {
offset: number;
limit: number;
}) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const,
getWorkspacePkiAlerts: (workspaceId: string) => [{ workspaceId }, "workspace-alerts"] as const,
getWorkspacePkiAlerts: (workspaceId: string) =>
[{ workspaceId }, "workspace-pki-alerts"] as const,
getWorkspacePkiCollections: (workspaceId: string) =>
[{ workspaceId }, "workspace-pki-collections"] as const
};
@@ -607,7 +608,7 @@ export const useListWorkspaceCertificates = ({
});
};
export const useListWorkspaceAlerts = ({ workspaceId }: { workspaceId: string }) => {
export const useListWorkspacePkiAlerts = ({ workspaceId }: { workspaceId: string }) => {
return useQuery({
queryKey: workspaceKeys.getWorkspacePkiAlerts(workspaceId),
queryFn: async () => {
@@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { PkiCollectionPage } from "@app/views/Project/PkiCollectionPage";
export default function PkiCollection() {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: "PKI Collection" })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<PkiCollectionPage />
</>
);
}
PkiCollection.requireAuth = true;
@@ -2,7 +2,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { AlertsTab, CaTab, CertificatesTab } from "./components";
import { CaTab, CertificatesTab,PkiAlertsTab } from "./components";
enum TabSections {
Ca = "certificate-authorities",
@@ -29,7 +29,7 @@ export const CertificatesPage = withProjectPermission(
<CaTab />
</TabPanel>
<TabPanel value={TabSections.Alerting}>
<AlertsTab />
<PkiAlertsTab />
</TabPanel>
</Tabs>
</div>
@@ -1 +0,0 @@
export { AlertsTab } from "./AlertsTab";
@@ -1,8 +1,8 @@
import { motion } from "framer-motion";
import { AlertsSection,PkiCollectionSection } from "./components";
import { PkiAlertsSection, PkiCollectionSection } from "./components";
export const AlertsTab = () => {
export const PkiAlertsTab = () => {
return (
<motion.div
key="panel-alerts"
@@ -12,7 +12,7 @@ export const AlertsTab = () => {
exit={{ opacity: 0, translateX: 30 }}
>
<PkiCollectionSection />
<AlertsSection />
<PkiAlertsSection />
</motion.div>
);
};
@@ -12,13 +12,15 @@ import {
ModalContent,
Select,
SelectItem,
TextArea} from "@app/components/v2";
TextArea
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useCreateAlert,
useGetAlertById,
useCreatePkiAlert,
useGetPkiAlertById,
useListWorkspacePkiCollections,
useUpdateAlert} from "@app/hooks/api";
useUpdatePkiAlert
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
enum TimeUnit {
@@ -54,24 +56,24 @@ const convertToDays = (unit: TimeUnit, value: number) => {
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["alert"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["alert"]>, state?: boolean) => void;
popUp: UsePopUpState<["pkiAlert"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["pkiAlert"]>, state?: boolean) => void;
};
export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => {
export const PkiAlertModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { data: alert } = useGetAlertById(
(popUp?.alert?.data as { alertId: string })?.alertId || ""
const { data: alert } = useGetPkiAlertById(
(popUp?.pkiAlert?.data as { alertId: string })?.alertId || ""
);
const { data: pkiCollections } = useListWorkspacePkiCollections({
workspaceId: projectId
});
const { mutateAsync: createAlert } = useCreateAlert();
const { mutateAsync: updateAlert } = useUpdateAlert();
const { mutateAsync: createPkiAlert } = useCreatePkiAlert();
const { mutateAsync: updatePkiAlert } = useUpdatePkiAlert();
const {
control,
@@ -95,11 +97,15 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => {
emails: alert.recipientEmails
});
} else {
// TODO: add default collection?
reset({
name: ""
name: "",
...(pkiCollections?.collections?.[0] && {
pkiCollectionId: pkiCollections.collections[0].id
})
});
}
}, [alert]);
}, [alert, pkiCollections]);
const onFormSubmit = async ({
name,
@@ -120,7 +126,7 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => {
if (alert) {
// update
await updateAlert({
await updatePkiAlert({
alertId: alert.id,
pkiCollectionId,
name,
@@ -130,7 +136,7 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => {
});
} else {
// create
await createAlert({
await createPkiAlert({
name,
projectId,
pkiCollectionId,
@@ -139,7 +145,7 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => {
});
}
handlePopUpToggle("alert", false);
handlePopUpToggle("pkiAlert", false);
reset();
@@ -158,9 +164,9 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => {
return (
<Modal
isOpen={popUp?.alert?.isOpen}
isOpen={popUp?.pkiAlert?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("alert", isOpen);
handlePopUpToggle("pkiAlert", isOpen);
reset();
}}
>
@@ -18,13 +18,16 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
alert: TPkiAlert;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["alert", "deleteAlert"]>, data?: {}) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["pkiAlert", "deletePkiAlert"]>,
data?: {}
) => void;
};
export const AlertRow = ({ alert, handlePopUpOpen }: Props) => {
export const PkiAlertRow = ({ alert, handlePopUpOpen }: Props) => {
const { data: pkiCollection } = useGetPkiCollectionById(alert.pkiCollectionId || "");
return (
<Tr className="h-10" key={`alert-${alert.id}`}>
<Tr className="h-10" key={`pki-alert-${alert.id}`}>
<Td>{alert.name}</Td>
<Td>{alert.alertBeforeDays}</Td>
<Td>{pkiCollection ? pkiCollection.name : "-"}</Td>
@@ -47,7 +50,7 @@ export const AlertRow = ({ alert, handlePopUpOpen }: Props) => {
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("alert", {
handlePopUpOpen("pkiAlert", {
alertId: alert.id
});
}}
@@ -70,7 +73,7 @@ export const AlertRow = ({ alert, handlePopUpOpen }: Props) => {
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteAlert", {
handlePopUpOpen("deletePkiAlert", {
alertId: alert.id,
name: alert.name
});
@@ -5,37 +5,37 @@ import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useDeleteAlert } from "@app/hooks/api";
import { useDeletePkiAlert } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { AlertModal } from "./AlertModal";
import { AlertsTable } from "./AlertsTable";
import { PkiAlertModal } from "./PkiAlertModal";
import { PkiAlertsTable } from "./PkiAlertsTable";
export const AlertsSection = () => {
export const PkiAlertsSection = () => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { mutateAsync: deleteAlert } = useDeleteAlert();
const { mutateAsync: deletePkiAlert } = useDeletePkiAlert();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"alert",
"deleteAlert"
"pkiAlert",
"deletePkiAlert"
] as const);
const onRemoveAlertSubmit = async (alertId: string) => {
try {
if (!projectId) return;
await deleteAlert({
await deletePkiAlert({
alertId,
projectId
});
await createNotification({
createNotification({
text: "Successfully deleted alert",
type: "success"
});
handlePopUpClose("deleteAlert");
handlePopUpClose("deletePkiAlert");
} catch (err) {
console.error(err);
createNotification({
@@ -58,7 +58,7 @@ export const AlertsSection = () => {
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("alert")}
onClick={() => handlePopUpOpen("pkiAlert")}
isDisabled={!isAllowed}
>
Create
@@ -66,17 +66,17 @@ export const AlertsSection = () => {
)}
</ProjectPermissionCan>
</div>
<AlertsTable handlePopUpOpen={handlePopUpOpen} />
<AlertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<PkiAlertsTable handlePopUpOpen={handlePopUpOpen} />
<PkiAlertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteAlert.isOpen}
isOpen={popUp.deletePkiAlert.isOpen}
title={`Are you sure want to remove the alert ${
(popUp?.deleteAlert?.data as { name: string })?.name || ""
(popUp?.deletePkiAlert?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteAlert", isOpen)}
onChange={(isOpen) => handlePopUpToggle("deletePkiAlert", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveAlertSubmit((popUp?.deleteAlert?.data as { alertId: string })?.alertId)
onRemoveAlertSubmit((popUp?.deletePkiAlert?.data as { alertId: string })?.alertId)
}
/>
</div>
@@ -11,20 +11,23 @@ import {
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useListWorkspaceAlerts } from "@app/hooks/api";
import { useListWorkspacePkiAlerts } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { AlertRow } from "./AlertRow";
import { PkiAlertRow } from "./PkiAlertRow";
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["alert", "deleteAlert"]>, data?: {}) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["pkiAlert", "deletePkiAlert"]>,
data?: {}
) => void;
};
export const AlertsTable = ({ handlePopUpOpen }: Props) => {
export const PkiAlertsTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { data, isLoading } = useListWorkspaceAlerts({
const { data, isLoading } = useListWorkspacePkiAlerts({
workspaceId: projectId
});
@@ -35,16 +38,16 @@ export const AlertsTable = ({ handlePopUpOpen }: Props) => {
<Tr>
<Th>Alert Name</Th>
<Th>Alert Before Days</Th>
<Th>Bound PKI Collection</Th>
<Th>Certificate Collection</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="project-alerts" />}
{isLoading && <TableSkeleton columns={4} innerKey="project-pki-alerts" />}
{!isLoading &&
data?.alerts.map((alert) => {
return (
<AlertRow
<PkiAlertRow
key={`alert-${alert.id}`}
alert={alert}
handlePopUpOpen={handlePopUpOpen}
@@ -54,7 +57,7 @@ export const AlertsTable = ({ handlePopUpOpen }: Props) => {
</TBody>
</Table>
{!isLoading && !data?.alerts?.length && (
<EmptyState title="No alerts have been created" icon={faExclamationCircle} />
<EmptyState title="No alerts have been created for any certificate collections" icon={faExclamationCircle} />
)}
</TableContainer>
);
@@ -1,5 +1,6 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -25,6 +26,7 @@ type Props = {
};
export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
const router = useRouter();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
@@ -69,10 +71,12 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
});
} else {
// create
await createPkiCollection({
const { id: createdId } = await createPkiCollection({
name,
projectId
});
router.push(`/project/${projectId}/pki-collections/${createdId}`);
}
handlePopUpToggle("pkiCollection", false);
@@ -100,7 +104,7 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
reset();
}}
>
<ModalContent title={`${pkiCollection ? "Edit" : "Create"} PKI Collection`}>
<ModalContent title={`${pkiCollection ? "Edit" : "Create"} Certificate Collection`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
@@ -125,7 +129,7 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
{pkiCollection ? "Update" : "Create"}
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
@@ -48,7 +48,7 @@ export const PkiCollectionSection = () => {
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">PKI Collection</p>
<p className="text-xl font-semibold text-mineshaft-100">Certificate Collection</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.PkiCollections}
@@ -1,3 +1,4 @@
import { useRouter } from "next/router";
import { faBoxesStacked, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
@@ -30,6 +31,7 @@ type Props = {
};
export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
const router = useRouter();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
@@ -52,7 +54,13 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
{!isLoading &&
data?.collections.map((pkiCollection) => {
return (
<Tr className="h-10" key={`pki-collection-${pkiCollection.id}`}>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`pki-collection-${pkiCollection.id}`}
onClick={() =>
router.push(`/project/${projectId}/pki-collections/${pkiCollection.id}`)
}
>
<Td>{pkiCollection.name}</Td>
<Td>
<DropdownMenu>
@@ -79,7 +87,7 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
}}
disabled={!isAllowed}
>
Edit PKI Collection
Edit Collection
</DropdownMenuItem>
)}
</ProjectPermissionCan>
@@ -103,7 +111,7 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
}}
disabled={!isAllowed}
>
Delete PKI Collection
Delete Collection
</DropdownMenuItem>
)}
</ProjectPermissionCan>
@@ -116,7 +124,7 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
</TBody>
</Table>
{!isLoading && !data?.collections?.length && (
<EmptyState title="No collections have been created" icon={faBoxesStacked} />
<EmptyState title="No certificate collections have been created" icon={faBoxesStacked} />
)}
</TableContainer>
</div>
@@ -1,2 +1,2 @@
export { AlertsSection } from "./AlertsSection";
export { PkiAlertsSection } from "./PkiAlertsSection";
export { PkiCollectionSection } from "./PkiCollectionSection";
@@ -0,0 +1 @@
export { PkiAlertsTab } from "./PkiAlertsTab";
@@ -1,3 +1,3 @@
export { AlertsTab } from "./AlertsTab";
export { CaTab } from "./CaTab";
export { CertificatesTab } from "./CertificatesTab";
export { PkiAlertsTab } from "./PkiAlertsTab";
@@ -0,0 +1,160 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useRouter } from "next/router";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Tooltip
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useDeletePkiCollection,useGetPkiCollectionById } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { PkiCollectionModal } from "../CertificatesPage/components/PkiAlertsTab/components/PkiCollectionModal";
import { PkiCollectionDetailsSection, PkiCollectionItemsSection } from "./components";
export const PkiCollectionPage = withProjectPermission(
() => {
const router = useRouter();
const collectionId = router.query.collectionId as string;
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { data } = useGetPkiCollectionById(collectionId);
const { mutateAsync: deletePkiCollection } = useDeletePkiCollection();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"pkiCollection",
"deletePkiCollection"
] as const);
const onDeletePkiCollectionSubmit = async (collectionIdToDelete: string) => {
try {
if (!projectId) return;
await deletePkiCollection({
projectId,
collectionId: collectionIdToDelete
});
createNotification({
text: "Successfully deleted PKI collection",
type: "success"
});
handlePopUpClose("deletePkiCollection");
router.push(`/project/${projectId}/certificates`);
} catch (err) {
console.error(err);
}
};
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
router.push(`/project/${projectId}/certificates`);
}}
className="mb-4"
>
Certificate Collections
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.name}</p>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.PkiCollections}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
handlePopUpOpen("pkiCollection", {
collectionId
});
}}
disabled={!isAllowed}
>
Edit PKI Collection
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.PkiCollections}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() =>
handlePopUpOpen("deletePkiCollection", {
collectionId: data.id,
name: data.name
})
}
disabled={!isAllowed}
>
Delete PKI Collection
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex">
<div className="mr-4 w-96">
<PkiCollectionDetailsSection
collectionId={collectionId}
handlePopUpOpen={handlePopUpOpen}
/>
</div>
<PkiCollectionItemsSection collectionId={collectionId} />
</div>
</div>
)}
<PkiCollectionModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deletePkiCollection.isOpen}
title={`Are you sure want to delete the PKI collection ${data?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("deletePkiCollection", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeletePkiCollectionSubmit(
(popUp.deletePkiCollection.data as { collectionId: string })?.collectionId
)
}
/>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.PkiCollections }
);
@@ -0,0 +1,199 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
CaStatus,
useAddItemToPkiCollection,
useListWorkspaceCas,
useListWorkspaceCertificates} from "@app/hooks/api";
import { PkiItemType, pkiItemTypeToNameMap } from "@app/hooks/api/pkiCollections/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
.object({
type: z.nativeEnum(PkiItemType),
itemId: z.string()
})
.required();
type FormData = z.infer<typeof schema>;
type Props = {
collectionId: string;
popUp: UsePopUpState<["addPkiCollectionItem"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["addPkiCollectionItem"]>,
state?: boolean
) => void;
};
// note: this component should be optimized so it is easier
// to find certificates and CAs
export const AddPkiCollectionItemModal = ({ collectionId, popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: cas } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug || "",
status: CaStatus.ACTIVE
});
const { data } = useListWorkspaceCertificates({
projectSlug: currentWorkspace?.slug || "",
offset: 0,
limit: 25
});
const { mutateAsync: addItemToPkiCollection } = useAddItemToPkiCollection();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting },
watch
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
type: PkiItemType.CA
}
});
const itemType = watch("type");
const onFormSubmit = async ({ type, itemId }: FormData) => {
try {
const item = await addItemToPkiCollection({
collectionId,
type,
itemId
});
createNotification({
text: `Successfully added ${
pkiItemTypeToNameMap[item.type as PkiItemType]
} to PKI collection`,
type: "success"
});
reset();
handlePopUpToggle("addPkiCollectionItem", false);
} catch (err) {
console.error(err);
}
};
return (
<Modal
isOpen={popUp?.addPkiCollectionItem?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addPkiCollectionItem", isOpen);
reset();
}}
>
<ModalContent title="Add CA / Certificate to Collection">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="type"
defaultValue={PkiItemType.CA}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Item Type" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
<SelectItem value={PkiItemType.CA} key="pki-item-type-ca">
Certificate Authority
</SelectItem>
<SelectItem value={PkiItemType.CERTIFICATE} key="pki-item-type-ca">
Certificate
</SelectItem>
</Select>
</FormControl>
)}
/>
{itemType === PkiItemType.CA && (
<Controller
control={control}
name="itemId"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="CA"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(cas || []).map(({ id, dn }) => (
<SelectItem value={id} key={`pki-item-cert-${id}`}>
{dn}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
{itemType === PkiItemType.CERTIFICATE && (
<Controller
control={control}
name="itemId"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Certificate"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(data?.certificates || []).map(({ id, commonName }) => (
<SelectItem value={id} key={`pki-item-cert-${id}`}>
{`CN=${commonName}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Add
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("addPkiCollectionItem", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,83 @@
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetPkiCollectionById } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
collectionId: string;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["pkiCollection"]>, data?: {}) => void;
};
export const PkiCollectionDetailsSection = ({ collectionId, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const { data: pkiCollection } = useGetPkiCollectionById(collectionId);
return pkiCollection ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Collection Details</h3>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.PkiCollections}
>
{(isAllowed) => {
return (
<Tooltip content="Edit Identity">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() =>
handlePopUpOpen("pkiCollection", {
collectionId
})
}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
</ProjectPermissionCan>
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">PKI Collection ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{pkiCollection.id}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(pkiCollection.id);
setCopyTextId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
<div>
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{pkiCollection.name}</p>
</div>
</div>
</div>
) : (
<div />
);
};
@@ -0,0 +1,83 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal, IconButton } from "@app/components/v2";
import { useGetPkiCollectionById, useRemoveItemFromPkiCollection } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { AddPkiCollectionItemModal } from "./AddPkiCollectionItemModal";
import { PkiCollectionItemsTable } from "./PkiCollectionItemsTable";
type Props = {
collectionId: string;
};
export const PkiCollectionItemsSection = ({ collectionId }: Props) => {
const { data: pkiCollection } = useGetPkiCollectionById(collectionId);
const { mutateAsync: removeItemFromPkiCollection } = useRemoveItemFromPkiCollection();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deletePkiCollectionItem",
"addPkiCollectionItem"
] as const);
const onRemovePkiCollectionItemSubmit = async (itemId: string) => {
try {
await removeItemFromPkiCollection({
itemId,
collectionId
});
createNotification({
text: "Successfully removed item from PKI collection",
type: "success"
});
handlePopUpClose("deletePkiCollectionItem");
} catch (err) {
console.error(err);
}
};
return pkiCollection ? (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Items</h3>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
handlePopUpOpen("addPkiCollectionItem");
}}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</div>
<div className="py-4">
<PkiCollectionItemsTable collectionId={collectionId} handlePopUpOpen={handlePopUpOpen} />
</div>
<AddPkiCollectionItemModal
collectionId={collectionId}
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deletePkiCollectionItem.isOpen}
title={`Are you sure want to remove the item from the collection ${pkiCollection.name}?`}
onChange={(isOpen) => handlePopUpToggle("deletePkiCollectionItem", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const popupData = popUp?.deletePkiCollectionItem?.data as {
itemId: string;
};
return onRemovePkiCollectionItemSubmit(popupData.itemId);
}}
/>
</div>
) : (
<div />
);
};
@@ -0,0 +1,112 @@
import { useState } from "react";
import { faBoxesStacked, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
Pagination,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useListPkiCollectionItems } from "@app/hooks/api";
import { PkiItemType,pkiItemTypeToNameMap } from "@app/hooks/api/pkiCollections/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
collectionId: string;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["deletePkiCollectionItem"]>, data?: {}) => void;
};
const PER_PAGE_INIT = 25;
export const PkiCollectionItemsTable = ({ collectionId, handlePopUpOpen }: Props) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { data, isLoading } = useListPkiCollectionItems({
collectionId,
offset: (page - 1) * perPage,
limit: perPage
});
return (
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Resource</Th>
<Th>ID</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={2} innerKey="pki-collections" />}
{!isLoading &&
data?.collectionItems.map((collectionItem) => {
return (
<Tr className="group" key={`pki-collection-item-${collectionItem.id}`}>
<Td>{pkiItemTypeToNameMap[collectionItem.type as PkiItemType]}</Td>
<Td>{collectionItem.itemId}</Td>
<Td>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.PkiCollections}
>
{(isAllowed) => (
<Tooltip content="Remove">
<IconButton
colorSchema="danger"
ariaLabel="copy icon"
variant="plain"
className="group relative"
isDisabled={!isAllowed}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deletePkiCollectionItem", {
collectionId,
itemId: collectionItem.id
});
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
)}
</ProjectPermissionCan>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && (
<Pagination
count={data.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isLoading && !data?.collectionItems?.length && (
<EmptyState
title="No CAs or certificates have been added to this collection"
icon={faBoxesStacked}
/>
)}
</TableContainer>
</div>
);
};
@@ -0,0 +1,2 @@
export { PkiCollectionDetailsSection } from "./PkiCollectionDetailsSection";
export { PkiCollectionItemsSection } from "./PkiCollectionItemsSection";
@@ -0,0 +1 @@
export { PkiCollectionPage } from "./PkiCollectionPage";