Preliminary scaffolding for CA structures

This commit is contained in:
Tuan Dang
2024-05-22 21:33:06 -07:00
parent 303edadb1e
commit 26ea949a4e
41 changed files with 1656 additions and 8 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;

View File

@@ -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<TUsers, TUsersInsert, TUsersUpdate>;
[TableName.Groups]: Knex.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
[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,

View File

@@ -0,0 +1,58 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
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<void> {
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);
}

View File

@@ -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<typeof CertificateAuthoritiesSchema>;
export type TCertificateAuthoritiesInsert = Omit<z.input<typeof CertificateAuthoritiesSchema>, TImmutableDBKeys>;
export type TCertificateAuthoritiesUpdate = Partial<
Omit<z.input<typeof CertificateAuthoritiesSchema>, TImmutableDBKeys>
>;

View File

@@ -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<typeof CertificateAuthorityCertsSchema>;
export type TCertificateAuthorityCertsInsert = Omit<z.input<typeof CertificateAuthorityCertsSchema>, TImmutableDBKeys>;
export type TCertificateAuthorityCertsUpdate = Partial<
Omit<z.input<typeof CertificateAuthorityCertsSchema>, TImmutableDBKeys>
>;

View File

@@ -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<typeof CertificateAuthoritySkSchema>;
export type TCertificateAuthoritySkInsert = Omit<z.input<typeof CertificateAuthoritySkSchema>, TImmutableDBKeys>;
export type TCertificateAuthoritySkUpdate = Partial<
Omit<z.input<typeof CertificateAuthoritySkSchema>, TImmutableDBKeys>
>;

View File

@@ -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";

View File

@@ -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",

View File

@@ -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,

View File

@@ -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
};

View File

@@ -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" });

View File

@@ -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 };
}
});
};

View File

@@ -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<typeof certificateAuthorityCertDALFactory>;
export const certificateAuthorityCertDALFactory = (db: TDbClient) => {
const caCertOrm = ormify(db, TableName.CertificateAuthorityCert);
return caCertOrm;
};

View File

@@ -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<typeof certificateAuthorityDALFactory>;
export const certificateAuthorityDALFactory = (db: TDbClient) => {
const caOrm = ormify(db, TableName.CertificateAuthority);
return caOrm;
};

View File

@@ -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<typeof certificateAuthorityServiceFactory>;
type DNParts = {
commonName?: string;
organization?: string;
ou?: string;
country?: string;
province?: string;
locality?: string;
};
function createDistinguishedName(parts: DNParts) {
const dnParts = [];
if (parts.country) dnParts.push(`C=${parts.country}`);
if (parts.organization) dnParts.push(`O=${parts.organization}`);
if (parts.ou) dnParts.push(`OU=${parts.ou}`);
if (parts.province) dnParts.push(`ST=${parts.province}`);
if (parts.commonName) dnParts.push(`CN=${parts.commonName}`);
if (parts.locality) dnParts.push(`L=${parts.locality}`);
return dnParts.join(", ");
}
export const certificateAuthorityServiceFactory = ({
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySkDAL,
projectDAL
}: 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
};
};

View File

@@ -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<typeof certificateAuthoritySkDALFactory>;
export const certificateAuthoritySkDALFactory = (db: TDbClient) => {
const caSkOrm = ormify(db, TableName.CertificateAuthoritySk);
return caSkOrm;
};

View File

@@ -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<TProjectPermission, "projectId">;
export type TGetCaCsrDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCaCertDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIssueCertFromCaDTO = {
caId: string;
csr: string;
notBefore: string;
notAfter: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -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<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -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
};
};

View File

@@ -75,3 +75,7 @@ export type AddUserToWsDTO = {
userPublicKey: string;
}[];
};
export type TListProjectCasDTO = {
filter: Filter;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -0,0 +1,6 @@
import { CertificateAuthorityType } from "./enums";
export const caTypeToNameMap: { [K in CertificateAuthorityType]: string } = {
[CertificateAuthorityType.ROOT]: "Root",
[CertificateAuthorityType.INTERMEDIATE]: "Intermediate"
};

View File

@@ -0,0 +1,4 @@
export enum CertificateAuthorityType {
ROOT = "root",
INTERMEDIATE = "intermediate"
}

View File

@@ -0,0 +1,2 @@
export { CertificateAuthorityType } from "./enums";
export { useCreateCa } from "./mutations";

View File

@@ -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<TCertificateAuthority, {}, TCreateCaDTO>({
mutationFn: async (body) => {
const {
data: { identity }
} = await apiRequest.post("/api/v1/ca/", body);
return identity;
},
onSuccess: () => {
// queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
}
});
};

