diff --git a/backend/package-lock.json b/backend/package-lock.json index cdaee75f4..77a18b567 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -25,6 +25,7 @@ "@node-saml/passport-saml": "^4.0.4", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", + "@peculiar/x509": "^1.10.0", "@serdnam/pino-cloudwatch-transport": "^1.0.4", "@sindresorhus/slugify": "^2.2.1", "@ucast/mongo2js": "^1.3.4", @@ -2458,9 +2459,9 @@ } }, "node_modules/@fastify/session": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/@fastify/session/-/session-10.7.0.tgz", - "integrity": "sha512-ECA75gnyaxcyIukgyO2NGT3XdbLReNl/pTKrrkRfDc6pVqNtdptwwfx9KXrIMOfsO4B3m84eF3wZ9GgnebiZ4w==", + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/@fastify/session/-/session-10.9.0.tgz", + "integrity": "sha512-u/c42RuAaxCeEuRCAwK2+/SfGqKOd0NSyRzEvDwFBWySQoKUZQyb9OmmJSWJBbOP1OfaU2OsDrjbPbghE1l/YQ==", "dependencies": { "fastify-plugin": "^4.0.0", "safe-stable-stringify": "^2.3.1" @@ -3298,6 +3299,149 @@ "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.1.0.tgz", "integrity": "sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w==" }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.8.tgz", + "integrity": "sha512-Wtk9R7yQxGaIaawHorWKP2OOOm/RZzamOmSWwaqGphIuU6TcKYih0slL6asZlSSZtVoYTrBfrddSOD/jTu9vuQ==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8", + "@peculiar/asn1-x509-attr": "^2.3.8", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.3.8.tgz", + "integrity": "sha512-ZmAaP2hfzgIGdMLcot8gHTykzoI+X/S53x1xoGbTmratETIaAbSWMiPGvZmXRA0SNEIydpMkzYtq4fQBxN1u1w==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.8.tgz", + "integrity": "sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.3.8.tgz", + "integrity": "sha512-XhdnCVznMmSmgy68B9pVxiZ1XkKoE1BjO4Hv+eUGiY1pM14msLsFZ3N7K46SoITIVZLq92kKkXpGiTfRjlNLyg==", + "dependencies": { + "@peculiar/asn1-cms": "^2.3.8", + "@peculiar/asn1-pkcs8": "^2.3.8", + "@peculiar/asn1-rsa": "^2.3.8", + "@peculiar/asn1-schema": "^2.3.8", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.3.8.tgz", + "integrity": "sha512-rL8k2x59v8lZiwLRqdMMmOJ30GHt6yuHISFIuuWivWjAJjnxzZBVzMTQ72sknX5MeTSSvGwPmEFk2/N8+UztFQ==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.3.8.tgz", + "integrity": "sha512-+nONq5tcK7vm3qdY7ZKoSQGQjhJYMJbwJGbXLFOhmqsFIxEWyQPHyV99+wshOjpOjg0wUSSkEEzX2hx5P6EKeQ==", + "dependencies": { + "@peculiar/asn1-cms": "^2.3.8", + "@peculiar/asn1-pfx": "^2.3.8", + "@peculiar/asn1-pkcs8": "^2.3.8", + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8", + "@peculiar/asn1-x509-attr": "^2.3.8", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.8.tgz", + "integrity": "sha512-ES/RVEHu8VMYXgrg3gjb1m/XG0KJWnV4qyZZ7mAg7rrF3VTmRbLxO8mk+uy0Hme7geSMebp+Wvi2U6RLLEs12Q==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz", + "integrity": "sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==", + "dependencies": { + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.8.tgz", + "integrity": "sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.8", + "asn1js": "^3.0.5", + "ipaddr.js": "^2.1.0", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.3.8.tgz", + "integrity": "sha512-4Z8mSN95MOuX04Aku9BUyMdsMKtVQUqWnr627IheiWnwFoheUhX3R4Y2zh23M7m80r4/WG8MOAckRKc77IRv6g==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-x509/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.10.0.tgz", + "integrity": "sha512-gdH6H8gWjAYoM4Yr6wPnRbzU77nU7xq/jipqYyyv5/AHTrulN2Z5DlnOSq9jjKrB+Ya0D6YJ2cGGtwkWDK75jA==", + "dependencies": { + "@peculiar/asn1-cms": "^2.3.8", + "@peculiar/asn1-csr": "^2.3.8", + "@peculiar/asn1-ecc": "^2.3.8", + "@peculiar/asn1-pkcs9": "^2.3.8", + "@peculiar/asn1-rsa": "^2.3.8", + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8", + "pvtsutils": "^1.3.5", + "reflect-metadata": "^0.2.2", + "tslib": "^2.6.2", + "tsyringe": "^4.8.0" + } + }, "node_modules/@phc/format": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", @@ -5948,6 +6092,19 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/asn1js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", + "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "dependencies": { + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -11701,6 +11858,22 @@ "node": ">=6" } }, + "node_modules/pvtsutils": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz", + "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", + "dependencies": { + "tslib": "^2.6.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -11882,6 +12055,11 @@ "node": ">=4" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -13665,6 +13843,22 @@ "fsevents": "~2.3.3" } }, + "node_modules/tsyringe": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz", + "integrity": "sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", diff --git a/backend/package.json b/backend/package.json index 85538d45a..720efb34b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -86,6 +86,7 @@ "@node-saml/passport-saml": "^4.0.4", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", + "@peculiar/x509": "^1.10.0", "@serdnam/pino-cloudwatch-transport": "^1.0.4", "@sindresorhus/slugify": "^2.2.1", "@ucast/mongo2js": "^1.3.4", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 4776e26dc..38d49a807 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -29,6 +29,7 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service"; import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; +import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; @@ -132,6 +133,7 @@ declare module "fastify" { ldap: TLdapConfigServiceFactory; auditLog: TAuditLogServiceFactory; auditLogStream: TAuditLogStreamServiceFactory; + certificateAuthority: TCertificateAuthorityServiceFactory; secretScanning: TSecretScanningServiceFactory; license: TLicenseServiceFactory; trustedIp: TTrustedIpServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 291197b0b..cd4c9418c 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -32,6 +32,15 @@ import { TBackupPrivateKey, TBackupPrivateKeyInsert, TBackupPrivateKeyUpdate, + TCertificateAuthorities, + TCertificateAuthoritiesInsert, + TCertificateAuthoritiesUpdate, + TCertificateAuthorityCerts, + TCertificateAuthorityCertsInsert, + TCertificateAuthorityCertsUpdate, + TCertificateAuthoritySk, + TCertificateAuthoritySkInsert, + TCertificateAuthoritySkUpdate, TDynamicSecretLeases, TDynamicSecretLeasesInsert, TDynamicSecretLeasesUpdate, @@ -240,6 +249,21 @@ declare module "knex/types/tables" { interface Tables { [TableName.Users]: Knex.CompositeTableType; [TableName.Groups]: Knex.CompositeTableType; + [TableName.CertificateAuthority]: Knex.CompositeTableType< + TCertificateAuthorities, + TCertificateAuthoritiesInsert, + TCertificateAuthoritiesUpdate + >; + [TableName.CertificateAuthorityCert]: Knex.CompositeTableType< + TCertificateAuthorityCerts, + TCertificateAuthorityCertsInsert, + TCertificateAuthorityCertsUpdate + >; + [TableName.CertificateAuthoritySk]: Knex.CompositeTableType< + TCertificateAuthoritySk, + TCertificateAuthoritySkInsert, + TCertificateAuthoritySkUpdate + >; [TableName.UserGroupMembership]: Knex.CompositeTableType< TUserGroupMembership, TUserGroupMembershipInsert, diff --git a/backend/src/db/migrations/20240522010055_certificate-authority.ts b/backend/src/db/migrations/20240522010055_certificate-authority.ts new file mode 100644 index 000000000..860fb8a86 --- /dev/null +++ b/backend/src/db/migrations/20240522010055_certificate-authority.ts @@ -0,0 +1,58 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.CertificateAuthority))) { + // TODO: consider adding algo details + await knex.schema.createTable(TableName.CertificateAuthority, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.uuid("parentCaId").nullable(); + t.foreign("parentCaId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE"); + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.string("type").notNullable(); // root / intermediate + t.string("dn").notNullable(); + }); + } + + if (!(await knex.schema.hasTable(TableName.CertificateAuthorityCert))) { + await knex.schema.createTable(TableName.CertificateAuthorityCert, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.uuid("caId").notNullable().unique(); // TODO: consider that cert can be rotated so may be multiple / non-unique + t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE"); + t.text("certificate").notNullable(); // TODO: encrypt + t.text("certificateChain").notNullable(); // TODO: encrypt + }); + } + + // TODO: consider renaming this to CertificateAuthoritySecret + if (!(await knex.schema.hasTable(TableName.CertificateAuthoritySk))) { + await knex.schema.createTable(TableName.CertificateAuthoritySk, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.uuid("caId").notNullable().unique(); + t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE"); + t.text("pk").notNullable(); // TODO: encrypt + t.text("sk").notNullable(); // TODO: encrypt + }); + } + + await createOnUpdateTrigger(knex, TableName.CertificateAuthority); + await createOnUpdateTrigger(knex, TableName.CertificateAuthorityCert); + await createOnUpdateTrigger(knex, TableName.CertificateAuthoritySk); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.CertificateAuthoritySk); + await dropOnUpdateTrigger(knex, TableName.CertificateAuthoritySk); + + await knex.schema.dropTableIfExists(TableName.CertificateAuthorityCert); + await dropOnUpdateTrigger(knex, TableName.CertificateAuthorityCert); + + await knex.schema.dropTableIfExists(TableName.CertificateAuthority); + await dropOnUpdateTrigger(knex, TableName.CertificateAuthority); +} diff --git a/backend/src/db/schemas/certificate-authorities.ts b/backend/src/db/schemas/certificate-authorities.ts new file mode 100644 index 000000000..b49a151c1 --- /dev/null +++ b/backend/src/db/schemas/certificate-authorities.ts @@ -0,0 +1,24 @@ +// 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 CertificateAuthoritiesSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + parentCaId: z.string().uuid().nullable().optional(), + projectId: z.string(), + type: z.string(), + dn: z.string() +}); + +export type TCertificateAuthorities = z.infer; +export type TCertificateAuthoritiesInsert = Omit, TImmutableDBKeys>; +export type TCertificateAuthoritiesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/certificate-authority-certs.ts b/backend/src/db/schemas/certificate-authority-certs.ts new file mode 100644 index 000000000..ec3d6b8e1 --- /dev/null +++ b/backend/src/db/schemas/certificate-authority-certs.ts @@ -0,0 +1,23 @@ +// 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 CertificateAuthorityCertsSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + caId: z.string().uuid(), + certificate: z.string(), + certificateChain: z.string() +}); + +export type TCertificateAuthorityCerts = z.infer; +export type TCertificateAuthorityCertsInsert = Omit, TImmutableDBKeys>; +export type TCertificateAuthorityCertsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/certificate-authority-sk.ts b/backend/src/db/schemas/certificate-authority-sk.ts new file mode 100644 index 000000000..b3eca38e2 --- /dev/null +++ b/backend/src/db/schemas/certificate-authority-sk.ts @@ -0,0 +1,23 @@ +// 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 CertificateAuthoritySkSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + caId: z.string().uuid(), + pk: z.string(), + sk: z.string() +}); + +export type TCertificateAuthoritySk = z.infer; +export type TCertificateAuthoritySkInsert = Omit, TImmutableDBKeys>; +export type TCertificateAuthoritySkUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index cffa4f492..b82f1a992 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -8,6 +8,9 @@ export * from "./audit-logs"; export * from "./auth-token-sessions"; export * from "./auth-tokens"; export * from "./backup-private-key"; +export * from "./certificate-authorities"; +export * from "./certificate-authority-certs"; +export * from "./certificate-authority-sk"; export * from "./dynamic-secret-leases"; export * from "./dynamic-secrets"; export * from "./git-app-install-sessions"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 28a6973b7..46c16eb1a 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -2,6 +2,9 @@ import { z } from "zod"; export enum TableName { Users = "users", + CertificateAuthority = "certificate_authorities", + CertificateAuthorityCert = "certificate_authority_certs", + CertificateAuthoritySk = "certificate_authority_sk", Groups = "groups", GroupProjectMembership = "group_project_memberships", GroupProjectMembershipRole = "group_project_membership_roles", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 1e5cb3bf9..2f8ef02de 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -71,6 +71,11 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service"; import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal"; import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service"; +import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; +// ca / certs +import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; +import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; +import { certificateAuthoritySkDALFactory } from "@app/services/certificate-authority/certificate-authority-sk-dal"; import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal"; import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service"; @@ -221,6 +226,16 @@ export const registerRoutes = async ( const trustedIpDAL = trustedIpDALFactory(db); const telemetryDAL = telemetryDALFactory(db); + const certificateAuthorityDAL = certificateAuthorityDALFactory(db); + const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db); + const certificateAuthoritySkDAL = certificateAuthoritySkDALFactory(db); + const certificateAuthorityService = certificateAuthorityServiceFactory({ + certificateAuthorityDAL, + certificateAuthorityCertDAL, + certificateAuthoritySkDAL, + projectDAL + }); + // ee db layer ops const permissionDAL = permissionDALFactory(db); const samlConfigDAL = samlConfigDALFactory(db); @@ -504,6 +519,7 @@ export const registerRoutes = async ( projectMembershipDAL, folderDAL, licenseService, + certificateAuthorityDAL, projectUserMembershipRoleDAL, identityProjectMembershipRoleDAL, keyStore @@ -826,6 +842,7 @@ export const registerRoutes = async ( ldap: ldapService, auditLog: auditLogService, auditLogStream: auditLogStreamService, + certificateAuthority: certificateAuthorityService, secretScanning: secretScanningService, license: licenseService, trustedIp: trustedIpService, diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts new file mode 100644 index 000000000..bfb670ac3 --- /dev/null +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -0,0 +1,151 @@ +import { z } from "zod"; + +import { CertificateAuthoritiesSchema } from "@app/db/schemas"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { CAType } from "@app/services/certificate-authority/certificate-authority-types"; + +export const registerCaRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Create CA", + body: z.object({ + projectSlug: z.string().trim(), + type: z.enum([CAType.ROOT, CAType.INTERMEDIATE]), + commonName: z.string().trim(), + organization: z.string().trim(), + ou: z.string().trim(), + country: z.string().trim(), + province: z.string().trim(), + locality: z.string().trim() + }), + response: { + 200: CertificateAuthoritiesSchema + } + }, + handler: async (req) => { + const ca = await server.services.certificateAuthority.createCa({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + return ca; + } + }); + + server.route({ + method: "GET", + url: "/:caId/csr", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Get CA CSR", + params: z.object({ + caId: z.string().trim() + }), + response: { + 200: z.object({ + csr: z.string() + }) + } + }, + handler: async (req) => { + const csr = await server.services.certificateAuthority.getCaCsr({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + return { + csr + }; + } + }); + + server.route({ + method: "GET", + url: ":caId/certificate", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Get cert and cert chain of a CA", + params: z.object({ + caId: z.string().trim() + }), + response: { + 200: z.object({ + certificate: z.string(), + certificateChain: z.string() + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain } = await server.services.certificateAuthority.getCaCert({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + return { + certificate, + certificateChain + }; + } + }); + + server.route({ + method: "POST", + url: ":caId/issue-certificate", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Issue (leaf) certificate from CA", + params: z.object({ + caId: z.string().trim() + }), + body: z.object({ + csr: z.string().trim(), + notBefore: z.string().trim(), + notAfter: z.string().trim() + }), + response: { + 200: z.object({ + certificateId: z.string().trim() + }) + } + }, + handler: async () => { + // await server.services.certificateAuthority.issueCertFromCa({ + // caId: req.params.caId, + // actor: req.permission.type, + // actorId: req.permission.id, + // actorAuthMethod: req.permission.authMethod, + // actorOrgId: req.permission.orgId, + // ...req.body + // }); + return { + certificateId: "111" + }; + } + }); + + // TODO 2: logic for creating intermediary ca + // TODO 1: get certificate + certificate chain for the root and intermediary CA GET /ca/:caId/certificate +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 262e3cb20..72439cd10 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -1,6 +1,7 @@ import { registerAdminRouter } from "./admin-router"; import { registerAuthRoutes } from "./auth-router"; import { registerProjectBotRouter } from "./bot-router"; +import { registerCaRouter } from "./certificate-authority-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router"; import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router"; @@ -58,6 +59,13 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { { prefix: "/workspace" } ); + await server.register( + async (certRouter) => { + await certRouter.register(registerCaRouter); + }, + { prefix: "/ca" } + ); + await server.register(registerProjectBotRouter, { prefix: "/bot" }); await server.register(registerIntegrationRouter, { prefix: "/integration" }); await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" }); diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index a199cf0d4..3653daae4 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -1,7 +1,7 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; -import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas"; +import { CertificateAuthoritiesSchema, ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { PROJECTS } from "@app/lib/api-docs"; import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -307,4 +307,38 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return project; } }); + + server.route({ + method: "GET", + url: "/:slug/cas", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + slug: slugSchema.describe("The slug of the project to list CAs.") + }), + response: { + 200: z.object({ + cas: z.array(CertificateAuthoritiesSchema) + }) + } + }, + + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const cas = await server.services.project.listProjectCas({ + filter: { + slug: req.params.slug, + orgId: req.permission.orgId, + type: ProjectFilterType.SLUG + }, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type + }); + return { cas }; + } + }); }; diff --git a/backend/src/services/certificate-authority/certificate-authority-cert-dal.ts b/backend/src/services/certificate-authority/certificate-authority-cert-dal.ts new file mode 100644 index 000000000..763240986 --- /dev/null +++ b/backend/src/services/certificate-authority/certificate-authority-cert-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TCertificateAuthorityCertDALFactory = ReturnType; + +export const certificateAuthorityCertDALFactory = (db: TDbClient) => { + const caCertOrm = ormify(db, TableName.CertificateAuthorityCert); + return caCertOrm; +}; diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts new file mode 100644 index 000000000..40d868560 --- /dev/null +++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TCertificateAuthorityDALFactory = ReturnType; + +export const certificateAuthorityDALFactory = (db: TDbClient) => { + const caOrm = ormify(db, TableName.CertificateAuthority); + return caOrm; +}; diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts new file mode 100644 index 000000000..ffb4ee187 --- /dev/null +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -0,0 +1,268 @@ +import * as x509 from "@peculiar/x509"; +import crypto, { KeyObject } from "crypto"; + +import { BadRequestError } from "@app/lib/errors"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; + +import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; +import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; +import { TCertificateAuthoritySkDALFactory } from "./certificate-authority-sk-dal"; +import { + // TIssueCertFromCaDTO, + CAType, + TCreateCaDTO, + TGetCaCertDTO, + TGetCaCsrDTO +} from "./certificate-authority-types"; + +type TCertificateAuthorityServiceFactoryDep = { + // TODO: Pick + certificateAuthorityDAL: TCertificateAuthorityDALFactory; + certificateAuthorityCertDAL: TCertificateAuthorityCertDALFactory; + certificateAuthoritySkDAL: TCertificateAuthoritySkDALFactory; + projectDAL: TProjectDALFactory; +}; + +export type TCertificateAuthorityServiceFactory = ReturnType; + +type DNParts = { + commonName?: string; + organization?: string; + ou?: string; + country?: string; + province?: string; + locality?: string; +}; + +function createDistinguishedName(parts: DNParts) { + const dnParts = []; + if (parts.country) dnParts.push(`C=${parts.country}`); + if (parts.organization) dnParts.push(`O=${parts.organization}`); + if (parts.ou) dnParts.push(`OU=${parts.ou}`); + if (parts.province) dnParts.push(`ST=${parts.province}`); + if (parts.commonName) dnParts.push(`CN=${parts.commonName}`); + if (parts.locality) dnParts.push(`L=${parts.locality}`); + return dnParts.join(", "); +} + +export const certificateAuthorityServiceFactory = ({ + certificateAuthorityDAL, + certificateAuthorityCertDAL, + certificateAuthoritySkDAL, + projectDAL +}: TCertificateAuthorityServiceFactoryDep) => { + /** + * Generates a new root or intermediate CA + * @param param0 + * @returns + */ + const createCa = async ({ + projectSlug, + type, + commonName, + organization, + ou, + country, + province, + locality, + // actorId, + // actorAuthMethod, + // actor, + actorOrgId + }: TCreateCaDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + + const dn = createDistinguishedName({ + commonName, + organization, + ou, + country, + province, + locality + }); + + const alg = { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 2048 + }; + const keys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey + const skObj = KeyObject.from(keys.privateKey); + const sk = skObj.export({ format: "pem", type: "pkcs8" }) as string; + const pkObj = KeyObject.from(keys.publicKey); + const pk = pkObj.export({ format: "pem", type: "spki" }) as string; + + const newCa = await certificateAuthorityDAL.transaction(async (tx) => { + const ca = await certificateAuthorityDAL.create( + { + projectId: project.id, + type, + dn + }, + tx + ); + + if (type === CAType.ROOT) { + // note: self-signed cert only applicable for root CA + const cert = await x509.X509CertificateGenerator.createSelfSigned({ + name: dn, + notBefore: new Date("2020/01/01"), + notAfter: new Date("2020/01/02"), + signingAlgorithm: alg, + keys, + extensions: [ + new x509.BasicConstraintsExtension(true, 2, true), + new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true), + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), + await x509.SubjectKeyIdentifierExtension.create(keys.publicKey) + ] + }); + const certificate = cert.toString("pem"); + await certificateAuthorityCertDAL.create( + { + caId: ca.id, + certificate, // TODO: encrypt + certificateChain: "" // TODO: encrypt + }, + tx + ); + } + + await certificateAuthoritySkDAL.create( + { + caId: ca.id, + pk, // TODO: encrypt + sk // TODO: encrypt + }, + tx + ); + + return ca; + }); + + return newCa; + }; + + /** + * Generates a CSR for a CA + */ + const getCaCsr = async ({ caId }: TGetCaCsrDTO) => { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) throw new BadRequestError({ message: "CA not found" }); + // TODO: permissioning + + const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); + const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id }); + + const alg = { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 2048 + }; + + const skObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" }); + const pkObj = crypto.createPublicKey({ key: caKeys.pk, format: "pem", type: "spki" }); + + const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [ + "sign" + ]); + const pk = await crypto.subtle.importKey("spki", pkObj.export({ format: "der", type: "spki" }), alg, true, [ + "verify" + ]); + + const cert = new x509.X509Certificate(caCert.certificate); + + const csr = await x509.Pkcs10CertificateRequestGenerator.create({ + name: cert.subject || ca.dn, + keys: { privateKey: sk, publicKey: pk }, + signingAlgorithm: alg, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment) + ], + attributes: [new x509.ChallengePasswordAttribute("password")] + }); + + return csr.toString("base64"); + }; + + /** + * Return certificate and certificate chain for CA + */ + const getCaCert = async ({ caId }: TGetCaCertDTO) => { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) throw new BadRequestError({ message: "CA not found" }); + // TODO: permissioning + + const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); + + return { + certificate: caCert.certificate, + certificateChain: caCert.certificateChain + }; + }; + + /** + * Issue new certificate + */ + const issueCertFromCa = async () => { + // WIP: parse publicKey from CSR + // const csrArrayBuffer = Uint8Array.from(atob(csr), (c) => c.charCodeAt(0)); + // const csrR = new x509.Pkcs10CertificateRequest(csrArrayBuffer); + + // const ca = await certificateAuthorityDAL.findById(caId); + // if (!ca) throw new BadRequestError({ message: "CA not found" }); + // // TODO: permissioning + + // const alg = { + // name: "RSASSA-PKCS1-v1_5", + // hash: "SHA-256", + // publicExponent: new Uint8Array([1, 0, 1]), + // modulusLength: 2048 + // }; + + // const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); + // const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id }); + + // const skObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" }); + // const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [ + // "sign" + // ]); + + // const cert = new x509.X509Certificate(caCert.certificate); + + // const leafCert = await x509.X509CertificateGenerator.create({ + // // serialNumber: "03", + // subject: "CN=Leaf", // TODO: make dynamic + // issuer: cert.subject, + // notBefore: new Date(notBefore), + // notAfter: new Date(notAfter), + // signingKey: sk, + // publicKey: leafKeys.publicKey, + // signingAlgorithm: alg, + // extensions: [ + // new x509.KeyUsagesExtension(x509.KeyUsageFlags.dataEncipherment, true), + // new x509.BasicConstraintsExtension(false), + // await x509.AuthorityKeyIdentifierExtension.create(cert, false), + // await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey) + // ] + // }); + + // console.log("leafCert: ", leafCert); + + return ""; + }; + + return { + createCa, + getCaCsr, + getCaCert, + issueCertFromCa + }; +}; diff --git a/backend/src/services/certificate-authority/certificate-authority-sk-dal.ts b/backend/src/services/certificate-authority/certificate-authority-sk-dal.ts new file mode 100644 index 000000000..6c9333243 --- /dev/null +++ b/backend/src/services/certificate-authority/certificate-authority-sk-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TCertificateAuthoritySkDALFactory = ReturnType; + +export const certificateAuthoritySkDALFactory = (db: TDbClient) => { + const caSkOrm = ormify(db, TableName.CertificateAuthoritySk); + return caSkOrm; +}; diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts new file mode 100644 index 000000000..0ad259829 --- /dev/null +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -0,0 +1,34 @@ +import { TProjectPermission } from "@app/lib/types"; + +export enum CAType { + ROOT = "root", + INTERMEDIATE = "intermediate" +} + +// TODO: attach permissions after draft impl + +export type TCreateCaDTO = { + projectSlug: string; + type: CAType; + commonName: string; + organization: string; + ou: string; + country: string; + province: string; + locality: string; +} & Omit; + +export type TGetCaCsrDTO = { + caId: string; +} & Omit; + +export type TGetCaCertDTO = { + caId: string; +} & Omit; + +export type TIssueCertFromCaDTO = { + caId: string; + csr: string; + notBefore: string; + notAfter: string; +} & Omit; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 008dac593..64f2bf7ac 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -16,6 +16,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TProjectPermission } from "@app/lib/types"; import { ActorType } from "../auth/auth-type"; +import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; @@ -36,6 +37,7 @@ import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO, + TListProjectCasDTO, TToggleProjectAutoCapitalizationDTO, TUpdateProjectDTO, TUpdateProjectNameDTO, @@ -62,6 +64,7 @@ type TProjectServiceFactoryDep = { projectMembershipDAL: Pick; projectUserMembershipRoleDAL: Pick; secretBlindIndexDAL: Pick; + certificateAuthorityDAL: Pick; permissionService: TPermissionServiceFactory; orgService: Pick; licenseService: Pick; @@ -89,6 +92,7 @@ export const projectServiceFactory = ({ licenseService, projectUserMembershipRoleDAL, identityProjectMembershipRoleDAL, + certificateAuthorityDAL, keyStore }: TProjectServiceFactoryDep) => { /* @@ -492,6 +496,30 @@ export const projectServiceFactory = ({ return project.upgradeStatus || null; }; + /** + * Return list of CAs for project + */ + const listProjectCas = async ({ + // actorId, + // actorOrgId, + // actorAuthMethod, + filter // actor + }: TListProjectCasDTO) => { + const project = await projectDAL.findProjectByFilter(filter); + + // const { permission } = await permissionService.getProjectPermission( + // actor, + // actorId, + // project.id, + // actorAuthMethod, + // actorOrgId + // ); + // TODO: add permissioning + + const cas = await certificateAuthorityDAL.find({ projectId: project.id }); + return cas; + }; + return { createProject, deleteProject, @@ -501,6 +529,7 @@ export const projectServiceFactory = ({ getAProject, toggleAutoCapitalization, updateName, - upgradeProject + upgradeProject, + listProjectCas }; }; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index dcd424e18..f6268a93e 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -75,3 +75,7 @@ export type AddUserToWsDTO = { userPublicKey: string; }[]; }; + +export type TListProjectCasDTO = { + filter: Filter; +} & Omit; diff --git a/frontend/src/hooks/api/ca/constants.tsx b/frontend/src/hooks/api/ca/constants.tsx new file mode 100644 index 000000000..d368d3c04 --- /dev/null +++ b/frontend/src/hooks/api/ca/constants.tsx @@ -0,0 +1,6 @@ +import { CertificateAuthorityType } from "./enums"; + +export const caTypeToNameMap: { [K in CertificateAuthorityType]: string } = { + [CertificateAuthorityType.ROOT]: "Root", + [CertificateAuthorityType.INTERMEDIATE]: "Intermediate" +}; diff --git a/frontend/src/hooks/api/ca/enums.tsx b/frontend/src/hooks/api/ca/enums.tsx new file mode 100644 index 000000000..0db5fcfdb --- /dev/null +++ b/frontend/src/hooks/api/ca/enums.tsx @@ -0,0 +1,4 @@ +export enum CertificateAuthorityType { + ROOT = "root", + INTERMEDIATE = "intermediate" +} diff --git a/frontend/src/hooks/api/ca/index.tsx b/frontend/src/hooks/api/ca/index.tsx new file mode 100644 index 000000000..f03d26abc --- /dev/null +++ b/frontend/src/hooks/api/ca/index.tsx @@ -0,0 +1,2 @@ +export { CertificateAuthorityType } from "./enums"; +export { useCreateCa } from "./mutations"; diff --git a/frontend/src/hooks/api/ca/mutations.tsx b/frontend/src/hooks/api/ca/mutations.tsx new file mode 100644 index 000000000..38cb1cf9f --- /dev/null +++ b/frontend/src/hooks/api/ca/mutations.tsx @@ -0,0 +1,23 @@ +import { + useMutation + // useQueryClient +} from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TCertificateAuthority, TCreateCaDTO } from "./types"; + +export const useCreateCa = () => { + // const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body) => { + const { + data: { identity } + } = await apiRequest.post("/api/v1/ca/", body); + return identity; + }, + onSuccess: () => { + // queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts new file mode 100644 index 000000000..34dcbb536 --- /dev/null +++ b/frontend/src/hooks/api/ca/types.ts @@ -0,0 +1,23 @@ +import { CertificateAuthorityType } from "./enums"; + +export type TCertificateAuthority = { + id: string; + parentCaId?: string; + projectId: string; + type: CertificateAuthorityType; + dn: string; + commonName: string; + createdAt: string; + updatedAt: string; +}; + +export type TCreateCaDTO = { + projectSlug: string; + type: string; + organization: string; + ou: string; + country: string; + province: string; + locality: string; + commonName: string; +}; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 61e8cb666..64e306714 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -5,6 +5,7 @@ export * from "./auditLogs"; export * from "./auditLogStreams"; export * from "./auth"; export * from "./bots"; +export * from "./ca"; export * from "./dynamicSecret"; export * from "./dynamicSecretLease"; export * from "./groups"; diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index b0cadac23..58c54f126 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -21,6 +21,7 @@ export { useGetWorkspaceIntegrations, useGetWorkspaceSecrets, useGetWorkspaceUsers, + useListWorkspaceCas, useListWorkspaceGroups, useNameWorkspaceSecrets, useRenameWorkspace, @@ -28,5 +29,4 @@ export { 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 71dbb8e01..07bd2901a 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { TCertificateAuthority } from "../ca/types"; import { TGroupMembership } from "../groups/types"; import { IdentityMembership } from "../identities/types"; import { IntegrationAuth } from "../integrationAuth/types"; @@ -39,7 +40,8 @@ export const workspaceKeys = { getWorkspaceIdentityMemberships: (workspaceId: string) => [{ workspaceId }, "workspace-identity-memberships"] as const, getWorkspaceGroupMemberships: (workspaceId: string) => - [{ workspaceId }, "workspace-groups"] as const + [{ workspaceId }, "workspace-groups"] as const, + getWorkspaceCas: (workspaceId: string) => [{ workspaceId }, "workspace-cas"] as const }; const fetchWorkspaceById = async (workspaceId: string) => { @@ -469,3 +471,18 @@ export const useListWorkspaceGroups = (projectSlug: string) => { enabled: true }); }; + +export const useListWorkspaceCas = (projectSlug: string) => { + return useQuery({ + queryKey: workspaceKeys.getWorkspaceCas(projectSlug), + queryFn: async () => { + const { + data: { cas } + } = await apiRequest.get<{ cas: TCertificateAuthority[] }>( + `/api/v2/workspace/${projectSlug}/cas` + ); + return cas; + }, + enabled: Boolean(projectSlug) + }); +}; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 2fcdc9339..b26f1965d 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -519,6 +519,18 @@ export const AppLayout = ({ children }: LayoutProps) => { + + + + Certificates + + + { + const { t } = useTranslation(); + + return ( +
+ + {t("common.head-title", { title: t("settings.project.title") })} + + + +
Test CA page
+ +
+ ); +}; + +export default Certificates; + +Certificates.requireAuth = true; diff --git a/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx b/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx new file mode 100644 index 000000000..655e83ad5 --- /dev/null +++ b/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx @@ -0,0 +1,44 @@ +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { withProjectPermission } from "@app/hoc"; + +import { CaTab } from "./components"; +// import { CaTable } from "./components"; + +// TODO: fix permission + +/** + * TODO 1: CA section + * TODO 2: Certificates section + */ + +enum TabSections { + Ca = "certificate-authorities", + Certificates = "certificates" +} + +export const CertificatesPage = withProjectPermission( + () => { + return ( +
+
+

