diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 408b9a4f9..dab2e1996 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -52,6 +52,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 { 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"; import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service"; @@ -155,6 +156,7 @@ declare module "fastify" { certificate: TCertificateServiceFactory; certificateAuthority: TCertificateAuthorityServiceFactory; certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory; + pkiCollection: TPkiCollectionServiceFactory; secretScanning: TSecretScanningServiceFactory; license: TLicenseServiceFactory; trustedIp: TTrustedIpServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 61b582e82..754c36de2 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -164,6 +164,9 @@ import { TOrgRoles, TOrgRolesInsert, TOrgRolesUpdate, + TPkiCollections, + TPkiCollectionsInsert, + TPkiCollectionsUpdate, TProjectBots, TProjectBotsInsert, TProjectBotsUpdate, @@ -368,6 +371,11 @@ declare module "knex/types/tables" { TCertificateSecretsInsert, TCertificateSecretsUpdate >; + [TableName.PkiCollection]: KnexOriginal.CompositeTableType< + TPkiCollections, + TPkiCollectionsInsert, + TPkiCollectionsUpdate + >; [TableName.UserGroupMembership]: KnexOriginal.CompositeTableType< TUserGroupMembership, TUserGroupMembershipInsert, diff --git a/backend/src/db/migrations/20240802181855_ca-cert-version.ts b/backend/src/db/migrations/20240802181855_ca-cert-version.ts index 7c7d956fc..62637cbab 100644 --- a/backend/src/db/migrations/20240802181855_ca-cert-version.ts +++ b/backend/src/db/migrations/20240802181855_ca-cert-version.ts @@ -22,7 +22,7 @@ export async function up(knex: Knex): Promise { if (!hasVersionColumn) { await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => { t.integer("version").nullable(); - t.dropUnique(["caId"]); + // t.dropUnique(["caId"]); }); await knex(TableName.CertificateAuthorityCert).update({ version: 1 }).whereNull("version"); @@ -54,11 +54,11 @@ export async function up(knex: Knex): Promise { } } - if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) { - await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => { - t.dropUnique(["caId"]); - }); - } + // if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) { + // await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => { + // t.dropUnique(["caId"]); + // }); + // } } export async function down(knex: Knex): Promise { diff --git a/backend/src/db/migrations/20240806173521_cert-alerting.ts b/backend/src/db/migrations/20240806173521_cert-alerting.ts index a85218640..1cc94badb 100644 --- a/backend/src/db/migrations/20240806173521_cert-alerting.ts +++ b/backend/src/db/migrations/20240806173521_cert-alerting.ts @@ -4,23 +4,39 @@ import { TableName } from "../schemas"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.PkiCollection))) { + await knex.schema.createTable(TableName.PkiCollection, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.string("name").notNullable(); + }); + } + if (!(await knex.schema.hasTable(TableName.Alert))) { + // TODO: rename to pki alert await knex.schema.createTable(TableName.Alert, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.timestamps(true, true, true); t.string("projectId").notNullable(); t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("pkiCollectionId").notNullable(); + t.foreign("pkiCollectionId").references("id").inTable(TableName.PkiCollection).onDelete("CASCADE"); t.string("name").notNullable(); t.integer("alertBeforeDays").notNullable(); t.string("recipientEmails").notNullable(); }); } + await createOnUpdateTrigger(knex, TableName.PkiCollection); await createOnUpdateTrigger(knex, TableName.Alert); } export async function down(knex: Knex): Promise { - // certificates + await knex.schema.dropTableIfExists(TableName.PkiCollection); + await dropOnUpdateTrigger(knex, TableName.PkiCollection); + await knex.schema.dropTableIfExists(TableName.Alert); await dropOnUpdateTrigger(knex, TableName.Alert); } diff --git a/backend/src/db/schemas/alerts.ts b/backend/src/db/schemas/alerts.ts index b1a7dcaa0..344bb499d 100644 --- a/backend/src/db/schemas/alerts.ts +++ b/backend/src/db/schemas/alerts.ts @@ -12,6 +12,7 @@ export const AlertsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), projectId: z.string(), + pkiCollectionId: z.string().uuid(), name: z.string(), alertBeforeDays: z.number(), recipientEmails: z.string() diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 26ee76ab4..98af39fcd 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -53,6 +53,7 @@ export * from "./org-bots"; export * from "./org-memberships"; export * from "./org-roles"; export * from "./organizations"; +export * from "./pki-collections"; export * from "./project-bots"; export * from "./project-environments"; export * from "./project-keys"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 7fe196514..0087b33e0 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -9,7 +9,8 @@ export enum TableName { Certificate = "certificates", CertificateBody = "certificate_bodies", CertificateSecret = "certificate_secrets", - Alert = "alerts", + Alert = "alerts", // TODO: rename + PkiCollection = "pki_collections", Groups = "groups", GroupProjectMembership = "group_project_memberships", GroupProjectMembershipRole = "group_project_membership_roles", diff --git a/backend/src/db/schemas/pki-collections.ts b/backend/src/db/schemas/pki-collections.ts new file mode 100644 index 000000000..4470c513c --- /dev/null +++ b/backend/src/db/schemas/pki-collections.ts @@ -0,0 +1,20 @@ +// 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 PkiCollectionsSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + projectId: z.string(), + name: z.string() +}); + +export type TPkiCollections = z.infer; +export type TPkiCollectionsInsert = Omit, TImmutableDBKeys>; +export type TPkiCollectionsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 22837f12e..7ea2ba39f 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -10,7 +10,6 @@ export enum ProjectPermissionActions { } export enum ProjectPermissionSub { - Alerts = "alerts", Role = "role", Member = "member", Groups = "groups", @@ -31,6 +30,8 @@ export enum ProjectPermissionSub { Identity = "identity", CertificateAuthorities = "certificate-authorities", Certificates = "certificates", + PkiAlerts = "pki-alerts", + PkiCollections = "pki-collections", Kms = "kms" } @@ -64,7 +65,8 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.Identity] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.Certificates] - | [ProjectPermissionActions, ProjectPermissionSub.Alerts] + | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] + | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] @@ -74,11 +76,6 @@ export type ProjectPermissionSet = const buildAdminPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Alerts); - can(ProjectPermissionActions.Create, ProjectPermissionSub.Alerts); - can(ProjectPermissionActions.Edit, ProjectPermissionSub.Alerts); - can(ProjectPermissionActions.Delete, ProjectPermissionSub.Alerts); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); @@ -168,6 +165,16 @@ const buildAdminPermissionRules = () => { can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); + can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); + can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); + can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); @@ -244,7 +251,8 @@ const buildMemberPermissionRules = () => { can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Alerts); + can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); + can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); return rules; }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 70fc8c231..b1827ea66 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -133,6 +133,8 @@ 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 { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-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"; import { projectServiceFactory } from "@app/services/project/project-service"; @@ -517,10 +519,6 @@ export const registerRoutes = async ( licenseService }); const apiKeyService = apiKeyServiceFactory({ apiKeyDAL, userDAL }); - const alertService = alertServiceFactory({ - alertDAL, - permissionService - }); const secretScanningQueue = secretScanningQueueFactory({ telemetryService, @@ -590,6 +588,8 @@ export const registerRoutes = async ( const certificateDAL = certificateDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db); + const pkiCollectionDAL = pkiCollectionDALFactory(db); + const certificateService = certificateServiceFactory({ certificateDAL, certificateBodyDAL, @@ -634,6 +634,17 @@ export const registerRoutes = async ( licenseService }); + const alertService = alertServiceFactory({ + alertDAL, + pkiCollectionDAL, + permissionService + }); + + const pkiCollectionService = pkiCollectionServiceFactory({ + pkiCollectionDAL, + permissionService + }); + const projectService = projectServiceFactory({ permissionService, projectDAL, @@ -651,6 +662,7 @@ export const registerRoutes = async ( certificateAuthorityDAL, certificateDAL, alertDAL, + pkiCollectionDAL, projectUserMembershipRoleDAL, identityProjectMembershipRoleDAL, keyStore, @@ -1123,6 +1135,7 @@ export const registerRoutes = async ( certificate: certificateService, certificateAuthority: certificateAuthorityService, certificateAuthorityCrl: certificateAuthorityCrlService, + pkiCollection: pkiCollectionService, secretScanning: secretScanningService, license: licenseService, trustedIp: trustedIpService, diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 3a2e36e1c..1a5a56bd0 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -1,5 +1,4 @@ import { registerAdminRouter } from "./admin-router"; -import { registerAlertRouter } from "./alert-router"; import { registerAuthRoutes } from "./auth-router"; import { registerProjectBotRouter } from "./bot-router"; import { registerCaRouter } from "./certificate-authority-router"; @@ -19,6 +18,8 @@ import { registerInviteOrgRouter } from "./invite-org-router"; import { registerOrgAdminRouter } from "./org-admin-router"; import { registerOrgRouter } from "./organization-router"; import { registerPasswordRouter } from "./password-router"; +import { registerPkiAlertRouter } from "./pki-alert-router"; +import { registerPkiCollectionRouter } from "./pki-collection-router"; import { registerProjectEnvRouter } from "./project-env-router"; import { registerProjectKeyRouter } from "./project-key-router"; import { registerProjectMembershipRouter } from "./project-membership-router"; @@ -49,7 +50,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { }, { prefix: "/auth" } ); - await server.register(registerAlertRouter, { prefix: "/alerts" }); await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerOrgRouter, { prefix: "/organization" }); await server.register(registerAdminRouter, { prefix: "/admin" }); @@ -76,6 +76,8 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { async (pkiRouter) => { await pkiRouter.register(registerCaRouter, { prefix: "/ca" }); await pkiRouter.register(registerCertRouter, { prefix: "/certificates" }); + await server.register(registerPkiAlertRouter, { prefix: "/alerts" }); + await server.register(registerPkiCollectionRouter, { prefix: "/collections" }); }, { prefix: "/pki" } ); diff --git a/backend/src/server/routes/v1/alert-router.ts b/backend/src/server/routes/v1/pki-alert-router.ts similarity index 88% rename from backend/src/server/routes/v1/alert-router.ts rename to backend/src/server/routes/v1/pki-alert-router.ts index e4e340588..a330f8c20 100644 --- a/backend/src/server/routes/v1/alert-router.ts +++ b/backend/src/server/routes/v1/pki-alert-router.ts @@ -5,7 +5,7 @@ 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 registerAlertRouter = async (server: FastifyZodProvider) => { +export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/", @@ -14,9 +14,10 @@ export const registerAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Create alert", + description: "Create PKI alert", body: z.object({ projectId: z.string().trim(), + pkiCollectionId: z.string().trim(), name: z.string().trim(), alertBeforeDays: z.number(), emails: z.array(z.string()) @@ -26,7 +27,7 @@ export const registerAlertRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const alert = await server.services.alert.createAlert({ + const alert = await server.services.alert.createPkiAlert({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -61,7 +62,7 @@ export const registerAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Get alert", + description: "Get PKI alert", params: z.object({ alertId: z.string().trim() }), @@ -70,7 +71,7 @@ export const registerAlertRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const alert = await server.services.alert.getAlertById({ + const alert = await server.services.alert.getPkiAlertById({ alertId: req.params.alertId, actor: req.permission.type, actorId: req.permission.id, @@ -104,13 +105,14 @@ export const registerAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Update alert", + description: "Update PKI alert", params: z.object({ alertId: z.string().trim() }), body: z.object({ name: z.string().trim().optional(), alertBeforeDays: z.number().optional(), + pkiCollectionId: z.string().trim().optional(), emails: z.array(z.string()).optional() }), response: { @@ -118,7 +120,7 @@ export const registerAlertRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const alert = await server.services.alert.updateAlert({ + const alert = await server.services.alert.updatePkiAlert({ alertId: req.params.alertId, actor: req.permission.type, actorId: req.permission.id, @@ -153,7 +155,7 @@ export const registerAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Delete alert", + description: "Delete PKI alert", params: z.object({ alertId: z.string().trim() }), @@ -162,7 +164,7 @@ export const registerAlertRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const alert = await server.services.alert.deleteAlert({ + const alert = await server.services.alert.deletePkiAlert({ alertId: req.params.alertId, actor: req.permission.type, actorId: req.permission.id, diff --git a/backend/src/server/routes/v1/pki-collection-router.ts b/backend/src/server/routes/v1/pki-collection-router.ts new file mode 100644 index 000000000..a41116a29 --- /dev/null +++ b/backend/src/server/routes/v1/pki-collection-router.ts @@ -0,0 +1,187 @@ +import { z } from "zod"; + +import { 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"; + +export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Create PKI collection", + body: z.object({ + projectId: z.string().trim(), + name: z.string().trim() + }), + response: { + 200: PkiCollectionsSchema + } + }, + handler: async (req) => { + const pkiCollection = await server.services.pkiCollection.createPkiCollection({ + 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 pkiCollection; + } + }); + + server.route({ + method: "GET", + url: "/:collectionId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Get PKI collection", + params: z.object({ + collectionId: z.string().trim() + }), + response: { + 200: PkiCollectionsSchema + } + }, + handler: async (req) => { + const pkiCollection = await server.services.pkiCollection.getPkiCollectionById({ + collectionId: req.params.collectionId, + 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.GET_CA, + // metadata: { + // caId: ca.id, + // dn: ca.dn + // } + // } + // }); + + return pkiCollection; + } + }); + + server.route({ + method: "PATCH", + url: "/:collectionId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Update PKI collection", + params: z.object({ + collectionId: z.string().trim() + }), + body: z.object({ + name: z.string().trim().optional() + }), + response: { + 200: PkiCollectionsSchema + } + }, + handler: async (req) => { + const pkiCollection = await server.services.pkiCollection.updatePkiCollection({ + 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.GET_CA, + // metadata: { + // caId: ca.id, + // dn: ca.dn + // } + // } + // }); + + return pkiCollection; + } + }); + + server.route({ + method: "DELETE", + url: "/:collectionId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Delete PKI collection", + params: z.object({ + collectionId: z.string().trim() + }), + response: { + 200: PkiCollectionsSchema + } + }, + handler: async (req) => { + const pkiCollection = await server.services.pkiCollection.deletePkiCollection({ + collectionId: req.params.collectionId, + 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 pkiCollection; + } + }); +}; diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index e332a17f9..34a9eea31 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -1,7 +1,13 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; -import { AlertsSchema, CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema } from "@app/db/schemas"; +import { + AlertsSchema, + CertificateAuthoritiesSchema, + CertificatesSchema, + PkiCollectionsSchema, + ProjectKeysSchema +} from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { PROJECTS } from "@app/lib/api-docs"; import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -395,7 +401,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:projectId/alerts", + url: "/:projectId/pki-alerts", config: { rateLimit: readLimit }, @@ -422,4 +428,34 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return { alerts }; } }); + + server.route({ + method: "GET", + url: "/:projectId/pki-collections", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim() + }), + response: { + 200: z.object({ + collections: z.array(PkiCollectionsSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { pkiCollections } = await server.services.project.listProjectPkiCollections({ + projectId: req.params.projectId, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type + }); + + return { collections: pkiCollections }; + } + }); }; diff --git a/backend/src/services/alert/alert-service.ts b/backend/src/services/alert/alert-service.ts index 26e7c1c56..cd8501513 100644 --- a/backend/src/services/alert/alert-service.ts +++ b/backend/src/services/alert/alert-service.ts @@ -2,22 +2,25 @@ import { ForbiddenError } from "@casl/ability"; 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 { 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"; type TAlertServiceFactoryDep = { alertDAL: TAlertDALFactory; + pkiCollectionDAL: TPkiCollectionDALFactory; permissionService: Pick; }; export type TAlertServiceFactory = ReturnType; -export const alertServiceFactory = ({ alertDAL, permissionService }: TAlertServiceFactoryDep) => { - const createAlert = async ({ +export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionService }: TAlertServiceFactoryDep) => { + const createPkiAlert = async ({ projectId, name, + pkiCollectionId, alertBeforeDays, emails, actorId, @@ -33,10 +36,16 @@ export const alertServiceFactory = ({ alertDAL, permissionService }: TAlertServi actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Alerts); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts); + + const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId); + if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" }); + if (pkiCollection.projectId !== projectId) + throw new UnauthorizedError({ message: "PKI collection not found in project" }); const alert = await alertDAL.create({ projectId, + pkiCollectionId, name, alertBeforeDays, recipientEmails: emails.join(",") @@ -44,7 +53,7 @@ export const alertServiceFactory = ({ alertDAL, permissionService }: TAlertServi return alert; }; - const getAlertById = async ({ alertId, actorId, actorAuthMethod, actor, actorOrgId }: TGetAlertByIdDTO) => { + const getPkiAlertById = async ({ alertId, actorId, actorAuthMethod, actor, actorOrgId }: TGetAlertByIdDTO) => { const alert = await alertDAL.findById(alertId); if (!alert) throw new NotFoundError({ message: "Alert not found" }); @@ -56,13 +65,14 @@ export const alertServiceFactory = ({ alertDAL, permissionService }: TAlertServi actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Alerts); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); return alert; }; - const updateAlert = async ({ + const updatePkiAlert = async ({ alertId, name, + pkiCollectionId, alertBeforeDays, emails, actorId, @@ -81,17 +91,26 @@ export const alertServiceFactory = ({ alertDAL, permissionService }: TAlertServi actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Alerts); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts); + + if (pkiCollectionId) { + const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId); + if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" }); + if (pkiCollection.projectId !== alert.projectId) + throw new UnauthorizedError({ message: "PKI collection not found in project" }); + } + alert = await alertDAL.updateById(alertId, { name, alertBeforeDays, - recipientEmails: emails ? emails.join(",") : undefined + ...(pkiCollectionId && { pkiCollectionId }), + ...(emails && { recipientEmails: emails.join(",") }) // TODO: standardize recipient emails }); return alert; }; - const deleteAlert = async ({ alertId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteAlertDTO) => { + const deletePkiAlert = async ({ alertId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteAlertDTO) => { let alert = await alertDAL.findById(alertId); if (!alert) throw new NotFoundError({ message: "Alert not found" }); @@ -103,15 +122,15 @@ export const alertServiceFactory = ({ alertDAL, permissionService }: TAlertServi actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Alerts); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts); alert = await alertDAL.deleteById(alertId); return alert; }; return { - createAlert, - getAlertById, - updateAlert, - deleteAlert + createPkiAlert, + getPkiAlertById, + updatePkiAlert, + deletePkiAlert }; }; diff --git a/backend/src/services/alert/alert-types.ts b/backend/src/services/alert/alert-types.ts index 959667b7e..c00be0e03 100644 --- a/backend/src/services/alert/alert-types.ts +++ b/backend/src/services/alert/alert-types.ts @@ -2,6 +2,7 @@ import { TProjectPermission } from "@app/lib/types"; export type TCreateAlertDTO = { name: string; + pkiCollectionId: string; alertBeforeDays: number; emails: string[]; } & TProjectPermission; @@ -13,6 +14,7 @@ export type TGetAlertByIdDTO = { export type TUpdateAlertDTO = { alertId: string; name?: string; + pkiCollectionId?: string; alertBeforeDays?: number; emails?: string[]; } & Omit; diff --git a/backend/src/services/pki-collection/pki-collection-dal.ts b/backend/src/services/pki-collection/pki-collection-dal.ts new file mode 100644 index 000000000..382b4c6ef --- /dev/null +++ b/backend/src/services/pki-collection/pki-collection-dal.ts @@ -0,0 +1,13 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TPkiCollectionDALFactory = ReturnType; + +export const pkiCollectionDALFactory = (db: TDbClient) => { + const pkiCollectionOrm = ormify(db, TableName.PkiCollection); + + return { + ...pkiCollectionOrm + }; +}; diff --git a/backend/src/services/pki-collection/pki-collection-service.ts b/backend/src/services/pki-collection/pki-collection-service.ts new file mode 100644 index 000000000..35328b7e4 --- /dev/null +++ b/backend/src/services/pki-collection/pki-collection-service.ts @@ -0,0 +1,138 @@ +import { ForbiddenError } from "@casl/ability"; + +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 { TPkiCollectionDALFactory } from "./pki-collection-dal"; +import { + TCreatePkiCollectionDTO, + TDeletePkiCollectionDTO, + TGetPkiCollectionByIdDTO, + TUpdatePkiCollectionDTO +} from "./pki-collection-types"; + +type TPkiCollectionServiceFactoryDep = { + pkiCollectionDAL: TPkiCollectionDALFactory; + permissionService: Pick; +}; + +export type TPkiCollectionServiceFactory = ReturnType; + +export const pkiCollectionServiceFactory = ({ + pkiCollectionDAL, + permissionService +}: TPkiCollectionServiceFactoryDep) => { + const createPkiCollection = async ({ + name, + projectId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TCreatePkiCollectionDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.PkiCollections + ); + + const pkiCollection = await pkiCollectionDAL.create({ + projectId, + name + }); + + return pkiCollection; + }; + + const getPkiCollectionById = async ({ + collectionId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TGetPkiCollectionByIdDTO) => { + 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); + return pkiCollection; + }; + + const updatePkiCollection = async ({ + collectionId, + name, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdatePkiCollectionDTO) => { + let 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.Edit, ProjectPermissionSub.PkiCollections); + pkiCollection = await pkiCollectionDAL.updateById(collectionId, { + name + }); + + return pkiCollection; + }; + + const deletePkiCollection = async ({ + collectionId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TDeletePkiCollectionDTO) => { + let 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 + ); + pkiCollection = await pkiCollectionDAL.deleteById(collectionId); + return pkiCollection; + }; + + // TODO: add/remove pki collection items + + return { + createPkiCollection, + getPkiCollectionById, + updatePkiCollection, + deletePkiCollection + }; +}; diff --git a/backend/src/services/pki-collection/pki-collection-types.ts b/backend/src/services/pki-collection/pki-collection-types.ts new file mode 100644 index 000000000..9281a1721 --- /dev/null +++ b/backend/src/services/pki-collection/pki-collection-types.ts @@ -0,0 +1,18 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TCreatePkiCollectionDTO = { + name: string; +} & TProjectPermission; + +export type TGetPkiCollectionByIdDTO = { + collectionId: string; +} & Omit; + +export type TUpdatePkiCollectionDTO = { + collectionId: string; + name?: string; +} & Omit; + +export type TDeletePkiCollectionDTO = { + collectionId: string; +} & Omit; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 0d58d7f4b..699b22a97 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -23,6 +23,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 { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; @@ -73,6 +74,7 @@ type TProjectServiceFactoryDep = { certificateAuthorityDAL: Pick; certificateDAL: Pick; alertDAL: Pick; + pkiCollectionDAL: Pick; permissionService: TPermissionServiceFactory; orgService: Pick; licenseService: Pick; @@ -110,6 +112,7 @@ export const projectServiceFactory = ({ identityProjectMembershipRoleDAL, certificateAuthorityDAL, certificateDAL, + pkiCollectionDAL, alertDAL, keyStore, kmsService, @@ -698,7 +701,7 @@ export const projectServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Alerts); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); const alerts = await alertDAL.find({ projectId }); @@ -707,6 +710,33 @@ export const projectServiceFactory = ({ }; }; + /** + * Return list of PKI collections for project + */ + const listProjectPkiCollections = async ({ + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TListProjectAlertsDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); + + const pkiCollections = await pkiCollectionDAL.find({ projectId }); + + return { + pkiCollections + }; + }; + const updateProjectKmsKey = async ({ projectId, kms, @@ -826,6 +856,7 @@ export const projectServiceFactory = ({ listProjectCas, listProjectCertificates, listProjectAlerts, + listProjectPkiCollections, updateVersionLimit, updateAuditLogsRetention, updateProjectKmsKey, diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 7f1700035..04edd80e4 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -8,7 +8,6 @@ export enum ProjectPermissionActions { } export enum ProjectPermissionSub { - Alerts = "alerts", Role = "role", Member = "member", Groups = "groups", @@ -29,6 +28,8 @@ export enum ProjectPermissionSub { Identity = "identity", CertificateAuthorities = "certificate-authorities", Certificates = "certificates", + PkiAlerts = "pki-alerts", + PkiCollections = "pki-collections", Kms = "kms" } @@ -42,7 +43,7 @@ export type ProjectPermissionSet = ProjectPermissionActions, ProjectPermissionSub.Secrets | (ForcedSubject & SubjectFields) ] - | [ProjectPermissionActions, ProjectPermissionSub.Alerts] + | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.Role] | [ProjectPermissionActions, ProjectPermissionSub.Tags] | [ProjectPermissionActions, ProjectPermissionSub.Member] diff --git a/frontend/src/hooks/api/alerts/mutations.tsx b/frontend/src/hooks/api/alerts/mutations.tsx deleted file mode 100644 index 065bbd39d..000000000 --- a/frontend/src/hooks/api/alerts/mutations.tsx +++ /dev/null @@ -1,48 +0,0 @@ -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"; - -export const useCreateAlert = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async (body) => { - const { data: alert } = await apiRequest.post("/api/v1/alerts", body); - return alert; - }, - onSuccess: (_, { projectId }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceAlerts(projectId)); - } - }); -}; - -export const useUpdateAlert = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ alertId, ...body }) => { - const { data: alert } = await apiRequest.patch(`/api/v1/alerts/${alertId}`, body); - return alert; - }, - onSuccess: (_, { projectId, alertId }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceAlerts(projectId)); - queryClient.invalidateQueries(alertKeys.getAlertById(alertId)); - } - }); -}; - -export const useDeleteAlert = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ alertId }) => { - const { data: alert } = await apiRequest.delete(`/api/v1/alerts/${alertId}`); - return alert; - }, - onSuccess: (_, { projectId, alertId }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceAlerts(projectId)); - queryClient.invalidateQueries(alertKeys.getAlertById(alertId)); - } - }); -}; diff --git a/frontend/src/hooks/api/alerts/queries.tsx b/frontend/src/hooks/api/alerts/queries.tsx deleted file mode 100644 index 29e3d88e6..000000000 --- a/frontend/src/hooks/api/alerts/queries.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { apiRequest } from "@app/config/request"; - -import { TAlert } from "./types"; - -export const alertKeys = { - getAlertById: (alertId: string) => [{ alertId }, "alert"] -}; - -export const useGetAlertById = (alertId: string) => { - return useQuery({ - queryKey: alertKeys.getAlertById(alertId), - queryFn: async () => { - const { data: alert } = await apiRequest.get(`/api/v1/alerts/${alertId}`); - return alert; - }, - enabled: Boolean(alertId) - }); -}; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 53ff4fa4e..94037cb62 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -1,6 +1,5 @@ export * from "./accessApproval"; export * from "./admin"; -export * from "./alerts"; export * from "./apiKeys"; export * from "./auditLogs"; export * from "./auditLogStreams"; @@ -22,6 +21,8 @@ export * from "./ldapConfig"; export * from "./oidcConfig"; export * from "./orgAdmin"; export * from "./organization"; +export * from "./pkiAlerts"; +export * from "./pkiCollections"; export * from "./projectUserAdditionalPrivilege"; export * from "./rateLimit"; export * from "./roles"; diff --git a/frontend/src/hooks/api/alerts/index.tsx b/frontend/src/hooks/api/pkiAlerts/index.tsx similarity index 100% rename from frontend/src/hooks/api/alerts/index.tsx rename to frontend/src/hooks/api/pkiAlerts/index.tsx diff --git a/frontend/src/hooks/api/pkiAlerts/mutations.tsx b/frontend/src/hooks/api/pkiAlerts/mutations.tsx new file mode 100644 index 000000000..6e08fe14b --- /dev/null +++ b/frontend/src/hooks/api/pkiAlerts/mutations.tsx @@ -0,0 +1,51 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { workspaceKeys } from "../workspace/queries"; +import { pkiAlertKeys } from "./queries"; +import { TCreatePkiAlertDTO, TDeletePkiAlertDTO, TPkiAlert, TUpdatePkiAlertDTO } from "./types"; + +export const useCreateAlert = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body) => { + const { data: alert } = await apiRequest.post("/api/v1/pki/alerts", body); + return alert; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspacePkiAlerts(projectId)); + } + }); +}; + +export const useUpdateAlert = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ alertId, ...body }) => { + const { data: alert } = await apiRequest.patch( + `/api/v1/pki/alerts/${alertId}`, + body + ); + return alert; + }, + onSuccess: (_, { projectId, alertId }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspacePkiAlerts(projectId)); + queryClient.invalidateQueries(pkiAlertKeys.getPkiAlertById(alertId)); + } + }); +}; + +export const useDeleteAlert = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ alertId }) => { + const { data: alert } = await apiRequest.delete(`/api/v1/pki/alerts/${alertId}`); + return alert; + }, + onSuccess: (_, { projectId, alertId }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspacePkiAlerts(projectId)); + queryClient.invalidateQueries(pkiAlertKeys.getPkiAlertById(alertId)); + } + }); +}; diff --git a/frontend/src/hooks/api/pkiAlerts/queries.tsx b/frontend/src/hooks/api/pkiAlerts/queries.tsx new file mode 100644 index 000000000..cd891f9a1 --- /dev/null +++ b/frontend/src/hooks/api/pkiAlerts/queries.tsx @@ -0,0 +1,20 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TPkiAlert } from "./types"; + +export const pkiAlertKeys = { + getPkiAlertById: (alertId: string) => [{ alertId }, "alert"] +}; + +export const useGetAlertById = (alertId: string) => { + return useQuery({ + queryKey: pkiAlertKeys.getPkiAlertById(alertId), + queryFn: async () => { + const { data: alert } = await apiRequest.get(`/api/v1/pki/alerts/${alertId}`); + return alert; + }, + enabled: Boolean(alertId) + }); +}; diff --git a/frontend/src/hooks/api/alerts/types.ts b/frontend/src/hooks/api/pkiAlerts/types.ts similarity index 64% rename from frontend/src/hooks/api/alerts/types.ts rename to frontend/src/hooks/api/pkiAlerts/types.ts index 70be5fc54..744c4e598 100644 --- a/frontend/src/hooks/api/alerts/types.ts +++ b/frontend/src/hooks/api/pkiAlerts/types.ts @@ -1,29 +1,32 @@ -export type TAlert = { +export type TPkiAlert = { id: string; name: string; projectId: string; + pkiCollectionId: string; alertBeforeDays: number; recipientEmails: string; createdAt: string; updatedAt: string; }; -export type TCreateAlertDTO = { +export type TCreatePkiAlertDTO = { projectId: string; name: string; + pkiCollectionId: string; alertBeforeDays: number; emails: string[]; }; -export type TUpdateAlertDTO = { +export type TUpdatePkiAlertDTO = { alertId: string; projectId: string; + pkiCollectionId?: string; name?: string; alertBeforeDays?: number; emails?: string[]; }; -export type TDeleteAlertDTO = { +export type TDeletePkiAlertDTO = { alertId: string; projectId: string; }; diff --git a/frontend/src/hooks/api/pkiCollections/index.tsx b/frontend/src/hooks/api/pkiCollections/index.tsx new file mode 100644 index 000000000..61f332507 --- /dev/null +++ b/frontend/src/hooks/api/pkiCollections/index.tsx @@ -0,0 +1,5 @@ +export { + useCreatePkiCollection, + useDeletePkiCollection, + useUpdatePkiCollection} from "./mutations"; +export { useGetPkiCollectionById } from "./queries"; diff --git a/frontend/src/hooks/api/pkiCollections/mutations.tsx b/frontend/src/hooks/api/pkiCollections/mutations.tsx new file mode 100644 index 000000000..23de0fca4 --- /dev/null +++ b/frontend/src/hooks/api/pkiCollections/mutations.tsx @@ -0,0 +1,64 @@ +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 { + TCreatePkiCollectionDTO, + TDeletePkiCollectionDTO, + TPkiCollection, + TUpdatePkiCollectionTO} from "./types"; + +export const useCreatePkiCollection = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body) => { + const { data: pkiCollection } = await apiRequest.post( + "/api/v1/pki/collections", + body + ); + return pkiCollection; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspacePkiCollections(projectId)); + } + }); +}; + +export const useUpdatePkiCollection = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ collectionId, ...body }) => { + const { data: pkiCollection } = await apiRequest.patch( + `/api/v1/pki/collections/${collectionId}`, + body + ); + return pkiCollection; + }, + onSuccess: (_, { projectId, collectionId }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspacePkiCollections(projectId)); + queryClient.invalidateQueries(pkiCollectionKeys.getPkiCollectionById(collectionId)); + } + }); +}; + +export const useDeletePkiCollection = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ collectionId }) => { + const { data: pkiCollection } = await apiRequest.delete( + `/api/v1/pki/collections/${collectionId}` + ); + return pkiCollection; + }, + onSuccess: (_, { projectId, collectionId }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspacePkiCollections(projectId)); + queryClient.invalidateQueries(pkiCollectionKeys.getPkiCollectionById(collectionId)); + } + }); +}; + +// TODO: add PKI Collection Item diff --git a/frontend/src/hooks/api/pkiCollections/queries.tsx b/frontend/src/hooks/api/pkiCollections/queries.tsx new file mode 100644 index 000000000..004458d23 --- /dev/null +++ b/frontend/src/hooks/api/pkiCollections/queries.tsx @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TPkiCollection } from "./types"; + +export const pkiCollectionKeys = { + getPkiCollectionById: (collectionId: string) => [{ collectionId }, "pki-collection"] +}; + +export const useGetPkiCollectionById = (collectionId: string) => { + return useQuery({ + queryKey: pkiCollectionKeys.getPkiCollectionById(collectionId), + queryFn: async () => { + const { data: pkiCollection } = await apiRequest.get( + `/api/v1/pki/collections/${collectionId}` + ); + return pkiCollection; + }, + enabled: Boolean(collectionId) + }); +}; diff --git a/frontend/src/hooks/api/pkiCollections/types.ts b/frontend/src/hooks/api/pkiCollections/types.ts new file mode 100644 index 000000000..e372d80da --- /dev/null +++ b/frontend/src/hooks/api/pkiCollections/types.ts @@ -0,0 +1,23 @@ +export type TPkiCollection = { + id: string; + name: string; + projectId: string; + createdAt: string; + updatedAt: string; +}; + +export type TCreatePkiCollectionDTO = { + projectId: string; + name: string; +}; + +export type TUpdatePkiCollectionTO = { + collectionId: string; + projectId: string; + name?: string; +}; + +export type TDeletePkiCollectionDTO = { + collectionId: string; + projectId: string; +}; diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index aa134d80a..81fb0f1be 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -27,10 +27,12 @@ export { useListWorkspaceCas, useListWorkspaceCertificates, useListWorkspaceGroups, + useListWorkspacePkiCollections, useNameWorkspaceSecrets, useRenameWorkspace, useToggleAutoCapitalization, useUpdateIdentityWorkspaceRole, useUpdateUserWorkspaceRole, useUpdateWsEnvironment, - useUpgradeProject} from "./queries"; + useUpgradeProject +} from "./queries"; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 5059f7334..51722111c 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -2,7 +2,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TAlert } from "../alerts/types"; import { CaStatus } from "../ca/enums"; import { TCertificateAuthority } from "../ca/types"; import { TCertificate } from "../certificates/types"; @@ -11,6 +10,8 @@ import { identitiesKeys } from "../identities/queries"; import { IdentityMembership } from "../identities/types"; import { IntegrationAuth } from "../integrationAuth/types"; import { TIntegration } from "../integrations/types"; +import { TPkiAlert } from "../pkiAlerts/types"; +import { TPkiCollection } from "../pkiCollections/types"; import { EncryptedSecret } from "../secrets/types"; import { userKeys } from "../users/queries"; import { TWorkspaceUser } from "../users/types"; @@ -64,7 +65,9 @@ export const workspaceKeys = { offset: number; limit: number; }) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const, - getWorkspaceAlerts: (workspaceId: string) => [{ workspaceId }, "workspace-alerts"] as const + getWorkspacePkiAlerts: (workspaceId: string) => [{ workspaceId }, "workspace-alerts"] as const, + getWorkspacePkiCollections: (workspaceId: string) => + [{ workspaceId }, "workspace-pki-collections"] as const }; const fetchWorkspaceById = async (workspaceId: string) => { @@ -606,14 +609,32 @@ export const useListWorkspaceCertificates = ({ export const useListWorkspaceAlerts = ({ workspaceId }: { workspaceId: string }) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceAlerts(workspaceId), + queryKey: workspaceKeys.getWorkspacePkiAlerts(workspaceId), queryFn: async () => { const { data: { alerts } - } = await apiRequest.get<{ alerts: TAlert[] }>(`/api/v2/workspace/${workspaceId}/alerts`); + } = await apiRequest.get<{ alerts: TPkiAlert[] }>( + `/api/v2/workspace/${workspaceId}/pki-alerts` + ); return { alerts }; }, enabled: Boolean(workspaceId) }); }; + +export const useListWorkspacePkiCollections = ({ workspaceId }: { workspaceId: string }) => { + return useQuery({ + queryKey: workspaceKeys.getWorkspacePkiCollections(workspaceId), + queryFn: async () => { + const { + data: { collections } + } = await apiRequest.get<{ collections: TPkiCollection[] }>( + `/api/v2/workspace/${workspaceId}/pki-collections` + ); + + return { collections }; + }, + enabled: Boolean(workspaceId) + }); +}; diff --git a/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx b/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx index 4f4b4aead..5fd5ee8d9 100644 --- a/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx +++ b/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx @@ -2,12 +2,12 @@ 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 { AlertsTab, CaTab, CertificatesTab } from "./components"; enum TabSections { Ca = "certificate-authorities", Certificates = "certificates", - Alerts = "alerts" + Alerting = "alerting" } export const CertificatesPage = withProjectPermission( @@ -20,7 +20,7 @@ export const CertificatesPage = withProjectPermission( Certificates Certificate Authorities - Alerting + Alerting @@ -28,7 +28,7 @@ export const CertificatesPage = withProjectPermission( - + diff --git a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/AlertsTab.tsx b/frontend/src/views/Project/CertificatesPage/components/AlertsTab/AlertsTab.tsx index b11d2d19a..8d1b12d52 100644 --- a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/AlertsTab.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/AlertsTab/AlertsTab.tsx @@ -1,6 +1,6 @@ import { motion } from "framer-motion"; -import { AlertsSection } from "./components"; +import { AlertsSection,PkiCollectionSection } from "./components"; export const AlertsTab = () => { return ( @@ -11,6 +11,7 @@ export const AlertsTab = () => { animate={{ opacity: 1, translateX: 0 }} exit={{ opacity: 0, translateX: 30 }} > + ); diff --git a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertModal.tsx b/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertModal.tsx index d9591aa85..6bcc56875 100644 --- a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertModal.tsx @@ -4,15 +4,53 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; +import { + Button, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem, + TextArea} from "@app/components/v2"; import { useWorkspace } from "@app/context"; -import { useCreateAlert, useGetAlertById,useUpdateAlert } from "@app/hooks/api"; +import { + useCreateAlert, + useGetAlertById, + useListWorkspacePkiCollections, + useUpdateAlert} from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; +enum TimeUnit { + DAY = "days", + WEEK = "weeks", + MONTH = "months", + YEAR = "years" +} + const schema = z.object({ - name: z.string().trim().min(1) + name: z.string().trim().min(1), + pkiCollectionId: z.string(), + alertBefore: z.string(), + alertUnit: z.nativeEnum(TimeUnit), + emails: z.string().trim() }); +const convertToDays = (unit: TimeUnit, value: number) => { + switch (unit) { + case TimeUnit.DAY: + return value; + case TimeUnit.WEEK: + return value * 7; + case TimeUnit.MONTH: + return value * 30; + case TimeUnit.YEAR: + return value * 365; + default: + throw new Error(`Unknown time unit: ${unit}`); + } +}; + export type FormData = z.infer; type Props = { @@ -28,6 +66,10 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => { (popUp?.alert?.data as { alertId: string })?.alertId || "" ); + const { data: pkiCollections } = useListWorkspacePkiCollections({ + workspaceId: projectId + }); + const { mutateAsync: createAlert } = useCreateAlert(); const { mutateAsync: updateAlert } = useUpdateAlert(); @@ -37,13 +79,20 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => { reset, formState: { isSubmitting } } = useForm({ - resolver: zodResolver(schema) + resolver: zodResolver(schema), + defaultValues: { + alertUnit: TimeUnit.DAY + } }); useEffect(() => { if (alert) { reset({ - name: alert.name + name: alert.name, + pkiCollectionId: alert.pkiCollectionId, + alertBefore: alert.alertBeforeDays.toString(), + alertUnit: TimeUnit.DAY, + emails: alert.recipientEmails }); } else { reset({ @@ -52,26 +101,41 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => { } }, [alert]); - const onFormSubmit = async ({ name }: FormData) => { + const onFormSubmit = async ({ + name, + pkiCollectionId, + alertBefore, + alertUnit, + emails + }: FormData) => { try { if (!projectId) return; + const emailArray = emails + .split(",") + .map((email) => email.trim()) + .filter((email) => email.length > 0); + + const alertBeforeDays = convertToDays(alertUnit, Number(alertBefore)); + if (alert) { // update await updateAlert({ alertId: alert.id, + pkiCollectionId, name, projectId, - alertBeforeDays: 3, - emails: ["test"] + alertBeforeDays, + emails: emailArray }); } else { // create await createAlert({ name, projectId, - alertBeforeDays: 3, - emails: ["test"] + pkiCollectionId, + alertBeforeDays, + emails: emailArray }); } @@ -107,11 +171,104 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => { defaultValue="" name="name" render={({ field, fieldState: { error } }) => ( - + )} /> + ( + + + + )} + /> +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ ( + +