View File

@@ -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;
};

View File

@@ -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";

View File

@@ -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";

View File

@@ -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)
});
};

View File

@@ -519,6 +519,18 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem>
</a>
</Link>
<Link href={`/project/${currentWorkspace?.id}/certificates`} passHref>
<a>
<MenuItem
isSelected={
router.asPath === `/project/${currentWorkspace?.id}/certificates`
}
icon="system-outline-90-lock-closed"
>
Certificates
</MenuItem>
</a>
</Link>
<Link href={`/project/${currentWorkspace?.id}/members`} passHref>
<a>
<MenuItem

View File

@@ -0,0 +1,24 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { CertificatesPage } from "@app/views/Project/CertificatesPage";
const Certificates = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Head>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
<div>Test CA page</div>
<CertificatesPage />
</div>
);
};
export default Certificates;
Certificates.requireAuth = true;

View File

@@ -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 (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Certificates</p>
<Tabs defaultValue={TabSections.Ca}>
<TabList>
<Tab value={TabSections.Ca}>Certificate Authorities</Tab>
<Tab value={TabSections.Certificates}>Certificates</Tab>
</TabList>
<TabPanel value={TabSections.Ca}>
<CaTab />
</TabPanel>
<TabPanel value={TabSections.Certificates}>
<div>Certs</div>
{/* <OrgGroupsTab /> */}
</TabPanel>
</Tabs>
</div>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs }
);

View File

@@ -0,0 +1,17 @@
import { motion } from "framer-motion";
import { CaSection } from "./components";
export const CaTab = () => {
return (
<motion.div
key="panel-certificate-authorities"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<CaSection />
</motion.div>
);
};

View File

@@ -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<typeof schema>;
type Props = {
popUp: UsePopUpState<["ca"]>;
// handlePopUpOpen: (
// popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
// data: {
// identityId: string;
// name: string;
// authMethod?: IdentityAuthMethod;
// }
// ) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void;
};
const caTypes = [
{ label: "Root", value: CertificateAuthorityType.ROOT },
{ label: "intermediate", value: CertificateAuthorityType.INTERMEDIATE }
];
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<FormData>({
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 (
<Modal
isOpen={popUp?.ca?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("ca", isOpen);
reset();
}}
>
<ModalContent title={`${popUp?.ca?.data ? "Update" : "Create"} CA`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="type"
defaultValue={CertificateAuthorityType.ROOT}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
// isDisabled={!!identityAuthMethodData?.authMethod}
>
{caTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="organization"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Organization (O)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Acme Corp" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="ou"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Organization Unit (OU)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Engineering" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="country"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Country Name (C)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="United States (US)" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="province"
render={({ field, fieldState: { error } }) => (
<FormControl
label="State or Province Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="California" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="locality"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Locality Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="San Francisco" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="commonName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Common Name (CN)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Example CA" />
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.ca?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("ca", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};

View File

@@ -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<string>("");
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 (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Certificate Authorities</p>
{/* <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => ( */}
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("ca")}
// isDisabled={!isAllowed}
>
Create CA
</Button>
{/* )} */}
{/* </OrgPermissionCan> */}
</div>
<CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaTable />
{/* <OrgMembersTable
handlePopUpOpen={handlePopUpOpen}
setCompleteInviteLink={setCompleteInviteLink}
/>
<AddOrgMemberModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
completeInviteLink={completeInviteLink}
setCompleteInviteLink={setCompleteInviteLink}
/>
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
title={`Are you sure want to remove member with username ${
(popUp?.removeMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveMemberSubmit(
(popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
/> */}
</div>
);
};

View File

@@ -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 (
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Subject</Th>
<Th>Status</Th>
<Th>Type</Th>
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={3} innerKey="project-cas" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map((ca) => {
return (
<Tr className="h-10" key={`ca-${ca.id}`}>
<Td>{ca.dn}</Td>
<Td>Pending</Td>
<Td>{caTypeToNameMap[ca.type]}</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && data?.length === 0 && (
<EmptyState title="No groups have been added to this project" icon={faCertificate} />
)}
</TableContainer>
</div>
);
};

View File

@@ -0,0 +1 @@
export { CaSection } from "./CaSection";

View File

@@ -0,0 +1 @@
export { CaTab } from "./CaTab";

View File

@@ -0,0 +1 @@
export { CaTab } from "./CaTab";

View File

@@ -0,0 +1 @@
export { CertificatesPage } from "./CertificatesPage";