Certificates

+ + + Certificate Authorities + Certificates + + + + + +
Certs
+ {/* */} +
+
+
+
+ ); + }, + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs } +); diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/CaTab.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/CaTab.tsx new file mode 100644 index 000000000..aadca4dfe --- /dev/null +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/CaTab.tsx @@ -0,0 +1,17 @@ +import { motion } from "framer-motion"; + +import { CaSection } from "./components"; + +export const CaTab = () => { + return ( + + + + ); +}; diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaModal.tsx new file mode 100644 index 000000000..b9461c6bb --- /dev/null +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaModal.tsx @@ -0,0 +1,346 @@ +// import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +// import { yupResolver } from "@hookform/resolvers/yup"; +// import * as yup from "yup"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { CertificateAuthorityType, useCreateCa } from "@app/hooks/api/ca"; +// import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z + .object({ + type: z.enum(["root", "intermediate"]), // move to ref enum of hooks/api + organization: z.string(), + ou: z.string(), + country: z.string(), + province: z.string(), + locality: z.string(), + commonName: z.string() + }) + .required(); + +export type FormData = z.infer; + +type Props = { + popUp: UsePopUpState<["ca"]>; + // handlePopUpOpen: ( + // popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + // data: { + // identityId: string; + // name: string; + // authMethod?: IdentityAuthMethod; + // } + // ) => void; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void; +}; + +const caTypes = [ + { label: "Root", value: CertificateAuthorityType.ROOT }, + { label: "intermediate", value: CertificateAuthorityType.INTERMEDIATE } +]; + +export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { + const { currentWorkspace } = useWorkspace(); + console.log("CaModal currentWorkspace: ", currentWorkspace); + + const { mutateAsync: createMutateAsync } = useCreateCa(); + // const { data: roles } = useGetOrgRoles(orgId); + // const { mutateAsync: createMutateAsync } = useCreateIdentity(); + // const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); + // const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + type: CertificateAuthorityType.ROOT, + organization: "", + ou: "", + country: "", + province: "", + locality: "", + commonName: "" + } + }); + + // useEffect(() => { + // const identity = popUp?.identity?.data as { + // identityId: string; + // name: string; + // role: string; + // customRole: { + // name: string; + // slug: string; + // }; + // }; + + // if (!roles?.length) return; + + // if (identity) { + // reset({ + // name: identity.name, + // role: identity?.customRole?.slug ?? identity.role + // }); + // } else { + // reset({ + // name: "", + // role: roles[0].slug + // }); + // } + // }, [popUp?.identity?.data, roles]); + + const onFormSubmit = async ({ + type, + commonName, + organization, + ou, + country, + locality, + province + }: FormData) => { + try { + console.log("onFormSubmit args: ", { + commonName, + organization, + ou, + country, + locality, + province + }); + + if (!currentWorkspace?.slug) return; + + await createMutateAsync({ + projectSlug: currentWorkspace.slug, + type, + commonName, + organization, + ou, + country, + province, + locality + }); + + // const identity = popUp?.identity?.data as { + // identityId: string; + // name: string; + // role: string; + // }; + + // if (identity) { + // // update + + // await updateMutateAsync({ + // identityId: identity.identityId, + // name, + // role: role || undefined, + // organizationId: orgId + // }); + + // handlePopUpToggle("identity", false); + // } else { + // // create + + // const { + // id: createdId, + // name: createdName, + // authMethod + // } = await createMutateAsync({ + // name, + // role: role || undefined, + // organizationId: orgId + // }); + + // await addMutateAsync({ + // organizationId: orgId, + // identityId: createdId, + // clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + // accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + // accessTokenTTL: 2592000, + // accessTokenMaxTTL: 2592000, + // accessTokenNumUsesLimit: 0 + // }); + + // handlePopUpToggle("identity", false); + // handlePopUpOpen("identityAuthMethod", { + // identityId: createdId, + // name: createdName, + // authMethod + // }); + // } + + createNotification({ + text: `Successfully ${popUp?.ca?.data ? "updated" : "created"} CA`, + type: "success" + }); + + reset(); + } catch (err) { + console.error(err); + const error = err as any; + const text = + error?.response?.data?.message ?? `Failed to ${popUp?.ca?.data ? "update" : "create"} CA`; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( + { + handlePopUpToggle("ca", isOpen); + reset(); + }} + > + +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaSection.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaSection.tsx new file mode 100644 index 000000000..b28c8be6c --- /dev/null +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaSection.tsx @@ -0,0 +1,141 @@ +// import { useState } from "react"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +// import { createNotification } from "@app/components/notifications"; +// import { OrgPermissionCan } from "@app/components/permissions"; +import { + Button + // DeleteActionModal, + // EmailServiceSetupModal, + // UpgradePlanModal +} from "@app/components/v2"; +// import { +// OrgPermissionActions, +// OrgPermissionSubjects, +// useOrganization, +// useSubscription +// } from "@app/context"; +// import { useDeleteOrgMembership } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { CaModal } from "./CaModal"; +import { CaTable } from "./CaTable"; +// import { AddOrgMemberModal } from "./AddOrgMemberModal"; +// import { OrgMembersTable } from "./OrgMembersTable"; + +export const CaSection = () => { + // const { subscription } = useSubscription(); + // const { currentOrg } = useOrganization(); + // const orgId = currentOrg?.id ?? ""; + + // const [completeInviteLink, setCompleteInviteLink] = useState(""); + + const { + popUp, + handlePopUpOpen, + // handlePopUpClose, + handlePopUpToggle + } = usePopUp(["ca"] as const); + + // const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership(); + + // const isMoreUsersNotAllowed = subscription?.memberLimit + // ? subscription.membersUsed >= subscription.memberLimit + // : false; + + // const handleAddMemberModal = () => { + // if (currentOrg?.authEnforced) { + // createNotification({ + // text: "You cannot manage users from Infisical when org-level auth is enforced for your organization", + // type: "error" + // }); + // return; + // } + + // if (isMoreUsersNotAllowed) { + // handlePopUpOpen("upgradePlan", { + // description: "You can add more members if you upgrade your Infisical plan." + // }); + // } else { + // handlePopUpOpen("addMember"); + // } + // }; + + // const onRemoveMemberSubmit = async (orgMembershipId: string) => { + // try { + // await deleteMutateAsync({ + // orgId, + // membershipId: orgMembershipId + // }); + + // createNotification({ + // text: "Successfully removed user from org", + // type: "success" + // }); + // } catch (err) { + // console.error(err); + // createNotification({ + // text: "Failed to remove user from the organization", + // type: "error" + // }); + // } + + // handlePopUpClose("removeMember"); + // }; + + return ( +
+
+

Certificate Authorities

+ {/* + {(isAllowed) => ( */} + + {/* )} */} + {/* */} +
+ + + {/* + + handlePopUpToggle("removeMember", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onRemoveMemberSubmit( + (popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId + ) + } + /> + handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + /> + handlePopUpToggle("setUpEmail", isOpen)} + /> */} +
+ ); +}; diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx new file mode 100644 index 000000000..992c3046e --- /dev/null +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx @@ -0,0 +1,54 @@ +import { faCertificate } from "@fortawesome/free-solid-svg-icons"; + +import { + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useListWorkspaceCas } from "@app/hooks/api"; +import { caTypeToNameMap } from "@app/hooks/api/ca/constants"; + +export const CaTable = () => { + const { currentWorkspace } = useWorkspace(); + const { data, isLoading } = useListWorkspaceCas(currentWorkspace?.slug ?? ""); + return ( +
+ + + + + + + + + + + {isLoading && } + {!isLoading && + data && + data.length > 0 && + data.map((ca) => { + return ( + + + + + + ); + })} + +
SubjectStatusType
{ca.dn}Pending{caTypeToNameMap[ca.type]}
+ {!isLoading && data?.length === 0 && ( + + )} +
+
+ ); +}; diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/index.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/index.tsx new file mode 100644 index 000000000..1f23813bc --- /dev/null +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/index.tsx @@ -0,0 +1 @@ +export { CaSection } from "./CaSection"; diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/index.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/index.tsx new file mode 100644 index 000000000..9e52be028 --- /dev/null +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/index.tsx @@ -0,0 +1 @@ +export { CaTab } from "./CaTab"; diff --git a/frontend/src/views/Project/CertificatesPage/components/index.tsx b/frontend/src/views/Project/CertificatesPage/components/index.tsx new file mode 100644 index 000000000..9e52be028 --- /dev/null +++ b/frontend/src/views/Project/CertificatesPage/components/index.tsx @@ -0,0 +1 @@ +export { CaTab } from "./CaTab"; diff --git a/frontend/src/views/Project/CertificatesPage/index.tsx b/frontend/src/views/Project/CertificatesPage/index.tsx new file mode 100644 index 000000000..a72556644 --- /dev/null +++ b/frontend/src/views/Project/CertificatesPage/index.tsx @@ -0,0 +1 @@ +export { CertificatesPage } from "./CertificatesPage";