mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-02 16:55:02 +00:00
Compare commits
173 Commits
infisical/
...
docs/updat
Author | SHA1 | Date | |
---|---|---|---|
b12fe66871 | |||
28582d9134 | |||
04908edb5b | |||
e8753a3ce8 | |||
1947989ca5 | |||
c22e616771 | |||
40711ac707 | |||
a47e6910b1 | |||
78c4a591a9 | |||
f6b7717517 | |||
b21a5b6425 | |||
66a5691ffd | |||
6bdf62d453 | |||
652a48b520 | |||
3148c54e18 | |||
bd4cf64fc6 | |||
f4e3d7d576 | |||
8298f9974f | |||
da347e96e1 | |||
5df96234a0 | |||
e78682560c | |||
1602fac5ca | |||
0100bf7032 | |||
e2c49878c6 | |||
e74117b7fd | |||
335aada941 | |||
b949fe06c3 | |||
28e539c481 | |||
5c4c881b60 | |||
8ffb92bfb3 | |||
db9a1726c2 | |||
15986633c7 | |||
c4809bbb54 | |||
6305aab0d1 | |||
456493ff5a | |||
8cfaefcec5 | |||
e39e80a0e7 | |||
8cae92f29e | |||
918911f2e4 | |||
a1aee45eb2 | |||
5fe93dc35a | |||
5e0e7763a3 | |||
f663d1d4a6 | |||
650f6d9585 | |||
7994034639 | |||
48619ed24c | |||
21fb8df39b | |||
f03a7cc249 | |||
f2dcbfa91c | |||
d08510ebe4 | |||
767159bf8f | |||
98457cdb34 | |||
8ed8f1200d | |||
30252c2bcb | |||
9687f33122 | |||
a5282a56c9 | |||
cc3551c417 | |||
9e6fe39609 | |||
2bc91c42a7 | |||
c7ec825830 | |||
5b7f445e33 | |||
7fe53ab00e | |||
90c17820fc | |||
e739b29b3c | |||
1a89f2a479 | |||
78568bffe2 | |||
1407a122b9 | |||
8168b5faf8 | |||
8b9e035bf6 | |||
d36d0784ca | |||
e69354b546 | |||
64bd5ddcc8 | |||
72088634d8 | |||
f3a84f6001 | |||
13672481a8 | |||
058394f892 | |||
c623c615a1 | |||
034a8112b7 | |||
5fc6fd71ce | |||
f91bbe1f31 | |||
1e4ca2f48f | |||
e5bc609a2a | |||
b812761bdd | |||
14362dbe6a | |||
b7b90aea33 | |||
28a3bf0b94 | |||
5712c24370 | |||
4a391c7ac2 | |||
95ef113aea | |||
07bf65b1c3 | |||
12071e4816 | |||
a40d4efa39 | |||
5b200f42a3 | |||
64f724ed95 | |||
2b21c9d348 | |||
e8d00161eb | |||
0a5a073db1 | |||
0f14685d54 | |||
d5888d5bbb | |||
8ff95aedd5 | |||
2b948a18f3 | |||
f06004370d | |||
df75b3b8d3 | |||
04989372b1 | |||
77de085ffc | |||
c985690e9a | |||
bb2a70b986 | |||
3ac3710273 | |||
92cb034155 | |||
2493bbbc97 | |||
77b42836e7 | |||
949615606f | |||
44aa743d56 | |||
fefb71dd86 | |||
1748052cb0 | |||
c01a98ccf1 | |||
9ea9f90928 | |||
6319f53802 | |||
eb31318d39 | |||
7f6dcd3afa | |||
2b4a6ad907 | |||
ba8fcb6891 | |||
c2df8cf869 | |||
e383872486 | |||
490c589a44 | |||
b358f2dbb7 | |||
10ed6f6b52 | |||
e0f1311f6d | |||
1cff92d000 | |||
db8f43385d | |||
41b45c212d | |||
ef9269fe10 | |||
4d95052896 | |||
260679b01d | |||
56b7328231 | |||
edefa7698c | |||
60ea4bb579 | |||
04d553f052 | |||
6d10afc9d2 | |||
c2949964b3 | |||
6faad102e2 | |||
8bfd3913da | |||
d1e5ae2d85 | |||
e5555ffd3f | |||
6b95bb0ceb | |||
b0e25a8bd1 | |||
d483e70748 | |||
4b94848a79 | |||
879b12002c | |||
bc93db8603 | |||
c43a87947f | |||
9e1d38a27b | |||
78d5bc823d | |||
e8d424bbb0 | |||
f0c52cc8da | |||
e58dbe853e | |||
f493a617b1 | |||
32a3e1d200 | |||
7447d17e94 | |||
4efa4ad8df | |||
c6e56f0380 | |||
d61216ed62 | |||
580de0565b | |||
bbfd4a44c3 | |||
01e13ca7bd | |||
f5fdd1a266 | |||
bda74ce13e | |||
6a973be6f3 | |||
7f836ed9bc | |||
4d847ab2cb | |||
80cecbb937 | |||
8b6c97d5bc | |||
5641d334cd |
@ -15,8 +15,8 @@ import { mockSmtpServer } from "./mocks/smtp";
|
||||
import { initDbConnection } from "@app/db";
|
||||
import { queueServiceFactory } from "@app/queue";
|
||||
import { keyStoreFactory } from "@app/keystore/keystore";
|
||||
import { Redis } from "ioredis";
|
||||
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
||||
import { buildRedisFromConfig } from "@app/lib/config/redis";
|
||||
|
||||
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
|
||||
export default {
|
||||
@ -30,7 +30,7 @@ export default {
|
||||
dbRootCert: envConfig.DB_ROOT_CERT
|
||||
});
|
||||
|
||||
const redis = new Redis(envConfig.REDIS_URL);
|
||||
const redis = buildRedisFromConfig(envConfig);
|
||||
await redis.flushdb("SYNC");
|
||||
|
||||
try {
|
||||
@ -55,8 +55,8 @@ export default {
|
||||
});
|
||||
|
||||
const smtp = mockSmtpServer();
|
||||
const queue = queueServiceFactory(envConfig.REDIS_URL, { dbConnectionUrl: envConfig.DB_CONNECTION_URI });
|
||||
const keyStore = keyStoreFactory(envConfig.REDIS_URL);
|
||||
const queue = queueServiceFactory(envConfig, { dbConnectionUrl: envConfig.DB_CONNECTION_URI });
|
||||
const keyStore = keyStoreFactory(envConfig);
|
||||
|
||||
const hsmModule = initializeHsmModule(envConfig);
|
||||
hsmModule.initialize();
|
||||
|
1877
backend/package-lock.json
generated
1877
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -131,6 +131,7 @@
|
||||
"@aws-sdk/client-elasticache": "^3.637.0",
|
||||
"@aws-sdk/client-iam": "^3.525.0",
|
||||
"@aws-sdk/client-kms": "^3.609.0",
|
||||
"@aws-sdk/client-route-53": "^3.810.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||
"@aws-sdk/client-sts": "^3.600.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
@ -174,6 +175,7 @@
|
||||
"@slack/oauth": "^3.0.2",
|
||||
"@slack/web-api": "^7.8.0",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"acme-client": "^5.4.0",
|
||||
"ajv": "^8.12.0",
|
||||
"argon2": "^0.31.2",
|
||||
"aws-sdk": "^2.1553.0",
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -53,6 +53,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
|
||||
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
||||
@ -269,6 +270,7 @@ declare module "fastify" {
|
||||
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||
githubOrgSync: TGithubOrgSyncServiceFactory;
|
||||
internalCertificateAuthority: TInternalCertificateAuthorityServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
16
backend/src/@types/knex.d.ts
vendored
16
backend/src/@types/knex.d.ts
vendored
@ -68,6 +68,9 @@ import {
|
||||
TDynamicSecrets,
|
||||
TDynamicSecretsInsert,
|
||||
TDynamicSecretsUpdate,
|
||||
TExternalCertificateAuthorities,
|
||||
TExternalCertificateAuthoritiesInsert,
|
||||
TExternalCertificateAuthoritiesUpdate,
|
||||
TExternalGroupOrgRoleMappings,
|
||||
TExternalGroupOrgRoleMappingsInsert,
|
||||
TExternalGroupOrgRoleMappingsUpdate,
|
||||
@ -155,6 +158,9 @@ import {
|
||||
TIntegrations,
|
||||
TIntegrationsInsert,
|
||||
TIntegrationsUpdate,
|
||||
TInternalCertificateAuthorities,
|
||||
TInternalCertificateAuthoritiesInsert,
|
||||
TInternalCertificateAuthoritiesUpdate,
|
||||
TInternalKms,
|
||||
TInternalKmsInsert,
|
||||
TInternalKmsUpdate,
|
||||
@ -538,6 +544,16 @@ declare module "knex/types/tables" {
|
||||
TCertificateAuthorityCrlInsert,
|
||||
TCertificateAuthorityCrlUpdate
|
||||
>;
|
||||
[TableName.InternalCertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||
TInternalCertificateAuthorities,
|
||||
TInternalCertificateAuthoritiesInsert,
|
||||
TInternalCertificateAuthoritiesUpdate
|
||||
>;
|
||||
[TableName.ExternalCertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||
TExternalCertificateAuthorities,
|
||||
TExternalCertificateAuthoritiesInsert,
|
||||
TExternalCertificateAuthoritiesUpdate
|
||||
>;
|
||||
[TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
|
||||
[TableName.CertificateTemplate]: KnexOriginal.CompositeTableType<
|
||||
TCertificateTemplates,
|
||||
|
@ -0,0 +1,44 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||
const hasProjectIdColumn = await knex.schema.hasColumn(TableName.Certificate, "projectId");
|
||||
if (!hasProjectIdColumn) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.string("projectId", 36).nullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE "${TableName.Certificate}" cert
|
||||
SET "projectId" = ca."projectId"
|
||||
FROM "${TableName.CertificateAuthority}" ca
|
||||
WHERE cert."caId" = ca.id
|
||||
`);
|
||||
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.string("projectId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.uuid("caId").nullable().alter();
|
||||
t.uuid("caCertId").nullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||
if (await knex.schema.hasColumn(TableName.Certificate, "projectId")) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.dropForeign("projectId");
|
||||
t.dropColumn("projectId");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Altering back to notNullable for caId and caCertId will fail
|
||||
}
|
205
backend/src/db/migrations/20250521110635_add-external-ca-pki.ts
Normal file
205
backend/src/db/migrations/20250521110635_add-external-ca-pki.ts
Normal file
@ -0,0 +1,205 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasCATable = await knex.schema.hasTable(TableName.CertificateAuthority);
|
||||
const hasExternalCATable = await knex.schema.hasTable(TableName.ExternalCertificateAuthority);
|
||||
const hasInternalCATable = await knex.schema.hasTable(TableName.InternalCertificateAuthority);
|
||||
|
||||
if (hasCATable && !hasInternalCATable) {
|
||||
await knex.schema.createTableLike(TableName.InternalCertificateAuthority, TableName.CertificateAuthority, (t) => {
|
||||
t.uuid("caId").nullable();
|
||||
});
|
||||
|
||||
// @ts-expect-error intentional: migration
|
||||
await knex(TableName.InternalCertificateAuthority).insert(knex(TableName.CertificateAuthority).select("*"));
|
||||
await knex(TableName.InternalCertificateAuthority).update("caId", knex.ref("id"));
|
||||
|
||||
await knex.schema.alterTable(TableName.InternalCertificateAuthority, (t) => {
|
||||
t.dropColumn("projectId");
|
||||
t.dropColumn("requireTemplateForIssuance");
|
||||
t.dropColumn("createdAt");
|
||||
t.dropColumn("updatedAt");
|
||||
t.dropColumn("status");
|
||||
t.uuid("parentCaId")
|
||||
.nullable()
|
||||
.references("id")
|
||||
.inTable(TableName.CertificateAuthority)
|
||||
.onDelete("CASCADE")
|
||||
.alter();
|
||||
t.uuid("activeCaCertId").nullable().references("id").inTable(TableName.CertificateAuthorityCert).alter();
|
||||
t.uuid("caId").notNullable().references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE").alter();
|
||||
});
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.renameColumn("requireTemplateForIssuance", "enableDirectIssuance");
|
||||
t.string("name").nullable();
|
||||
});
|
||||
|
||||
// prefill name for existing internal CAs and flip enableDirectIssuance
|
||||
const cas = await knex(TableName.CertificateAuthority).select("id", "friendlyName", "enableDirectIssuance");
|
||||
await Promise.all(
|
||||
cas.map((ca) => {
|
||||
const slugifiedName = ca.friendlyName
|
||||
? slugify(`${ca.friendlyName.slice(0, 16)}-${alphaNumericNanoId(8)}`)
|
||||
: slugify(alphaNumericNanoId(12));
|
||||
|
||||
return knex(TableName.CertificateAuthority)
|
||||
.where({ id: ca.id })
|
||||
.update({ name: slugifiedName, enableDirectIssuance: !ca.enableDirectIssuance });
|
||||
})
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.dropColumn("parentCaId");
|
||||
t.dropColumn("type");
|
||||
t.dropColumn("friendlyName");
|
||||
t.dropColumn("organization");
|
||||
t.dropColumn("ou");
|
||||
t.dropColumn("country");
|
||||
t.dropColumn("province");
|
||||
t.dropColumn("locality");
|
||||
t.dropColumn("commonName");
|
||||
t.dropColumn("dn");
|
||||
t.dropColumn("serialNumber");
|
||||
t.dropColumn("maxPathLength");
|
||||
t.dropColumn("keyAlgorithm");
|
||||
t.dropColumn("notBefore");
|
||||
t.dropColumn("notAfter");
|
||||
t.dropColumn("activeCaCertId");
|
||||
t.boolean("enableDirectIssuance").notNullable().defaultTo(true).alter();
|
||||
t.string("name").notNullable().alter();
|
||||
t.unique(["name", "projectId"]);
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasExternalCATable) {
|
||||
await knex.schema.createTable(TableName.ExternalCertificateAuthority, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("type").notNullable();
|
||||
t.uuid("appConnectionId").nullable();
|
||||
t.foreign("appConnectionId").references("id").inTable(TableName.AppConnection);
|
||||
t.uuid("dnsAppConnectionId").nullable();
|
||||
t.foreign("dnsAppConnectionId").references("id").inTable(TableName.AppConnection);
|
||||
t.uuid("caId").notNullable().references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||
t.binary("credentials");
|
||||
t.json("configuration");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.PkiSubscriber)) {
|
||||
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
|
||||
t.string("ttl").nullable().alter();
|
||||
|
||||
t.boolean("enableAutoRenewal").notNullable().defaultTo(false);
|
||||
t.integer("autoRenewalPeriodInDays");
|
||||
t.datetime("lastAutoRenewAt");
|
||||
|
||||
t.string("lastOperationStatus");
|
||||
t.text("lastOperationMessage");
|
||||
t.dateTime("lastOperationAt");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasCATable = await knex.schema.hasTable(TableName.CertificateAuthority);
|
||||
const hasExternalCATable = await knex.schema.hasTable(TableName.ExternalCertificateAuthority);
|
||||
const hasInternalCATable = await knex.schema.hasTable(TableName.InternalCertificateAuthority);
|
||||
|
||||
if (hasCATable && hasInternalCATable) {
|
||||
// First add all columns as nullable
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.uuid("parentCaId").nullable().references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||
t.string("type").nullable();
|
||||
t.string("friendlyName").nullable();
|
||||
t.string("organization").nullable();
|
||||
t.string("ou").nullable();
|
||||
t.string("country").nullable();
|
||||
t.string("province").nullable();
|
||||
t.string("locality").nullable();
|
||||
t.string("commonName").nullable();
|
||||
t.string("dn").nullable();
|
||||
t.string("serialNumber").nullable().unique();
|
||||
t.integer("maxPathLength").nullable();
|
||||
t.string("keyAlgorithm").nullable();
|
||||
t.timestamp("notBefore").nullable();
|
||||
t.timestamp("notAfter").nullable();
|
||||
t.uuid("activeCaCertId").nullable().references("id").inTable(TableName.CertificateAuthorityCert);
|
||||
t.renameColumn("enableDirectIssuance", "requireTemplateForIssuance");
|
||||
t.dropColumn("name");
|
||||
});
|
||||
|
||||
// flip requireTemplateForIssuance for existing internal CAs
|
||||
const cas = await knex(TableName.CertificateAuthority).select("id", "requireTemplateForIssuance");
|
||||
await Promise.all(
|
||||
cas.map((ca) => {
|
||||
return (
|
||||
knex(TableName.CertificateAuthority)
|
||||
.where({ id: ca.id })
|
||||
// @ts-expect-error intentional: migration
|
||||
.update({ requireTemplateForIssuance: !ca.requireTemplateForIssuance })
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE ${TableName.CertificateAuthority} ca
|
||||
SET
|
||||
type = ica.type,
|
||||
"friendlyName" = ica."friendlyName",
|
||||
organization = ica.organization,
|
||||
ou = ica.ou,
|
||||
country = ica.country,
|
||||
province = ica.province,
|
||||
locality = ica.locality,
|
||||
"commonName" = ica."commonName",
|
||||
dn = ica.dn,
|
||||
"parentCaId" = ica."parentCaId",
|
||||
"serialNumber" = ica."serialNumber",
|
||||
"maxPathLength" = ica."maxPathLength",
|
||||
"keyAlgorithm" = ica."keyAlgorithm",
|
||||
"notBefore" = ica."notBefore",
|
||||
"notAfter" = ica."notAfter",
|
||||
"activeCaCertId" = ica."activeCaCertId"
|
||||
FROM ${TableName.InternalCertificateAuthority} ica
|
||||
WHERE ca.id = ica."caId"
|
||||
`);
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.string("type").notNullable().alter();
|
||||
t.string("friendlyName").notNullable().alter();
|
||||
t.string("organization").notNullable().alter();
|
||||
t.string("ou").notNullable().alter();
|
||||
t.string("country").notNullable().alter();
|
||||
t.string("province").notNullable().alter();
|
||||
t.string("locality").notNullable().alter();
|
||||
t.string("commonName").notNullable().alter();
|
||||
t.string("dn").notNullable().alter();
|
||||
t.string("keyAlgorithm").notNullable().alter();
|
||||
t.boolean("requireTemplateForIssuance").notNullable().defaultTo(false).alter();
|
||||
});
|
||||
|
||||
await knex.schema.dropTable(TableName.InternalCertificateAuthority);
|
||||
}
|
||||
|
||||
if (hasExternalCATable) {
|
||||
await knex.schema.dropTable(TableName.ExternalCertificateAuthority);
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.PkiSubscriber)) {
|
||||
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
|
||||
t.dropColumn("enableAutoRenewal");
|
||||
t.dropColumn("autoRenewalPeriodInDays");
|
||||
t.dropColumn("lastAutoRenewAt");
|
||||
|
||||
t.dropColumn("lastOperationStatus");
|
||||
t.dropColumn("lastOperationMessage");
|
||||
t.dropColumn("lastOperationAt");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityAccessToken, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityAwsAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityOidcAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityAzureAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityAzureAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityGcpAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityGcpAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityJwtAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityJwtAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityLdapAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityOciAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityOciAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityTokenAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityTokenAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.IdentityAccessToken, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityAwsAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityOidcAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityAzureAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityAzureAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityGcpAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityGcpAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityJwtAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityJwtAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityLdapAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityOciAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityOciAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityTokenAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityTokenAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
const hasEncryptedSalt = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSalt");
|
||||
|
||||
if (hasEncryptedSalt) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.dropColumn("encryptedSalt");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
const hasEncryptedSalt = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSalt");
|
||||
|
||||
if (!hasEncryptedSalt) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.binary("encryptedSalt").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -11,25 +11,10 @@ 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(),
|
||||
enableDirectIssuance: z.boolean().default(true),
|
||||
status: z.string(),
|
||||
friendlyName: z.string(),
|
||||
organization: z.string(),
|
||||
ou: z.string(),
|
||||
country: z.string(),
|
||||
province: z.string(),
|
||||
locality: z.string(),
|
||||
commonName: z.string(),
|
||||
dn: z.string(),
|
||||
serialNumber: z.string().nullable().optional(),
|
||||
maxPathLength: z.number().nullable().optional(),
|
||||
keyAlgorithm: z.string(),
|
||||
notBefore: z.date().nullable().optional(),
|
||||
notAfter: z.date().nullable().optional(),
|
||||
activeCaCertId: z.string().uuid().nullable().optional(),
|
||||
requireTemplateForIssuance: z.boolean().default(false)
|
||||
name: z.string()
|
||||
});
|
||||
|
||||
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
|
||||
|
@ -11,7 +11,7 @@ export const CertificatesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
caId: z.string().uuid(),
|
||||
caId: z.string().uuid().nullable().optional(),
|
||||
status: z.string(),
|
||||
serialNumber: z.string(),
|
||||
friendlyName: z.string(),
|
||||
@ -21,11 +21,12 @@ export const CertificatesSchema = z.object({
|
||||
revokedAt: z.date().nullable().optional(),
|
||||
revocationReason: z.number().nullable().optional(),
|
||||
altNames: z.string().nullable().optional(),
|
||||
caCertId: z.string().uuid(),
|
||||
caCertId: z.string().uuid().nullable().optional(),
|
||||
certificateTemplateId: z.string().uuid().nullable().optional(),
|
||||
keyUsages: z.string().array().nullable().optional(),
|
||||
extendedKeyUsages: z.string().array().nullable().optional(),
|
||||
pkiSubscriberId: z.string().uuid().nullable().optional()
|
||||
pkiSubscriberId: z.string().uuid().nullable().optional(),
|
||||
projectId: z.string()
|
||||
});
|
||||
|
||||
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||
|
29
backend/src/db/schemas/external-certificate-authorities.ts
Normal file
29
backend/src/db/schemas/external-certificate-authorities.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// 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 { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ExternalCertificateAuthoritiesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
type: z.string(),
|
||||
appConnectionId: z.string().uuid().nullable().optional(),
|
||||
dnsAppConnectionId: z.string().uuid().nullable().optional(),
|
||||
caId: z.string().uuid(),
|
||||
credentials: zodBuffer.nullable().optional(),
|
||||
configuration: z.unknown().nullable().optional()
|
||||
});
|
||||
|
||||
export type TExternalCertificateAuthorities = z.infer<typeof ExternalCertificateAuthoritiesSchema>;
|
||||
export type TExternalCertificateAuthoritiesInsert = Omit<
|
||||
z.input<typeof ExternalCertificateAuthoritiesSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TExternalCertificateAuthoritiesUpdate = Partial<
|
||||
Omit<z.input<typeof ExternalCertificateAuthoritiesSchema>, TImmutableDBKeys>
|
||||
>;
|
@ -21,7 +21,8 @@ export const IdentityAccessTokensSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
name: z.string().nullable().optional(),
|
||||
authMethod: z.string()
|
||||
authMethod: z.string(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityAccessTokens = z.infer<typeof IdentityAccessTokensSchema>;
|
||||
|
@ -19,7 +19,8 @@ export const IdentityAwsAuthsSchema = z.object({
|
||||
type: z.string(),
|
||||
stsEndpoint: z.string(),
|
||||
allowedPrincipalArns: z.string(),
|
||||
allowedAccountIds: z.string()
|
||||
allowedAccountIds: z.string(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityAwsAuths = z.infer<typeof IdentityAwsAuthsSchema>;
|
||||
|
@ -18,7 +18,8 @@ export const IdentityAzureAuthsSchema = z.object({
|
||||
identityId: z.string().uuid(),
|
||||
tenantId: z.string(),
|
||||
resource: z.string(),
|
||||
allowedServicePrincipalIds: z.string()
|
||||
allowedServicePrincipalIds: z.string(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityAzureAuths = z.infer<typeof IdentityAzureAuthsSchema>;
|
||||
|
@ -19,7 +19,8 @@ export const IdentityGcpAuthsSchema = z.object({
|
||||
type: z.string(),
|
||||
allowedServiceAccounts: z.string().nullable().optional(),
|
||||
allowedProjects: z.string().nullable().optional(),
|
||||
allowedZones: z.string().nullable().optional()
|
||||
allowedZones: z.string().nullable().optional(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityGcpAuths = z.infer<typeof IdentityGcpAuthsSchema>;
|
||||
|
@ -25,7 +25,8 @@ export const IdentityJwtAuthsSchema = z.object({
|
||||
boundClaims: z.unknown(),
|
||||
boundSubject: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityJwtAuths = z.infer<typeof IdentityJwtAuthsSchema>;
|
||||
|
@ -30,7 +30,8 @@ export const IdentityKubernetesAuthsSchema = z.object({
|
||||
allowedAudience: z.string(),
|
||||
encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(),
|
||||
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(),
|
||||
gatewayId: z.string().uuid().nullable().optional()
|
||||
gatewayId: z.string().uuid().nullable().optional(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>;
|
||||
|
@ -24,7 +24,8 @@ export const IdentityLdapAuthsSchema = z.object({
|
||||
searchFilter: z.string(),
|
||||
allowedFields: z.unknown().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityLdapAuths = z.infer<typeof IdentityLdapAuthsSchema>;
|
||||
|
@ -18,7 +18,8 @@ export const IdentityOciAuthsSchema = z.object({
|
||||
identityId: z.string().uuid(),
|
||||
type: z.string(),
|
||||
tenancyOcid: z.string(),
|
||||
allowedUsernames: z.string().nullable().optional()
|
||||
allowedUsernames: z.string().nullable().optional(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityOciAuths = z.infer<typeof IdentityOciAuthsSchema>;
|
||||
|
@ -27,7 +27,8 @@ export const IdentityOidcAuthsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
encryptedCaCertificate: zodBuffer.nullable().optional(),
|
||||
claimMetadataMapping: z.unknown().nullable().optional()
|
||||
claimMetadataMapping: z.unknown().nullable().optional(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityOidcAuths = z.infer<typeof IdentityOidcAuthsSchema>;
|
||||
|
@ -15,7 +15,8 @@ export const IdentityTokenAuthsSchema = z.object({
|
||||
accessTokenTrustedIps: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
identityId: z.string().uuid()
|
||||
identityId: z.string().uuid(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityTokenAuths = z.infer<typeof IdentityTokenAuthsSchema>;
|
||||
|
@ -17,7 +17,8 @@ export const IdentityUniversalAuthsSchema = z.object({
|
||||
accessTokenTrustedIps: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
identityId: z.string().uuid()
|
||||
identityId: z.string().uuid(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityUniversalAuths = z.infer<typeof IdentityUniversalAuthsSchema>;
|
||||
|
@ -20,6 +20,7 @@ export * from "./certificate-templates";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
export * from "./external-certificate-authorities";
|
||||
export * from "./external-group-org-role-mappings";
|
||||
export * from "./external-kms";
|
||||
export * from "./gateways";
|
||||
@ -49,6 +50,7 @@ export * from "./identity-universal-auths";
|
||||
export * from "./incident-contacts";
|
||||
export * from "./integration-auths";
|
||||
export * from "./integrations";
|
||||
export * from "./internal-certificate-authorities";
|
||||
export * from "./internal-kms";
|
||||
export * from "./kmip-client-certificates";
|
||||
export * from "./kmip-clients";
|
||||
|
38
backend/src/db/schemas/internal-certificate-authorities.ts
Normal file
38
backend/src/db/schemas/internal-certificate-authorities.ts
Normal file
@ -0,0 +1,38 @@
|
||||
// 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 InternalCertificateAuthoritiesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
parentCaId: z.string().uuid().nullable().optional(),
|
||||
type: z.string(),
|
||||
friendlyName: z.string(),
|
||||
organization: z.string(),
|
||||
ou: z.string(),
|
||||
country: z.string(),
|
||||
province: z.string(),
|
||||
locality: z.string(),
|
||||
commonName: z.string(),
|
||||
dn: z.string(),
|
||||
serialNumber: z.string().nullable().optional(),
|
||||
maxPathLength: z.number().nullable().optional(),
|
||||
keyAlgorithm: z.string(),
|
||||
notBefore: z.date().nullable().optional(),
|
||||
notAfter: z.date().nullable().optional(),
|
||||
activeCaCertId: z.string().uuid().nullable().optional(),
|
||||
caId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TInternalCertificateAuthorities = z.infer<typeof InternalCertificateAuthoritiesSchema>;
|
||||
export type TInternalCertificateAuthoritiesInsert = Omit<
|
||||
z.input<typeof InternalCertificateAuthoritiesSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TInternalCertificateAuthoritiesUpdate = Partial<
|
||||
Omit<z.input<typeof InternalCertificateAuthoritiesSchema>, TImmutableDBKeys>
|
||||
>;
|
@ -13,6 +13,8 @@ export enum TableName {
|
||||
SshCertificate = "ssh_certificates",
|
||||
SshCertificateBody = "ssh_certificate_bodies",
|
||||
CertificateAuthority = "certificate_authorities",
|
||||
ExternalCertificateAuthority = "external_certificate_authorities",
|
||||
InternalCertificateAuthority = "internal_certificate_authorities",
|
||||
CertificateTemplateEstConfig = "certificate_template_est_configs",
|
||||
CertificateAuthorityCert = "certificate_authority_certs",
|
||||
CertificateAuthoritySecret = "certificate_authority_secret",
|
||||
|
@ -16,10 +16,16 @@ export const PkiSubscribersSchema = z.object({
|
||||
name: z.string(),
|
||||
commonName: z.string(),
|
||||
subjectAlternativeNames: z.string().array(),
|
||||
ttl: z.string(),
|
||||
ttl: z.string().nullable().optional(),
|
||||
keyUsages: z.string().array(),
|
||||
extendedKeyUsages: z.string().array(),
|
||||
status: z.string()
|
||||
status: z.string(),
|
||||
enableAutoRenewal: z.boolean().default(false),
|
||||
autoRenewalPeriodInDays: z.number().nullable().optional(),
|
||||
lastAutoRenewAt: z.date().nullable().optional(),
|
||||
lastOperationStatus: z.string().nullable().optional(),
|
||||
lastOperationMessage: z.string().nullable().optional(),
|
||||
lastOperationAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TPkiSubscribers = z.infer<typeof PkiSubscribersSchema>;
|
||||
|
@ -28,7 +28,6 @@ export const SecretSharingSchema = z.object({
|
||||
encryptedSecret: zodBuffer.nullable().optional(),
|
||||
identifier: z.string().nullable().optional(),
|
||||
type: z.string().default("share"),
|
||||
encryptedSalt: zodBuffer.nullable().optional(),
|
||||
authorizedEmails: z.unknown().nullable().optional()
|
||||
});
|
||||
|
||||
|
@ -47,7 +47,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
200: z.object({ plan: z.any() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const plan = await server.services.license.getOrgPlan({
|
||||
actorId: req.permission.id,
|
||||
|
@ -21,7 +21,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
||||
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
|
||||
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||
@ -232,6 +232,7 @@ export enum EventType {
|
||||
REMOVE_HOST_FROM_SSH_HOST_GROUP = "remove-host-from-ssh-host-group",
|
||||
CREATE_CA = "create-certificate-authority",
|
||||
GET_CA = "get-certificate-authority",
|
||||
GET_CAS = "get-certificate-authorities",
|
||||
UPDATE_CA = "update-certificate-authority",
|
||||
DELETE_CA = "delete-certificate-authority",
|
||||
RENEW_CA = "renew-certificate-authority",
|
||||
@ -242,6 +243,7 @@ export enum EventType {
|
||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||
GET_CA_CRLS = "get-certificate-authority-crls",
|
||||
ISSUE_CERT = "issue-cert",
|
||||
IMPORT_CERT = "import-cert",
|
||||
SIGN_CERT = "sign-cert",
|
||||
GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates",
|
||||
GET_CERT = "get-cert",
|
||||
@ -267,7 +269,9 @@ export enum EventType {
|
||||
GET_PKI_SUBSCRIBER = "get-pki-subscriber",
|
||||
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
|
||||
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
|
||||
AUTOMATED_RENEW_SUBSCRIBER_CERT = "automated-renew-subscriber-cert",
|
||||
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
|
||||
GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle",
|
||||
CREATE_KMS = "create-kms",
|
||||
UPDATE_KMS = "update-kms",
|
||||
DELETE_KMS = "delete-kms",
|
||||
@ -1778,7 +1782,8 @@ interface CreateCa {
|
||||
type: EventType.CREATE_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
name: string;
|
||||
dn?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1786,7 +1791,15 @@ interface GetCa {
|
||||
type: EventType.GET_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
name: string;
|
||||
dn?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCAs {
|
||||
type: EventType.GET_CAS;
|
||||
metadata: {
|
||||
caIds: string[];
|
||||
};
|
||||
}
|
||||
|
||||
@ -1794,7 +1807,8 @@ interface UpdateCa {
|
||||
type: EventType.UPDATE_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
name: string;
|
||||
dn?: string;
|
||||
status: CaStatus;
|
||||
};
|
||||
}
|
||||
@ -1803,7 +1817,8 @@ interface DeleteCa {
|
||||
type: EventType.DELETE_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
name: string;
|
||||
dn?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1873,6 +1888,15 @@ interface IssueCert {
|
||||
};
|
||||
}
|
||||
|
||||
interface ImportCert {
|
||||
type: EventType.IMPORT_CERT;
|
||||
metadata: {
|
||||
certId: string;
|
||||
cn: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SignCert {
|
||||
type: EventType.SIGN_CERT;
|
||||
metadata: {
|
||||
@ -2040,7 +2064,7 @@ interface CreatePkiSubscriber {
|
||||
caId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
ttl: string;
|
||||
ttl?: string;
|
||||
subjectAlternativeNames: string[];
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
@ -2082,7 +2106,15 @@ interface IssuePkiSubscriberCert {
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
name: string;
|
||||
serialNumber: string;
|
||||
serialNumber?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AutomatedRenewPkiSubscriberCert {
|
||||
type: EventType.AUTOMATED_RENEW_SUBSCRIBER_CERT;
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -2104,6 +2136,16 @@ interface ListPkiSubscriberCerts {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSubscriberActiveCertBundle {
|
||||
type: EventType.GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE;
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
name: string;
|
||||
certId: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateKmsEvent {
|
||||
type: EventType.CREATE_KMS;
|
||||
metadata: {
|
||||
@ -3088,6 +3130,7 @@ export type Event =
|
||||
| IssueSshHostHostCert
|
||||
| CreateCa
|
||||
| GetCa
|
||||
| GetCAs
|
||||
| UpdateCa
|
||||
| DeleteCa
|
||||
| RenewCa
|
||||
@ -3098,6 +3141,7 @@ export type Event =
|
||||
| ImportCaCert
|
||||
| GetCaCrls
|
||||
| IssueCert
|
||||
| ImportCert
|
||||
| SignCert
|
||||
| GetCaCertificateTemplates
|
||||
| GetCert
|
||||
@ -3123,7 +3167,9 @@ export type Event =
|
||||
| GetPkiSubscriber
|
||||
| IssuePkiSubscriberCert
|
||||
| SignPkiSubscriberCert
|
||||
| AutomatedRenewPkiSubscriberCert
|
||||
| ListPkiSubscriberCerts
|
||||
| GetSubscriberActiveCertBundle
|
||||
| CreateKmsEvent
|
||||
| UpdateKmsEvent
|
||||
| DeleteKmsEvent
|
||||
|
@ -7,6 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { expandInternalCa } from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
@ -14,7 +15,7 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns
|
||||
import { TGetCaCrlsDTO, TGetCrlById } from "./certificate-authority-crl-types";
|
||||
|
||||
type TCertificateAuthorityCrlServiceFactoryDep = {
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "find" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
@ -37,7 +38,8 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
const caCrl = await certificateAuthorityCrlDAL.findById(crlId);
|
||||
if (!caCrl) throw new NotFoundError({ message: `CRL with ID '${crlId}' not found` });
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(caCrl.caId);
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caCrl.caId);
|
||||
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caCrl.caId}' not found` });
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
@ -54,7 +56,7 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
const crl = new x509.X509Crl(decryptedCrl);
|
||||
|
||||
return {
|
||||
ca,
|
||||
ca: expandInternalCa(ca),
|
||||
caCrl,
|
||||
crl: crl.rawData
|
||||
};
|
||||
@ -64,8 +66,8 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
* Returns a list of CRL ids for CA with id [caId]
|
||||
*/
|
||||
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@ -108,7 +110,7 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
);
|
||||
|
||||
return {
|
||||
ca,
|
||||
ca: expandInternalCa(ca),
|
||||
crls: decryptedCrls
|
||||
};
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||
import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@ -16,10 +16,10 @@ import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { convertRawCertsToPkcs7 } from "./certificate-est-fns";
|
||||
|
||||
type TCertificateEstServiceFactoryDep = {
|
||||
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
|
||||
internalCertificateAuthorityService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa">;
|
||||
certificateTemplateService: Pick<TCertificateTemplateServiceFactory, "getEstConfiguration">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
@ -29,7 +29,7 @@ type TCertificateEstServiceFactoryDep = {
|
||||
export type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
|
||||
|
||||
export const certificateEstServiceFactory = ({
|
||||
certificateAuthorityService,
|
||||
internalCertificateAuthorityService,
|
||||
certificateTemplateService,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
@ -127,7 +127,7 @@ export const certificateEstServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId,
|
||||
csr
|
||||
@ -188,7 +188,7 @@ export const certificateEstServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId,
|
||||
csr
|
||||
@ -227,15 +227,15 @@ export const certificateEstServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(certTemplate.caId);
|
||||
if (!ca) {
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certTemplate.caId);
|
||||
if (!ca?.internalCa?.id) {
|
||||
throw new NotFoundError({
|
||||
message: `Certificate Authority with ID '${certTemplate.caId}' not found`
|
||||
message: `Internal Certificate Authority with ID '${certTemplate.caId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caCertId: ca.activeCaCertId as string,
|
||||
caCertId: ca.internalCa.activeCaCertId as string,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
|
@ -6,6 +6,7 @@ import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
import { CassandraProvider } from "./cassandra";
|
||||
import { ElasticSearchProvider } from "./elastic-search";
|
||||
import { KubernetesProvider } from "./kubernetes";
|
||||
import { LdapProvider } from "./ldap";
|
||||
import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
|
||||
import { MongoAtlasProvider } from "./mongo-atlas";
|
||||
@ -38,5 +39,6 @@ export const buildDynamicSecretProviders = ({
|
||||
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
|
||||
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
|
||||
[DynamicSecretProviders.Totp]: TotpProvider(),
|
||||
[DynamicSecretProviders.SapAse]: SapAseProvider()
|
||||
[DynamicSecretProviders.SapAse]: SapAseProvider(),
|
||||
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService })
|
||||
});
|
||||
|
199
backend/src/ee/services/dynamic-secret/providers/kubernetes.ts
Normal file
199
backend/src/ee/services/dynamic-secret/providers/kubernetes.ts
Normal file
@ -0,0 +1,199 @@
|
||||
import axios from "axios";
|
||||
import https from "https";
|
||||
|
||||
import { InternalServerError } from "@app/lib/errors";
|
||||
import { withGatewayProxy } from "@app/lib/gateway";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { DynamicSecretKubernetesSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
type TKubernetesProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
};
|
||||
|
||||
export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs);
|
||||
if (!providerInputs.gatewayId) {
|
||||
await blockLocalAndPrivateIpAddresses(providerInputs.url);
|
||||
}
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const $gatewayProxyWrapper = async <T>(
|
||||
inputs: {
|
||||
gatewayId: string;
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
},
|
||||
gatewayCallback: (host: string, port: number) => Promise<T>
|
||||
): Promise<T> => {
|
||||
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
|
||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||
|
||||
const callbackResult = await withGatewayProxy(
|
||||
async (port) => {
|
||||
// Needs to be https protocol or the kubernetes API server will fail with "Client sent an HTTP request to an HTTPS server"
|
||||
const res = await gatewayCallback("https://localhost", port);
|
||||
return res;
|
||||
},
|
||||
{
|
||||
targetHost: inputs.targetHost,
|
||||
targetPort: inputs.targetPort,
|
||||
relayHost,
|
||||
relayPort: Number(relayPort),
|
||||
identityId: relayDetails.identityId,
|
||||
orgId: relayDetails.orgId,
|
||||
tlsOptions: {
|
||||
ca: relayDetails.certChain,
|
||||
cert: relayDetails.certificate,
|
||||
key: relayDetails.privateKey.toString()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return callbackResult;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const serviceAccountGetCallback = async (host: string, port: number) => {
|
||||
const baseUrl = port ? `${host}:${port}` : host;
|
||||
|
||||
await axios.get(
|
||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${providerInputs.serviceAccountName}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${providerInputs.clusterToken}`
|
||||
},
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent: new https.Agent({
|
||||
ca: providerInputs.ca,
|
||||
rejectUnauthorized: providerInputs.sslEnabled
|
||||
})
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const url = new URL(providerInputs.url);
|
||||
const k8sPort = url.port ? Number(url.port) : 443;
|
||||
|
||||
try {
|
||||
if (providerInputs.gatewayId) {
|
||||
const k8sHost = url.hostname;
|
||||
|
||||
await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sHost,
|
||||
targetPort: k8sPort
|
||||
},
|
||||
serviceAccountGetCallback
|
||||
);
|
||||
} else {
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
await serviceAccountGetCallback(k8sHost, k8sPort);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
|
||||
errorMessage = (error.response?.data as { message: string }).message;
|
||||
}
|
||||
|
||||
throw new InternalServerError({
|
||||
message: `Failed to validate connection: ${errorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const tokenRequestCallback = async (host: string, port: number) => {
|
||||
const baseUrl = port ? `${host}:${port}` : host;
|
||||
|
||||
const res = await axios.post<TKubernetesTokenRequest>(
|
||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${providerInputs.serviceAccountName}/token`,
|
||||
{
|
||||
spec: {
|
||||
expirationSeconds: Math.floor((expireAt - Date.now()) / 1000),
|
||||
...(providerInputs.audiences?.length ? { audiences: providerInputs.audiences } : {})
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${providerInputs.clusterToken}`
|
||||
},
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent: new https.Agent({
|
||||
ca: providerInputs.ca,
|
||||
rejectUnauthorized: providerInputs.sslEnabled
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const url = new URL(providerInputs.url);
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
const k8sGatewayHost = url.hostname;
|
||||
const k8sPort = url.port ? Number(url.port) : 443;
|
||||
|
||||
try {
|
||||
const tokenData = providerInputs.gatewayId
|
||||
? await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sGatewayHost,
|
||||
targetPort: k8sPort
|
||||
},
|
||||
tokenRequestCallback
|
||||
)
|
||||
: await tokenRequestCallback(k8sHost, k8sPort);
|
||||
|
||||
return {
|
||||
entityId: providerInputs.serviceAccountName,
|
||||
data: { TOKEN: tokenData.status.token }
|
||||
};
|
||||
} catch (error) {
|
||||
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
|
||||
errorMessage = (error.response?.data as { message: string }).message;
|
||||
}
|
||||
|
||||
throw new InternalServerError({
|
||||
message: `Failed to create dynamic secret: ${errorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (_inputs: unknown, entityId: string) => {
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
// No renewal necessary
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
@ -29,6 +29,10 @@ export enum LdapCredentialType {
|
||||
Static = "static"
|
||||
}
|
||||
|
||||
export enum KubernetesCredentialType {
|
||||
Static = "static"
|
||||
}
|
||||
|
||||
export enum TotpConfigType {
|
||||
URL = "url",
|
||||
MANUAL = "manual"
|
||||
@ -277,6 +281,18 @@ export const LdapSchema = z.union([
|
||||
})
|
||||
]);
|
||||
|
||||
export const DynamicSecretKubernetesSchema = z.object({
|
||||
url: z.string().url().trim().min(1),
|
||||
gatewayId: z.string().nullable().optional(),
|
||||
sslEnabled: z.boolean().default(true),
|
||||
clusterToken: z.string().trim().min(1),
|
||||
ca: z.string().optional(),
|
||||
serviceAccountName: z.string().trim().min(1),
|
||||
credentialType: z.literal(KubernetesCredentialType.Static),
|
||||
namespace: z.string().trim().min(1),
|
||||
audiences: z.array(z.string().trim().min(1))
|
||||
});
|
||||
|
||||
export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [
|
||||
z.object({
|
||||
configType: z.literal(TotpConfigType.URL),
|
||||
@ -320,7 +336,8 @@ export enum DynamicSecretProviders {
|
||||
SapHana = "sap-hana",
|
||||
Snowflake = "snowflake",
|
||||
Totp = "totp",
|
||||
SapAse = "sap-ase"
|
||||
SapAse = "sap-ase",
|
||||
Kubernetes = "kubernetes"
|
||||
}
|
||||
|
||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
@ -338,7 +355,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema })
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema })
|
||||
]);
|
||||
|
||||
export type TDynamicProviderFns = {
|
||||
|
@ -60,12 +60,19 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
enterpriseAppConnections: false
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
export const setupLicenseRequestWithStore = (
|
||||
baseURL: string,
|
||||
refreshUrl: string,
|
||||
licenseKey: string,
|
||||
region?: string
|
||||
) => {
|
||||
let token: string;
|
||||
const licenseReq = axios.create({
|
||||
baseURL,
|
||||
timeout: 35 * 1000
|
||||
// signal: AbortSignal.timeout(60 * 1000)
|
||||
timeout: 35 * 1000,
|
||||
headers: {
|
||||
"x-region": region
|
||||
}
|
||||
});
|
||||
|
||||
const refreshLicense = async () => {
|
||||
|
@ -77,13 +77,15 @@ export const licenseServiceFactory = ({
|
||||
const licenseServerCloudApi = setupLicenseRequestWithStore(
|
||||
appCfg.LICENSE_SERVER_URL || "",
|
||||
LICENSE_SERVER_CLOUD_LOGIN,
|
||||
appCfg.LICENSE_SERVER_KEY || ""
|
||||
appCfg.LICENSE_SERVER_KEY || "",
|
||||
appCfg.INTERNAL_REGION
|
||||
);
|
||||
|
||||
const licenseServerOnPremApi = setupLicenseRequestWithStore(
|
||||
appCfg.LICENSE_SERVER_URL || "",
|
||||
LICENSE_SERVER_ON_PREM_LOGIN,
|
||||
appCfg.LICENSE_KEY || ""
|
||||
appCfg.LICENSE_KEY || "",
|
||||
appCfg.INTERNAL_REGION
|
||||
);
|
||||
|
||||
const syncLicenseKeyOnPremFeatures = async (shouldThrow: boolean = false) => {
|
||||
|
@ -44,6 +44,7 @@ import {
|
||||
TOidcLoginDTO,
|
||||
TUpdateOidcCfgDTO
|
||||
} from "./oidc-config-types";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
type TOidcConfigServiceFactoryDep = {
|
||||
userDAL: Pick<
|
||||
@ -699,6 +700,7 @@ export const oidcConfigServiceFactory = ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(_req: any, tokenSet: TokenSet, cb: any) => {
|
||||
const claims = tokenSet.claims();
|
||||
logger.info(`User OIDC claims received for [orgId=${org.id}] [claims=${JSON.stringify(claims)}]`);
|
||||
if (!claims.email || !claims.given_name) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid request. Missing email or first name"
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
import { buildRedisFromConfig, TRedisConfigKeys } from "@app/lib/config/redis";
|
||||
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
|
||||
import { applyJitter } from "@app/lib/dates";
|
||||
import { delay as delayMs } from "@app/lib/delay";
|
||||
@ -37,6 +36,8 @@ export const KeyStorePrefixes = {
|
||||
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
|
||||
SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const,
|
||||
CaOrderCertificateForSubscriberLock: (subscriberId: string) =>
|
||||
`ca-order-certificate-for-subscriber-lock-${subscriberId}` as const,
|
||||
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
|
||||
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
||||
`identity-access-token-status:${identityAccessTokenId}`,
|
||||
@ -66,8 +67,8 @@ type TWaitTillReady = {
|
||||
jitter?: number;
|
||||
};
|
||||
|
||||
export const keyStoreFactory = (redisUrl: string) => {
|
||||
const redis = new Redis(redisUrl);
|
||||
export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys) => {
|
||||
const redis = buildRedisFromConfig(redisConfigKeys);
|
||||
const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 });
|
||||
|
||||
const setItem = async (key: string, value: string | number | Buffer, prefix?: string) =>
|
||||
|
@ -5,6 +5,8 @@ import {
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { CERTIFICATE_AUTHORITIES_TYPE_MAP } from "@app/services/certificate-authority/certificate-authority-maps";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
||||
|
||||
@ -145,7 +147,9 @@ export const UNIVERSAL_AUTH = {
|
||||
accessTokenMaxTTL:
|
||||
"The maximum lifetime for an access token in seconds. This value will be referenced at renewal time.",
|
||||
accessTokenNumUsesLimit:
|
||||
"The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
|
||||
"The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses.",
|
||||
accessTokenPeriod:
|
||||
"The period for an access token in seconds. This value will be referenced at renewal time. Default value is 0."
|
||||
},
|
||||
RETRIEVE: {
|
||||
identityId: "The ID of the identity to retrieve the auth method for."
|
||||
@ -159,7 +163,8 @@ export const UNIVERSAL_AUTH = {
|
||||
accessTokenTrustedIps: "The new list of IPs or CIDR ranges that access tokens can be used from.",
|
||||
accessTokenTTL: "The new lifetime for an access token in seconds.",
|
||||
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
|
||||
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
|
||||
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
|
||||
accessTokenPeriod: "The new period for an access token in seconds."
|
||||
},
|
||||
CREATE_CLIENT_SECRET: {
|
||||
identityId: "The ID of the identity to create a client secret for.",
|
||||
@ -1707,6 +1712,19 @@ export const CERTIFICATES = {
|
||||
certificateChain: "The certificate chain of the certificate.",
|
||||
serialNumberRes: "The serial number of the certificate.",
|
||||
privateKey: "The private key of the certificate."
|
||||
},
|
||||
IMPORT: {
|
||||
projectSlug: "Slug of the project to import the certificate into.",
|
||||
certificatePem: "The PEM-encoded leaf certificate.",
|
||||
privateKeyPem: "The PEM-encoded private key corresponding to the certificate.",
|
||||
chainPem: "The PEM-encoded chain of intermediate certificates.",
|
||||
friendlyName: "A friendly name for the certificate.",
|
||||
pkiCollectionId: "The ID of the PKI collection to add the certificate to.",
|
||||
|
||||
certificate: "The issued certificate.",
|
||||
certificateChain: "The certificate chain of the issued certificate.",
|
||||
privateKey: "The private key of the issued certificate.",
|
||||
serialNumber: "The serial number of the issued certificate."
|
||||
}
|
||||
};
|
||||
|
||||
@ -1778,6 +1796,14 @@ export const PKI_SUBSCRIBERS = {
|
||||
subscriberName: "The name of the PKI subscriber to get.",
|
||||
projectId: "The ID of the project to get the PKI subscriber for."
|
||||
},
|
||||
GET_LATEST_CERT_BUNDLE: {
|
||||
subscriberName: "The name of the PKI subscriber to get the active certificate bundle for.",
|
||||
projectId: "The ID of the project to get the active certificate bundle for.",
|
||||
certificate: "The active certificate for the subscriber.",
|
||||
certificateChain: "The certificate chain of the active certificate for the subscriber.",
|
||||
privateKey: "The private key of the active certificate for the subscriber.",
|
||||
serialNumber: "The serial number of the active certificate for the subscriber."
|
||||
},
|
||||
CREATE: {
|
||||
projectId: "The ID of the project to create the PKI subscriber in.",
|
||||
caId: "The ID of the CA that will issue certificates for the PKI subscriber.",
|
||||
@ -1788,7 +1814,9 @@ export const PKI_SUBSCRIBERS = {
|
||||
subjectAlternativeNames:
|
||||
"A list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.",
|
||||
keyUsages: "The key usage extension to be used on certificates issued for this subscriber.",
|
||||
extendedKeyUsages: "The extended key usage extension to be used on certificates issued for this subscriber."
|
||||
extendedKeyUsages: "The extended key usage extension to be used on certificates issued for this subscriber.",
|
||||
enableAutoRenewal: "Whether or not to enable auto renewal for the PKI subscriber.",
|
||||
autoRenewalPeriodInDays: "The period in days to auto renew the PKI subscriber's certificates."
|
||||
},
|
||||
UPDATE: {
|
||||
projectId: "The ID of the project to update the PKI subscriber in.",
|
||||
@ -1802,7 +1830,9 @@ export const PKI_SUBSCRIBERS = {
|
||||
"A comma-delimited list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.",
|
||||
keyUsages: "The key usage extension to be used on certificates issued for this subscriber to update to.",
|
||||
extendedKeyUsages:
|
||||
"The extended key usage extension to be used on certificates issued for this subscriber to update to."
|
||||
"The extended key usage extension to be used on certificates issued for this subscriber to update to.",
|
||||
enableAutoRenewal: "Whether or not to enable auto renewal for the PKI subscriber.",
|
||||
autoRenewalPeriodInDays: "The period in days to auto renew the PKI subscriber's certificates."
|
||||
},
|
||||
DELETE: {
|
||||
subscriberName: "The name of the PKI subscriber to delete.",
|
||||
@ -1991,6 +2021,47 @@ export const ProjectTemplates = {
|
||||
}
|
||||
};
|
||||
|
||||
export const CertificateAuthorities = {
|
||||
CREATE: (type: CaType) => ({
|
||||
name: `The name of the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority to create. Must be slug-friendly.`,
|
||||
projectId: `The ID of the project to create the Certificate Authority in.`,
|
||||
enableDirectIssuance: `Whether or not to enable direct issuance of certificates for the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority.`,
|
||||
status: `The status of the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority.`
|
||||
}),
|
||||
UPDATE: (type: CaType) => ({
|
||||
caId: `The ID of the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority to update.`,
|
||||
projectId: `The ID of the project to update the Certificate Authority in.`,
|
||||
name: `The updated name of the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority. Must be slug-friendly.`,
|
||||
enableDirectIssuance: `Whether or not to enable direct issuance of certificates for the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority.`,
|
||||
status: `The updated status of the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority.`
|
||||
}),
|
||||
CONFIGURATIONS: {
|
||||
ACME: {
|
||||
dnsAppConnectionId: `The ID of the App Connection to use for creating and managing DNS TXT records required for ACME domain validation. This connection must have permissions to create and delete TXT records in your DNS provider (e.g., Route53) for the ACME challenge process.`,
|
||||
directoryUrl: `The directory URL for the ACME Certificate Authority.`,
|
||||
accountEmail: `The email address for the ACME Certificate Authority.`,
|
||||
provider: `The DNS provider for the ACME Certificate Authority.`,
|
||||
hostedZoneId: `The hosted zone ID for the ACME Certificate Authority.`
|
||||
},
|
||||
INTERNAL: {
|
||||
type: "The type of CA to create.",
|
||||
friendlyName: "A friendly name for the CA.",
|
||||
organization: "The organization (O) for the CA.",
|
||||
ou: "The organization unit (OU) for the CA.",
|
||||
country: "The country name (C) for the CA.",
|
||||
province: "The state of province name for the CA.",
|
||||
locality: "The locality name for the CA.",
|
||||
commonName: "The common name (CN) for the CA.",
|
||||
notBefore: "The date and time when the CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format.",
|
||||
notAfter: "The date and time when the CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format.",
|
||||
maxPathLength:
|
||||
"The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.",
|
||||
keyAlgorithm:
|
||||
"The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const AppConnections = {
|
||||
GET_BY_ID: (app: AppConnection) => ({
|
||||
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
|
||||
|
@ -30,7 +30,19 @@ const envSchema = z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((el) => el === "true"),
|
||||
REDIS_URL: zpStr(z.string()),
|
||||
REDIS_URL: zpStr(z.string().optional()),
|
||||
REDIS_SENTINEL_HOSTS: zpStr(
|
||||
z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Comma-separated list of Sentinel host:port pairs. Eg: 192.168.65.254:26379,192.168.65.254:26380")
|
||||
),
|
||||
REDIS_SENTINEL_MASTER_NAME: zpStr(
|
||||
z.string().optional().default("mymaster").describe("The name of the Redis master set monitored by Sentinel")
|
||||
),
|
||||
REDIS_SENTINEL_ENABLE_TLS: zodStrBool.optional().describe("Whether to use TLS/SSL for Redis Sentinel connection"),
|
||||
REDIS_SENTINEL_USERNAME: zpStr(z.string().optional().describe("Authentication username for Redis Sentinel")),
|
||||
REDIS_SENTINEL_PASSWORD: zpStr(z.string().optional().describe("Authentication password for Redis Sentinel")),
|
||||
HOST: zpStr(z.string().default("localhost")),
|
||||
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(
|
||||
`postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`
|
||||
@ -233,7 +245,6 @@ const envSchema = z
|
||||
DATADOG_HOSTNAME: zpStr(z.string().optional()),
|
||||
|
||||
/* CORS ----------------------------------------------------------------------------- */
|
||||
|
||||
CORS_ALLOWED_ORIGINS: zpStr(
|
||||
z
|
||||
.string()
|
||||
@ -243,7 +254,6 @@ const envSchema = z
|
||||
return JSON.parse(val) as string[];
|
||||
})
|
||||
),
|
||||
|
||||
CORS_ALLOWED_HEADERS: zpStr(
|
||||
z
|
||||
.string()
|
||||
@ -252,33 +262,44 @@ const envSchema = z
|
||||
if (!val) return undefined;
|
||||
return JSON.parse(val) as string[];
|
||||
})
|
||||
)
|
||||
),
|
||||
|
||||
/* INTERNAL ----------------------------------------------------------------------------- */
|
||||
INTERNAL_REGION: zpStr(z.enum(["us", "eu"]).optional())
|
||||
})
|
||||
// To ensure that basic encryption is always possible.
|
||||
.refine(
|
||||
(data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY),
|
||||
"Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined."
|
||||
)
|
||||
.refine(
|
||||
(data) => Boolean(data.REDIS_URL) || Boolean(data.REDIS_SENTINEL_HOSTS),
|
||||
"Either REDIS_URL or REDIS_SENTINEL_HOSTS must be defined."
|
||||
)
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
|
||||
DB_READ_REPLICAS: data.DB_READ_REPLICAS
|
||||
? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS))
|
||||
: undefined,
|
||||
isCloud: Boolean(data.LICENSE_SERVER_KEY),
|
||||
isSmtpConfigured: Boolean(data.SMTP_HOST),
|
||||
isRedisConfigured: Boolean(data.REDIS_URL),
|
||||
isRedisConfigured: Boolean(data.REDIS_URL || data.REDIS_SENTINEL_HOSTS),
|
||||
isDevelopmentMode: data.NODE_ENV === "development",
|
||||
isRotationDevelopmentMode: data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE,
|
||||
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
|
||||
|
||||
isRedisSentinelMode: Boolean(data.REDIS_SENTINEL_HOSTS),
|
||||
REDIS_SENTINEL_HOSTS: data.REDIS_SENTINEL_HOSTS?.trim()
|
||||
?.split(",")
|
||||
.map((el) => {
|
||||
const [host, port] = el.trim().split(":");
|
||||
return { host: host.trim(), port: Number(port.trim()) };
|
||||
}),
|
||||
isSecretScanningConfigured:
|
||||
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
|
||||
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
||||
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
|
||||
isHsmConfigured:
|
||||
Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined,
|
||||
|
||||
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
|
||||
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
|
||||
}));
|
||||
|
24
backend/src/lib/config/redis.ts
Normal file
24
backend/src/lib/config/redis.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
export type TRedisConfigKeys = Partial<{
|
||||
REDIS_URL: string;
|
||||
REDIS_SENTINEL_HOSTS: { host: string; port: number }[];
|
||||
REDIS_SENTINEL_MASTER_NAME: string;
|
||||
REDIS_SENTINEL_ENABLE_TLS: boolean;
|
||||
REDIS_SENTINEL_USERNAME: string;
|
||||
REDIS_SENTINEL_PASSWORD: string;
|
||||
}>;
|
||||
|
||||
export const buildRedisFromConfig = (cfg: TRedisConfigKeys) => {
|
||||
if (cfg.REDIS_URL) return new Redis(cfg.REDIS_URL, { maxRetriesPerRequest: null });
|
||||
|
||||
return new Redis({
|
||||
// refine at tope will catch this case
|
||||
sentinels: cfg.REDIS_SENTINEL_HOSTS!,
|
||||
name: cfg.REDIS_SENTINEL_MASTER_NAME!,
|
||||
maxRetriesPerRequest: null,
|
||||
sentinelUsername: cfg.REDIS_SENTINEL_USERNAME,
|
||||
sentinelPassword: cfg.REDIS_SENTINEL_PASSWORD,
|
||||
enableTLSForSentinelMode: cfg.REDIS_SENTINEL_ENABLE_TLS
|
||||
});
|
||||
};
|
@ -3,6 +3,7 @@ import crypto from "node:crypto";
|
||||
import net from "node:net";
|
||||
|
||||
import quicDefault, * as quicModule from "@infisical/quic";
|
||||
import axios from "axios";
|
||||
|
||||
import { BadRequestError } from "../errors";
|
||||
import { logger } from "../logger";
|
||||
@ -378,7 +379,12 @@ export const withGatewayProxy = async <T>(
|
||||
logger.error(new Error(proxyErrorMessage), "Failed to proxy");
|
||||
}
|
||||
logger.error(err, "Failed to do gateway");
|
||||
throw new BadRequestError({ message: proxyErrorMessage || (err as Error)?.message });
|
||||
let errorMessage = proxyErrorMessage || (err as Error)?.message;
|
||||
if (axios.isAxiosError(err) && (err.response?.data as { message?: string })?.message) {
|
||||
errorMessage = (err.response?.data as { message: string }).message;
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: errorMessage });
|
||||
} finally {
|
||||
// Ensure cleanup happens regardless of success or failure
|
||||
await cleanup();
|
||||
|
@ -1,7 +1,6 @@
|
||||
import "./lib/telemetry/instrumentation";
|
||||
|
||||
import dotenv from "dotenv";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
||||
|
||||
@ -9,6 +8,7 @@ import { runMigrations } from "./auto-start-migrations";
|
||||
import { initAuditLogDbConnection, initDbConnection } from "./db";
|
||||
import { keyStoreFactory } from "./keystore/keystore";
|
||||
import { formatSmtpConfig, initEnvConfig } from "./lib/config/env";
|
||||
import { buildRedisFromConfig } from "./lib/config/redis";
|
||||
import { removeTemporaryBaseDirectory } from "./lib/files";
|
||||
import { initLogger } from "./lib/logger";
|
||||
import { queueServiceFactory } from "./queue";
|
||||
@ -44,15 +44,15 @@ const run = async () => {
|
||||
|
||||
const smtp = smtpServiceFactory(formatSmtpConfig());
|
||||
|
||||
const queue = queueServiceFactory(envConfig.REDIS_URL, {
|
||||
const queue = queueServiceFactory(envConfig, {
|
||||
dbConnectionUrl: envConfig.DB_CONNECTION_URI,
|
||||
dbRootCert: envConfig.DB_ROOT_CERT
|
||||
});
|
||||
|
||||
await queue.initialize();
|
||||
|
||||
const keyStore = keyStoreFactory(envConfig.REDIS_URL);
|
||||
const redis = new Redis(envConfig.REDIS_URL);
|
||||
const keyStore = keyStoreFactory(envConfig);
|
||||
const redis = buildRedisFromConfig(envConfig);
|
||||
|
||||
const hsmModule = initializeHsmModule(envConfig);
|
||||
hsmModule.initialize();
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
|
||||
import Redis from "ioredis";
|
||||
import PgBoss, { WorkOptions } from "pg-boss";
|
||||
|
||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||
@ -13,7 +12,9 @@ import {
|
||||
TScanPushEventPayload
|
||||
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { buildRedisFromConfig, TRedisConfigKeys } from "@app/lib/config/redis";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import {
|
||||
TFailedIntegrationSyncEmailsPayload,
|
||||
TIntegrationSyncPayload,
|
||||
@ -36,6 +37,7 @@ export enum QueueName {
|
||||
AuditLogPrune = "audit-log-prune",
|
||||
DailyResourceCleanUp = "daily-resource-cleanup",
|
||||
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
|
||||
PkiSubscriber = "pki-subscriber",
|
||||
TelemetryInstanceStats = "telemtry-self-hosted-stats",
|
||||
IntegrationSync = "sync-integrations",
|
||||
SecretWebhook = "secret-webhook",
|
||||
@ -44,6 +46,7 @@ export enum QueueName {
|
||||
UpgradeProjectToGhost = "upgrade-project-to-ghost",
|
||||
DynamicSecretRevocation = "dynamic-secret-revocation",
|
||||
CaCrlRotation = "ca-crl-rotation",
|
||||
CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber
|
||||
SecretReplication = "secret-replication",
|
||||
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
|
||||
ProjectV3Migration = "project-v3-migration",
|
||||
@ -84,7 +87,9 @@ export enum QueueJobs {
|
||||
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
||||
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
||||
InvalidateCache = "invalidate-cache"
|
||||
InvalidateCache = "invalidate-cache",
|
||||
CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber",
|
||||
PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal"
|
||||
}
|
||||
|
||||
export type TQueueJobTypes = {
|
||||
@ -245,14 +250,25 @@ export type TQueueJobTypes = {
|
||||
};
|
||||
};
|
||||
};
|
||||
[QueueName.CaLifecycle]: {
|
||||
name: QueueJobs.CaOrderCertificateForSubscriber;
|
||||
payload: {
|
||||
subscriberId: string;
|
||||
caType: CaType;
|
||||
};
|
||||
};
|
||||
[QueueName.PkiSubscriber]: {
|
||||
name: QueueJobs.PkiSubscriberDailyAutoRenewal;
|
||||
payload: undefined;
|
||||
};
|
||||
};
|
||||
|
||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||
export const queueServiceFactory = (
|
||||
redisUrl: string,
|
||||
redisCfg: TRedisConfigKeys,
|
||||
{ dbConnectionUrl, dbRootCert }: { dbConnectionUrl: string; dbRootCert?: string }
|
||||
) => {
|
||||
const connection = new Redis(redisUrl, { maxRetriesPerRequest: null });
|
||||
const connection = buildRedisFromConfig(redisCfg);
|
||||
const queueContainer = {} as Record<
|
||||
QueueName,
|
||||
Queue<TQueueJobTypes[QueueName]["payload"], void, TQueueJobTypes[QueueName]["name"]>
|
||||
|
@ -1,9 +1,9 @@
|
||||
/* eslint-disable no-console */
|
||||
import { Redis } from "ioredis";
|
||||
import { Knex } from "knex";
|
||||
import { createTransport } from "nodemailer";
|
||||
|
||||
import { formatSmtpConfig, getConfig } from "@app/lib/config/env";
|
||||
import { buildRedisFromConfig } from "@app/lib/config/redis";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
@ -65,12 +65,15 @@ export const bootstrapCheck = async ({ db }: BootstrapOpt) => {
|
||||
});
|
||||
|
||||
console.log("Testing redis connection");
|
||||
const redis = new Redis(appCfg.REDIS_URL);
|
||||
const redis = buildRedisFromConfig(appCfg);
|
||||
const redisPing = await redis?.ping();
|
||||
if (!redisPing) {
|
||||
console.error("Redis - Failed to connect");
|
||||
} else {
|
||||
console.error("Redis successfully connected");
|
||||
console.log("Redis successfully connected");
|
||||
if (appCfg.isRedisSentinelMode) {
|
||||
console.log("Redis Sentinel Mode");
|
||||
}
|
||||
redis.disconnect();
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,12 @@
|
||||
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { buildRedisFromConfig } from "@app/lib/config/redis";
|
||||
import { RateLimitError } from "@app/lib/errors";
|
||||
|
||||
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
||||
const appCfg = getConfig();
|
||||
const redis = appCfg.isRedisConfigured
|
||||
? new Redis(appCfg.REDIS_URL, { connectTimeout: 500, maxRetriesPerRequest: 1 })
|
||||
: null;
|
||||
const redis = appCfg.isRedisConfigured ? buildRedisFromConfig(appCfg) : null;
|
||||
|
||||
return {
|
||||
errorResponseBuilder: (_, context) => {
|
||||
|
@ -132,6 +132,10 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
|
||||
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
|
||||
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { externalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal";
|
||||
import { internalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-dal";
|
||||
import { InternalCertificateAuthorityFns } from "@app/services/certificate-authority/internal/internal-certificate-authority-fns";
|
||||
import { internalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
@ -199,6 +203,7 @@ import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collec
|
||||
import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
||||
import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
|
||||
import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
import { pkiSubscriberQueueServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-queue";
|
||||
import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
|
||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||
import { projectQueueFactory } from "@app/services/project/project-queue";
|
||||
@ -817,6 +822,8 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const certificateAuthorityDAL = certificateAuthorityDALFactory(db);
|
||||
const internalCertificateAuthorityDAL = internalCertificateAuthorityDALFactory(db);
|
||||
const externalCertificateAuthorityDAL = externalCertificateAuthorityDALFactory(db);
|
||||
const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db);
|
||||
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
|
||||
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
|
||||
@ -842,17 +849,9 @@ export const registerRoutes = async (
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const certificateAuthorityQueue = certificateAuthorityQueueFactory({
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
queueService
|
||||
permissionService,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL
|
||||
});
|
||||
|
||||
const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({
|
||||
@ -901,23 +900,6 @@ export const registerRoutes = async (
|
||||
groupDAL
|
||||
});
|
||||
|
||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityQueue,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const certificateAuthorityCrlService = certificateAuthorityCrlServiceFactory({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
@ -937,17 +919,6 @@ export const registerRoutes = async (
|
||||
licenseService
|
||||
});
|
||||
|
||||
const certificateEstService = certificateEstServiceFactory({
|
||||
certificateAuthorityService,
|
||||
certificateTemplateService,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const pkiAlertService = pkiAlertServiceFactory({
|
||||
pkiAlertDAL,
|
||||
pkiCollectionDAL,
|
||||
@ -965,20 +936,6 @@ export const registerRoutes = async (
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const pkiSubscriberService = pkiSubscriberServiceFactory({
|
||||
pkiSubscriberDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const projectTemplateService = projectTemplateServiceFactory({
|
||||
licenseService,
|
||||
permissionService,
|
||||
@ -1648,6 +1605,52 @@ export const registerRoutes = async (
|
||||
licenseService
|
||||
});
|
||||
|
||||
const certificateAuthorityQueue = certificateAuthorityQueueFactory({
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
queueService,
|
||||
pkiSubscriberDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
externalCertificateAuthorityDAL,
|
||||
keyStore,
|
||||
appConnectionDAL,
|
||||
appConnectionService
|
||||
});
|
||||
|
||||
const internalCertificateAuthorityService = internalCertificateAuthorityServiceFactory({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityQueue,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
projectDAL,
|
||||
internalCertificateAuthorityDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const certificateEstService = certificateEstServiceFactory({
|
||||
internalCertificateAuthorityService,
|
||||
certificateTemplateService,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const kmipService = kmipServiceFactory({
|
||||
kmipClientDAL,
|
||||
permissionService,
|
||||
@ -1687,6 +1690,59 @@ export const registerRoutes = async (
|
||||
appConnectionDAL
|
||||
});
|
||||
|
||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
permissionService,
|
||||
appConnectionDAL,
|
||||
appConnectionService,
|
||||
externalCertificateAuthorityDAL,
|
||||
internalCertificateAuthorityService,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
pkiSubscriberDAL
|
||||
});
|
||||
|
||||
const internalCaFns = InternalCertificateAuthorityFns({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const pkiSubscriberQueue = pkiSubscriberQueueServiceFactory({
|
||||
queueService,
|
||||
pkiSubscriberDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityQueue,
|
||||
certificateDAL,
|
||||
auditLogService,
|
||||
internalCaFns
|
||||
});
|
||||
|
||||
const pkiSubscriberService = pkiSubscriberServiceFactory({
|
||||
pkiSubscriberDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService,
|
||||
certificateAuthorityQueue,
|
||||
internalCaFns
|
||||
});
|
||||
|
||||
await secretRotationV2QueueServiceFactory({
|
||||
secretRotationV2Service,
|
||||
secretRotationV2DAL,
|
||||
@ -1707,6 +1763,7 @@ export const registerRoutes = async (
|
||||
await telemetryQueue.startTelemetryCheck();
|
||||
await dailyResourceCleanUp.startCleanUp();
|
||||
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||
await pkiSubscriberQueue.startDailyAutoRenewalJob();
|
||||
await kmsService.startService();
|
||||
await microsoftTeamsService.start();
|
||||
|
||||
@ -1772,6 +1829,7 @@ export const registerRoutes = async (
|
||||
sshHost: sshHostService,
|
||||
sshHostGroup: sshHostGroupService,
|
||||
certificateAuthority: certificateAuthorityService,
|
||||
internalCertificateAuthority: internalCertificateAuthorityService,
|
||||
certificateTemplate: certificateTemplateService,
|
||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||
certificateEst: certificateEstService,
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
CertificateAuthoritiesSchema,
|
||||
DynamicSecretsSchema,
|
||||
IdentityProjectAdditionalPrivilegeSchema,
|
||||
IntegrationAuthsSchema,
|
||||
InternalCertificateAuthoritiesSchema,
|
||||
ProjectRolesSchema,
|
||||
ProjectsSchema,
|
||||
SecretApprovalPoliciesSchema,
|
||||
@ -272,3 +274,15 @@ export const SanitizedTagSchema = SecretTagsSchema.pick({
|
||||
}).extend({
|
||||
name: z.string()
|
||||
});
|
||||
|
||||
export const InternalCertificateAuthorityResponseSchema = CertificateAuthoritiesSchema.merge(
|
||||
InternalCertificateAuthoritiesSchema.omit({
|
||||
caId: true,
|
||||
notAfter: true,
|
||||
notBefore: true
|
||||
})
|
||||
).extend({
|
||||
requireTemplateForIssuance: z.boolean().optional(),
|
||||
notAfter: z.string().optional(),
|
||||
notBefore: z.string().optional()
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificateAuthoritiesSchema, CertificateTemplatesSchema } from "@app/db/schemas";
|
||||
import { CertificateTemplatesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
@ -10,13 +10,19 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import {
|
||||
CaRenewalType,
|
||||
CaStatus,
|
||||
InternalCaType
|
||||
} from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import {
|
||||
validateAltNamesField,
|
||||
validateCaDateField
|
||||
} from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
import { InternalCertificateAuthorityResponseSchema } from "../sanitizedSchemas";
|
||||
|
||||
export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
@ -32,7 +38,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
body: z
|
||||
.object({
|
||||
projectSlug: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.projectSlug),
|
||||
type: z.nativeEnum(CaType).describe(CERTIFICATE_AUTHORITIES.CREATE.type),
|
||||
type: z.nativeEnum(InternalCaType).describe(CERTIFICATE_AUTHORITIES.CREATE.type),
|
||||
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
|
||||
commonName: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.commonName),
|
||||
organization: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.organization),
|
||||
@ -68,16 +74,18 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
ca: CertificateAuthoritiesSchema
|
||||
ca: InternalCertificateAuthorityResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const ca = await server.services.certificateAuthority.createCa({
|
||||
const ca = await server.services.internalCertificateAuthority.createCa({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
isInternal: false,
|
||||
actorOrgId: req.permission.orgId,
|
||||
enableDirectIssuance: !req.body.requireTemplateForIssuance,
|
||||
...req.body
|
||||
});
|
||||
|
||||
@ -87,6 +95,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
event: {
|
||||
type: EventType.CREATE_CA,
|
||||
metadata: {
|
||||
name: ca.name,
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
}
|
||||
@ -115,12 +124,12 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
ca: CertificateAuthoritiesSchema
|
||||
ca: InternalCertificateAuthorityResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const ca = await server.services.certificateAuthority.getCaById({
|
||||
const ca = await server.services.internalCertificateAuthority.getCaById({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -135,6 +144,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
type: EventType.GET_CA,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
name: ca.name,
|
||||
dn: ca.dn
|
||||
}
|
||||
}
|
||||
@ -167,7 +177,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const caCert = await server.services.certificateAuthority.getCaCertById(req.params);
|
||||
const caCert = await server.services.internalCertificateAuthority.getCaCertById(req.params);
|
||||
|
||||
res.header("Content-Type", "application/pkix-cert");
|
||||
|
||||
@ -198,17 +208,19 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
ca: CertificateAuthoritiesSchema
|
||||
ca: InternalCertificateAuthorityResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const ca = await server.services.certificateAuthority.updateCaById({
|
||||
const ca = await server.services.internalCertificateAuthority.updateCaById({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
isInternal: false,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
enableDirectIssuance: !req.body.requireTemplateForIssuance,
|
||||
...req.body
|
||||
});
|
||||
|
||||
@ -220,6 +232,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn,
|
||||
name: ca.name,
|
||||
status: ca.status as CaStatus
|
||||
}
|
||||
}
|
||||
@ -247,12 +260,12 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
ca: CertificateAuthoritiesSchema
|
||||
ca: InternalCertificateAuthorityResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const ca = await server.services.certificateAuthority.deleteCaById({
|
||||
const ca = await server.services.internalCertificateAuthority.deleteCaById({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -266,6 +279,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
event: {
|
||||
type: EventType.DELETE_CA,
|
||||
metadata: {
|
||||
name: ca.name,
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
}
|
||||
@ -299,7 +313,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { ca, csr } = await server.services.certificateAuthority.getCaCsr({
|
||||
const { ca, csr } = await server.services.internalCertificateAuthority.getCaCsr({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -353,7 +367,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.renewCaCert({
|
||||
await server.services.internalCertificateAuthority.renewCaCert({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -408,7 +422,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { caCerts, ca } = await server.services.certificateAuthority.getCaCerts({
|
||||
const { caCerts, ca } = await server.services.internalCertificateAuthority.getCaCerts({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -455,13 +469,14 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, serialNumber, ca } = 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
|
||||
});
|
||||
const { certificate, certificateChain, serialNumber, ca } =
|
||||
await server.services.internalCertificateAuthority.getCaCert({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
@ -517,7 +532,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.signIntermediate({
|
||||
await server.services.internalCertificateAuthority.signIntermediate({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -574,7 +589,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { ca } = await server.services.certificateAuthority.importCertToCa({
|
||||
const { ca } = await server.services.internalCertificateAuthority.importCertToCa({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -653,7 +668,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.issueCertFromCa({
|
||||
await server.services.internalCertificateAuthority.issueCertFromCa({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -746,7 +761,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
await server.services.internalCertificateAuthority.signCertFromCa({
|
||||
isInternal: false,
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
@ -809,13 +824,15 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificateTemplates, ca } = await server.services.certificateAuthority.getCaCertificateTemplates({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
const { certificateTemplates, ca } = await server.services.internalCertificateAuthority.getCaCertificateTemplates(
|
||||
{
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
}
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
|
@ -0,0 +1,18 @@
|
||||
import {
|
||||
AcmeCertificateAuthoritySchema,
|
||||
CreateAcmeCertificateAuthoritySchema,
|
||||
UpdateAcmeCertificateAuthoritySchema
|
||||
} from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas";
|
||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
|
||||
import { registerCertificateAuthorityEndpoints } from "./certificate-authority-endpoints";
|
||||
|
||||
export const registerAcmeCertificateAuthorityRouter = async (server: FastifyZodProvider) => {
|
||||
registerCertificateAuthorityEndpoints({
|
||||
caType: CaType.ACME,
|
||||
server,
|
||||
responseSchema: AcmeCertificateAuthoritySchema,
|
||||
createSchema: CreateAcmeCertificateAuthoritySchema,
|
||||
updateSchema: UpdateAcmeCertificateAuthoritySchema
|
||||
});
|
||||
};
|
@ -0,0 +1,258 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||
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 { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import {
|
||||
TCertificateAuthority,
|
||||
TCertificateAuthorityInput
|
||||
} from "@app/services/certificate-authority/certificate-authority-types";
|
||||
|
||||
export const registerCertificateAuthorityEndpoints = <
|
||||
T extends TCertificateAuthority,
|
||||
I extends TCertificateAuthorityInput
|
||||
>({
|
||||
server,
|
||||
caType,
|
||||
createSchema,
|
||||
updateSchema,
|
||||
responseSchema
|
||||
}: {
|
||||
caType: CaType;
|
||||
server: FastifyZodProvider;
|
||||
createSchema: z.ZodType<{
|
||||
name: string;
|
||||
projectId: string;
|
||||
status: CaStatus;
|
||||
configuration: I["configuration"];
|
||||
enableDirectIssuance: boolean;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{
|
||||
projectId: string;
|
||||
name?: string;
|
||||
status?: CaStatus;
|
||||
configuration?: I["configuration"];
|
||||
enableDirectIssuance?: boolean;
|
||||
}>;
|
||||
responseSchema: z.ZodTypeAny;
|
||||
}) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1, "Project ID required")
|
||||
}),
|
||||
response: {
|
||||
200: responseSchema.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId }
|
||||
} = req;
|
||||
|
||||
const certificateAuthorities = (await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId(
|
||||
{ projectId, type: caType },
|
||||
req.permission
|
||||
)) as T[];
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.GET_CAS,
|
||||
metadata: {
|
||||
caIds: certificateAuthorities.map((ca) => ca.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return certificateAuthorities;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caName",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
params: z.object({
|
||||
caName: z.string()
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: responseSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { caName } = req.params;
|
||||
const { projectId } = req.query;
|
||||
|
||||
const certificateAuthority =
|
||||
(await server.services.certificateAuthority.findCertificateAuthorityByNameAndProjectId(
|
||||
{ caName, type: caType, projectId },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: certificateAuthority.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CA,
|
||||
metadata: {
|
||||
caId: certificateAuthority.id,
|
||||
name: certificateAuthority.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return certificateAuthority;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
body: createSchema,
|
||||
response: {
|
||||
200: responseSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const certificateAuthority = (await server.services.certificateAuthority.createCertificateAuthority(
|
||||
{ ...req.body, type: caType },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: certificateAuthority.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_CA,
|
||||
metadata: {
|
||||
name: certificateAuthority.name,
|
||||
caId: certificateAuthority.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return certificateAuthority;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:caName",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
params: z.object({
|
||||
caName: z.string()
|
||||
}),
|
||||
body: updateSchema,
|
||||
response: {
|
||||
200: responseSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { caName } = req.params;
|
||||
|
||||
const certificateAuthority = (await server.services.certificateAuthority.updateCertificateAuthority(
|
||||
{
|
||||
...req.body,
|
||||
type: caType,
|
||||
caName
|
||||
},
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: certificateAuthority.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_CA,
|
||||
metadata: {
|
||||
name: certificateAuthority.name,
|
||||
caId: certificateAuthority.id,
|
||||
status: certificateAuthority.status
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return certificateAuthority;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:caName",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
params: z.object({
|
||||
caName: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
projectId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: responseSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { caName } = req.params;
|
||||
const { projectId } = req.body;
|
||||
|
||||
const certificateAuthority = (await server.services.certificateAuthority.deleteCertificateAuthority(
|
||||
{ caName, type: caType, projectId },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: certificateAuthority.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_CA,
|
||||
metadata: {
|
||||
name: certificateAuthority.name,
|
||||
caId: certificateAuthority.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return certificateAuthority;
|
||||
}
|
||||
});
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
|
||||
import { registerAcmeCertificateAuthorityRouter } from "./acme-certificate-authority-router";
|
||||
import { registerInternalCertificateAuthorityRouter } from "./internal-certificate-authority-router";
|
||||
|
||||
export * from "./internal-certificate-authority-router";
|
||||
|
||||
export const CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP: Record<CaType, (server: FastifyZodProvider) => Promise<void>> =
|
||||
{
|
||||
[CaType.INTERNAL]: registerInternalCertificateAuthorityRouter,
|
||||
[CaType.ACME]: registerAcmeCertificateAuthorityRouter
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import {
|
||||
CreateInternalCertificateAuthoritySchema,
|
||||
InternalCertificateAuthoritySchema,
|
||||
UpdateInternalCertificateAuthoritySchema
|
||||
} from "@app/services/certificate-authority/internal/internal-certificate-authority-schemas";
|
||||
|
||||
import { registerCertificateAuthorityEndpoints } from "./certificate-authority-endpoints";
|
||||
|
||||
export const registerInternalCertificateAuthorityRouter = async (server: FastifyZodProvider) => {
|
||||
registerCertificateAuthorityEndpoints({
|
||||
caType: CaType.INTERNAL,
|
||||
server,
|
||||
responseSchema: InternalCertificateAuthoritySchema,
|
||||
createSchema: CreateInternalCertificateAuthoritySchema,
|
||||
updateSchema: UpdateInternalCertificateAuthoritySchema
|
||||
});
|
||||
};
|
@ -39,7 +39,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { cert, ca } = await server.services.certificate.getCert({
|
||||
const { cert } = await server.services.certificate.getCert({
|
||||
serialNumber: req.params.serialNumber,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -49,7 +49,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERT,
|
||||
metadata: {
|
||||
@ -86,7 +86,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req, reply) => {
|
||||
const { ca, cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
|
||||
const { cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
|
||||
serialNumber: req.params.serialNumber,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -96,7 +96,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERT_PRIVATE_KEY,
|
||||
metadata: {
|
||||
@ -138,7 +138,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req, reply) => {
|
||||
const { certificate, certificateChain, serialNumber, cert, ca, privateKey } =
|
||||
const { certificate, certificateChain, serialNumber, cert, privateKey } =
|
||||
await server.services.certificate.getCertBundle({
|
||||
serialNumber: req.params.serialNumber,
|
||||
actor: req.permission.type,
|
||||
@ -149,7 +149,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERT_BUNDLE,
|
||||
metadata: {
|
||||
@ -242,7 +242,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.issueCertFromCa({
|
||||
await server.services.internalCertificateAuthority.issueCertFromCa({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@ -284,6 +284,68 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/import-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
description: "Import certificate",
|
||||
body: z.object({
|
||||
projectSlug: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.projectSlug),
|
||||
|
||||
certificatePem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.certificatePem),
|
||||
privateKeyPem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.privateKeyPem),
|
||||
chainPem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.chainPem),
|
||||
|
||||
friendlyName: z.string().trim().optional().describe(CERTIFICATES.IMPORT.friendlyName),
|
||||
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATES.IMPORT.pkiCollectionId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATES.IMPORT.certificate),
|
||||
certificateChain: z.string().trim().describe(CERTIFICATES.IMPORT.certificateChain),
|
||||
privateKey: z.string().trim().describe(CERTIFICATES.IMPORT.privateKey),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATES.IMPORT.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, privateKey, serialNumber, cert } =
|
||||
await server.services.certificate.importCert({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: cert.projectId,
|
||||
event: {
|
||||
type: EventType.IMPORT_CERT,
|
||||
metadata: {
|
||||
certId: cert.id,
|
||||
cn: cert.commonName,
|
||||
serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/sign-certificate",
|
||||
@ -355,7 +417,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
await server.services.internalCertificateAuthority.signCertFromCa({
|
||||
isInternal: false,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -474,7 +536,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { deletedCert, ca } = await server.services.certificate.deleteCert({
|
||||
const { deletedCert } = await server.services.certificate.deleteCert({
|
||||
serialNumber: req.params.serialNumber,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -484,7 +546,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
projectId: deletedCert.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_CERT,
|
||||
metadata: {
|
||||
@ -524,7 +586,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, serialNumber, cert, ca } = await server.services.certificate.getCertBody({
|
||||
const { certificate, certificateChain, serialNumber, cert } = await server.services.certificate.getCertBody({
|
||||
serialNumber: req.params.serialNumber,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -534,7 +596,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERT_BODY,
|
||||
metadata: {
|
||||
|
@ -47,8 +47,15 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityUa, accessToken, identityAccessToken, validClientSecretInfo, identityMembershipOrg } =
|
||||
await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp);
|
||||
const {
|
||||
identityUa,
|
||||
accessToken,
|
||||
identityAccessToken,
|
||||
validClientSecretInfo,
|
||||
identityMembershipOrg,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL
|
||||
} = await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
@ -63,11 +70,12 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
tokenType: "Bearer" as const,
|
||||
expiresIn: identityUa.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityUa.accessTokenMaxTTL
|
||||
expiresIn: accessTokenTTL,
|
||||
accessTokenMaxTTL
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -128,7 +136,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
.int()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit),
|
||||
accessTokenPeriod: z.number().int().min(0).default(0).describe(UNIVERSAL_AUTH.ATTACH.accessTokenPeriod)
|
||||
})
|
||||
.refine(
|
||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||
@ -227,7 +236,14 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.optional()
|
||||
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenMaxTTL)
|
||||
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenMaxTTL),
|
||||
accessTokenPeriod: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.optional()
|
||||
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenPeriod)
|
||||
})
|
||||
.refine(
|
||||
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||
|
@ -10,6 +10,7 @@ import { registerAdminRouter } from "./admin-router";
|
||||
import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
|
||||
import { registerCertRouter } from "./certificate-router";
|
||||
import { registerCertificateTemplateRouter } from "./certificate-template-router";
|
||||
import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router";
|
||||
@ -104,6 +105,16 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(
|
||||
async (pkiRouter) => {
|
||||
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
|
||||
await pkiRouter.register(
|
||||
async (caRouter) => {
|
||||
for await (const [caType, router] of Object.entries(CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP)) {
|
||||
await caRouter.register(router, { prefix: `/${caType}` });
|
||||
}
|
||||
},
|
||||
{
|
||||
prefix: "/ca"
|
||||
}
|
||||
);
|
||||
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
|
||||
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
||||
|
@ -5,6 +5,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, PKI_SUBSCRIBERS } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { addNoCacheHeaders } from "@app/server/lib/caching";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -90,7 +91,8 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
ttl: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.refine((val) => !val || ms(val) > 0, "TTL must be a positive number")
|
||||
.optional()
|
||||
.describe(PKI_SUBSCRIBERS.CREATE.ttl),
|
||||
subjectAlternativeNames: validateAltNameField
|
||||
.array()
|
||||
@ -108,7 +110,9 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
.array()
|
||||
.default([])
|
||||
.transform((arr) => Array.from(new Set(arr)))
|
||||
.describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages)
|
||||
.describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages),
|
||||
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.CREATE.enableAutoRenewal),
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.CREATE.autoRenewalPeriodInDays)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedPkiSubscriber
|
||||
@ -134,7 +138,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
caId: subscriber.caId ?? undefined,
|
||||
name: subscriber.name,
|
||||
commonName: subscriber.commonName,
|
||||
ttl: subscriber.ttl,
|
||||
ttl: subscriber.ttl ?? undefined,
|
||||
subjectAlternativeNames: subscriber.subjectAlternativeNames,
|
||||
keyUsages: subscriber.keyUsages as CertKeyUsage[],
|
||||
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
|
||||
@ -179,7 +183,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
ttl: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.refine((val) => !val || ms(val) > 0, "TTL must be a positive number")
|
||||
.optional()
|
||||
.describe(PKI_SUBSCRIBERS.UPDATE.ttl),
|
||||
keyUsages: z
|
||||
@ -193,7 +197,9 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
.array()
|
||||
.transform((arr) => Array.from(new Set(arr)))
|
||||
.optional()
|
||||
.describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages)
|
||||
.describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages),
|
||||
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.UPDATE.enableAutoRenewal),
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.UPDATE.autoRenewalPeriodInDays)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedPkiSubscriber
|
||||
@ -219,7 +225,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
caId: subscriber.caId ?? undefined,
|
||||
name: subscriber.name,
|
||||
commonName: subscriber.commonName,
|
||||
ttl: subscriber.ttl,
|
||||
ttl: subscriber.ttl ?? undefined,
|
||||
subjectAlternativeNames: subscriber.subjectAlternativeNames,
|
||||
keyUsages: subscriber.keyUsages as CertKeyUsage[],
|
||||
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
|
||||
@ -278,6 +284,67 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:subscriberName/order-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSubscribers],
|
||||
description: "Order certificate",
|
||||
params: z.object({
|
||||
subscriberName: z.string().describe(PKI_SUBSCRIBERS.ISSUE_CERT.subscriberName)
|
||||
}),
|
||||
body: z.object({
|
||||
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string().trim()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const subscriber = await server.services.pkiSubscriber.orderSubscriberCert({
|
||||
subscriberName: req.params.subscriberName,
|
||||
projectId: req.body.projectId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: subscriber.projectId,
|
||||
event: {
|
||||
type: EventType.ISSUE_PKI_SUBSCRIBER_CERT,
|
||||
metadata: {
|
||||
subscriberId: subscriber.id,
|
||||
name: subscriber.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
subscriberId: subscriber.id,
|
||||
commonName: subscriber.commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: "Successfully placed order for certificate"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:subscriberName/issue-certificate",
|
||||
@ -420,6 +487,72 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:subscriberName/latest-certificate-bundle",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSubscribers],
|
||||
description: "Get latest certificate bundle of a subscriber",
|
||||
params: z.object({
|
||||
subscriberName: z.string().describe(PKI_SUBSCRIBERS.GET_LATEST_CERT_BUNDLE.subscriberName)
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.GET_LATEST_CERT_BUNDLE.projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(PKI_SUBSCRIBERS.GET_LATEST_CERT_BUNDLE.certificate),
|
||||
certificateChain: z
|
||||
.string()
|
||||
.trim()
|
||||
.nullable()
|
||||
.describe(PKI_SUBSCRIBERS.GET_LATEST_CERT_BUNDLE.certificateChain),
|
||||
privateKey: z.string().trim().describe(PKI_SUBSCRIBERS.GET_LATEST_CERT_BUNDLE.privateKey),
|
||||
serialNumber: z.string().trim().describe(PKI_SUBSCRIBERS.GET_LATEST_CERT_BUNDLE.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req, reply) => {
|
||||
const { certificate, certificateChain, serialNumber, cert, privateKey, subscriber } =
|
||||
await server.services.pkiSubscriber.getSubscriberActiveCertBundle({
|
||||
subscriberName: req.params.subscriberName,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: cert.projectId,
|
||||
event: {
|
||||
type: EventType.GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE,
|
||||
metadata: {
|
||||
subscriberId: subscriber.id,
|
||||
name: subscriber.name,
|
||||
certId: cert.id,
|
||||
serialNumber: cert.serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addNoCacheHeaders(reply);
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
serialNumber,
|
||||
privateKey
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:subscriberName/certificates",
|
||||
|
@ -62,9 +62,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
}),
|
||||
body: z.object({
|
||||
hashedHex: z.string().min(1).optional(),
|
||||
password: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
hash: z.string().optional()
|
||||
password: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -91,8 +89,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
hashedHex: req.body.hashedHex,
|
||||
password: req.body.password,
|
||||
orgId: req.permission?.orgId,
|
||||
email: req.body.email,
|
||||
hash: req.body.hash
|
||||
actorId: req.permission?.id
|
||||
});
|
||||
|
||||
if (sharedSecret.secret?.orgId) {
|
||||
@ -156,7 +153,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
expiresAt: z.string(),
|
||||
expiresAfterViews: z.number().min(1).optional(),
|
||||
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization),
|
||||
emails: z.string().email().array().max(100).optional()
|
||||
emails: z
|
||||
.string()
|
||||
.email()
|
||||
.array()
|
||||
.max(100)
|
||||
.optional()
|
||||
.transform((val) => (val ? [...new Set(val)] : undefined))
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
71
backend/src/server/routes/v2/certificate-authority-router.ts
Normal file
71
backend/src/server/routes/v2/certificate-authority-router.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { AcmeCertificateAuthoritySchema } from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas";
|
||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { InternalCertificateAuthoritySchema } from "@app/services/certificate-authority/internal/internal-certificate-authority-schemas";
|
||||
|
||||
const CertificateAuthoritySchema = z.discriminatedUnion("type", [
|
||||
InternalCertificateAuthoritySchema,
|
||||
AcmeCertificateAuthoritySchema
|
||||
]);
|
||||
|
||||
export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
description: "Get Certificate Authorities",
|
||||
querystring: z.object({
|
||||
projectId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificateAuthorities: CertificateAuthoritySchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const internalCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId(
|
||||
{
|
||||
projectId: req.query.projectId,
|
||||
type: CaType.INTERNAL
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
const acmeCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId(
|
||||
{
|
||||
projectId: req.query.projectId,
|
||||
type: CaType.ACME
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CAS,
|
||||
metadata: {
|
||||
caIds: [...(internalCas ?? []).map((ca) => ca.id), ...(acmeCas ?? []).map((ca) => ca.id)]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificateAuthorities: [...(internalCas ?? []), ...(acmeCas ?? [])]
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { registerGroupProjectRouter } from "./group-project-router";
|
||||
import { registerIdentityOrgRouter } from "./identity-org-router";
|
||||
import { registerIdentityProjectRouter } from "./identity-project-router";
|
||||
@ -14,6 +15,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerUserRouter, { prefix: "/users" });
|
||||
await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
|
||||
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||
await server.register(registerCaRouter, { prefix: "/pki/ca" });
|
||||
await server.register(
|
||||
async (orgRouter) => {
|
||||
await orgRouter.register(registerOrgRouter);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
CertificateAuthoritiesSchema,
|
||||
CertificatesSchema,
|
||||
PkiAlertsSchema,
|
||||
PkiCollectionsSchema,
|
||||
@ -22,13 +21,13 @@ import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
||||
import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema";
|
||||
import { ProjectFilterType } from "@app/services/project/project-types";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||
import { InternalCertificateAuthorityResponseSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||
|
||||
const projectWithEnv = SanitizedProjectSchema.extend({
|
||||
_id: z.string(),
|
||||
@ -385,7 +384,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
cas: z.array(CertificateAuthoritiesSchema)
|
||||
cas: z.array(InternalCertificateAuthorityResponseSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
@ -0,0 +1,3 @@
|
||||
export enum AcmeDnsProvider {
|
||||
Route53 = "route53"
|
||||
}
|
@ -0,0 +1,521 @@
|
||||
import { ChangeResourceRecordSetsCommand, Route53Client } from "@aws-sdk/client-route-53";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import acme from "acme-client";
|
||||
import { KeyObject } from "crypto";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import { TAwsConnection, TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertKeyAlgorithm,
|
||||
CertKeyUsage,
|
||||
CertStatus
|
||||
} from "@app/services/certificate/certificate-types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal";
|
||||
import { CaStatus, CaType } from "../certificate-authority-enums";
|
||||
import { keyAlgorithmToAlgCfg } from "../certificate-authority-fns";
|
||||
import { TExternalCertificateAuthorityDALFactory } from "../external-certificate-authority-dal";
|
||||
import { AcmeDnsProvider } from "./acme-certificate-authority-enums";
|
||||
import { AcmeCertificateAuthorityCredentialsSchema } from "./acme-certificate-authority-schemas";
|
||||
import {
|
||||
TAcmeCertificateAuthority,
|
||||
TCreateAcmeCertificateAuthorityDTO,
|
||||
TUpdateAcmeCertificateAuthorityDTO
|
||||
} from "./acme-certificate-authority-types";
|
||||
|
||||
type TAcmeCertificateAuthorityFnsDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
certificateAuthorityDAL: Pick<
|
||||
TCertificateAuthorityDALFactory,
|
||||
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa"
|
||||
>;
|
||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||
kmsService: Pick<
|
||||
TKmsServiceFactory,
|
||||
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
|
||||
>;
|
||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
|
||||
};
|
||||
|
||||
type DBConfigurationColumn = {
|
||||
dnsProvider: string;
|
||||
directoryUrl: string;
|
||||
accountEmail: string;
|
||||
hostedZoneId: string;
|
||||
};
|
||||
|
||||
export const castDbEntryToAcmeCertificateAuthority = (
|
||||
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
||||
): TAcmeCertificateAuthority & { credentials: unknown } => {
|
||||
if (!ca.externalCa?.id) {
|
||||
throw new BadRequestError({ message: "Malformed ACME certificate authority" });
|
||||
}
|
||||
|
||||
const dbConfigurationCol = ca.externalCa.configuration as DBConfigurationColumn;
|
||||
|
||||
return {
|
||||
id: ca.id,
|
||||
type: CaType.ACME,
|
||||
enableDirectIssuance: ca.enableDirectIssuance,
|
||||
name: ca.name,
|
||||
projectId: ca.projectId,
|
||||
credentials: ca.externalCa.credentials,
|
||||
configuration: {
|
||||
dnsAppConnectionId: ca.externalCa.dnsAppConnectionId as string,
|
||||
dnsProviderConfig: {
|
||||
provider: dbConfigurationCol.dnsProvider as AcmeDnsProvider,
|
||||
hostedZoneId: dbConfigurationCol.hostedZoneId
|
||||
},
|
||||
directoryUrl: dbConfigurationCol.directoryUrl,
|
||||
accountEmail: dbConfigurationCol.accountEmail
|
||||
},
|
||||
status: ca.status as CaStatus
|
||||
};
|
||||
};
|
||||
|
||||
export const route53InsertTxtRecord = async (
|
||||
connection: TAwsConnectionConfig,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
|
||||
const route53Client = new Route53Client({
|
||||
credentials: config.credentials!,
|
||||
region: config.region
|
||||
});
|
||||
|
||||
const command = new ChangeResourceRecordSetsCommand({
|
||||
HostedZoneId: hostedZoneId,
|
||||
ChangeBatch: {
|
||||
Comment: "Set ACME challenge TXT record",
|
||||
Changes: [
|
||||
{
|
||||
Action: "UPSERT",
|
||||
ResourceRecordSet: {
|
||||
Name: domain,
|
||||
Type: "TXT",
|
||||
TTL: 30,
|
||||
ResourceRecords: [{ Value: value }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
await route53Client.send(command);
|
||||
};
|
||||
|
||||
export const route53DeleteTxtRecord = async (
|
||||
connection: TAwsConnectionConfig,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
|
||||
const route53Client = new Route53Client({
|
||||
credentials: config.credentials!,
|
||||
region: config.region
|
||||
});
|
||||
|
||||
const command = new ChangeResourceRecordSetsCommand({
|
||||
HostedZoneId: hostedZoneId,
|
||||
ChangeBatch: {
|
||||
Comment: "Delete ACME challenge TXT record",
|
||||
Changes: [
|
||||
{
|
||||
Action: "DELETE",
|
||||
ResourceRecordSet: {
|
||||
Name: domain,
|
||||
Type: "TXT",
|
||||
TTL: 30,
|
||||
ResourceRecords: [{ Value: value }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
await route53Client.send(command);
|
||||
};
|
||||
|
||||
export const AcmeCertificateAuthorityFns = ({
|
||||
appConnectionDAL,
|
||||
appConnectionService,
|
||||
certificateAuthorityDAL,
|
||||
externalCertificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
projectDAL,
|
||||
pkiSubscriberDAL
|
||||
}: TAcmeCertificateAuthorityFnsDeps) => {
|
||||
const createCertificateAuthority = async ({
|
||||
name,
|
||||
projectId,
|
||||
configuration,
|
||||
enableDirectIssuance,
|
||||
actor,
|
||||
status
|
||||
}: {
|
||||
status: CaStatus;
|
||||
name: string;
|
||||
projectId: string;
|
||||
configuration: TCreateAcmeCertificateAuthorityDTO["configuration"];
|
||||
enableDirectIssuance: boolean;
|
||||
actor: OrgServiceActor;
|
||||
}) => {
|
||||
const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig } = configuration;
|
||||
const appConnection = await appConnectionDAL.findById(dnsAppConnectionId);
|
||||
|
||||
if (!appConnection) {
|
||||
throw new NotFoundError({ message: `App connection with ID '${dnsAppConnectionId}' not found` });
|
||||
}
|
||||
|
||||
if (dnsProviderConfig.provider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) {
|
||||
throw new BadRequestError({
|
||||
message: `App connection with ID '${dnsAppConnectionId}' is not an AWS connection`
|
||||
});
|
||||
}
|
||||
|
||||
// validates permission to connect
|
||||
await appConnectionService.connectAppConnectionById(appConnection.app as AppConnection, dnsAppConnectionId, actor);
|
||||
|
||||
const caEntity = await certificateAuthorityDAL.transaction(async (tx) => {
|
||||
try {
|
||||
const ca = await certificateAuthorityDAL.create(
|
||||
{
|
||||
projectId,
|
||||
enableDirectIssuance,
|
||||
name,
|
||||
status
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await externalCertificateAuthorityDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
dnsAppConnectionId,
|
||||
type: CaType.ACME,
|
||||
configuration: {
|
||||
directoryUrl,
|
||||
accountEmail,
|
||||
dnsProvider: dnsProviderConfig.provider,
|
||||
hostedZoneId: dnsProviderConfig.hostedZoneId
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return await certificateAuthorityDAL.findByIdWithAssociatedCa(ca.id, tx);
|
||||
} catch (error) {
|
||||
// @ts-expect-error We're expecting a database error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (error?.error?.code === "23505") {
|
||||
throw new BadRequestError({
|
||||
message: "Certificate authority with the same name already exists in your project"
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
if (!caEntity.externalCa?.id) {
|
||||
throw new BadRequestError({ message: "Failed to create external certificate authority" });
|
||||
}
|
||||
|
||||
return castDbEntryToAcmeCertificateAuthority(caEntity);
|
||||
};
|
||||
|
||||
const updateCertificateAuthority = async ({
|
||||
id,
|
||||
status,
|
||||
configuration,
|
||||
enableDirectIssuance,
|
||||
actor,
|
||||
name
|
||||
}: {
|
||||
id: string;
|
||||
status?: CaStatus;
|
||||
configuration: TUpdateAcmeCertificateAuthorityDTO["configuration"];
|
||||
enableDirectIssuance?: boolean;
|
||||
actor: OrgServiceActor;
|
||||
name?: string;
|
||||
}) => {
|
||||
const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => {
|
||||
if (configuration) {
|
||||
const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig } = configuration;
|
||||
const appConnection = await appConnectionDAL.findById(dnsAppConnectionId);
|
||||
|
||||
if (!appConnection) {
|
||||
throw new NotFoundError({ message: `App connection with ID '${dnsAppConnectionId}' not found` });
|
||||
}
|
||||
|
||||
if (dnsProviderConfig.provider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) {
|
||||
throw new BadRequestError({
|
||||
message: `App connection with ID '${dnsAppConnectionId}' is not an AWS connection`
|
||||
});
|
||||
}
|
||||
|
||||
// validates permission to connect
|
||||
await appConnectionService.connectAppConnectionById(
|
||||
appConnection.app as AppConnection,
|
||||
dnsAppConnectionId,
|
||||
actor
|
||||
);
|
||||
|
||||
await externalCertificateAuthorityDAL.update(
|
||||
{
|
||||
caId: id,
|
||||
type: CaType.ACME
|
||||
},
|
||||
{
|
||||
dnsAppConnectionId,
|
||||
configuration: {
|
||||
directoryUrl,
|
||||
accountEmail,
|
||||
dnsProvider: dnsProviderConfig.provider,
|
||||
hostedZoneId: dnsProviderConfig.hostedZoneId
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (name || status || enableDirectIssuance) {
|
||||
await certificateAuthorityDAL.updateById(
|
||||
id,
|
||||
{
|
||||
name,
|
||||
status,
|
||||
enableDirectIssuance
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return certificateAuthorityDAL.findByIdWithAssociatedCa(id, tx);
|
||||
});
|
||||
|
||||
if (!updatedCa.externalCa?.id) {
|
||||
throw new BadRequestError({ message: "Failed to update external certificate authority" });
|
||||
}
|
||||
|
||||
return castDbEntryToAcmeCertificateAuthority(updatedCa);
|
||||
};
|
||||
|
||||
const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => {
|
||||
const cas = await certificateAuthorityDAL.findWithAssociatedCa({
|
||||
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
|
||||
[`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.ACME
|
||||
});
|
||||
|
||||
return cas.map(castDbEntryToAcmeCertificateAuthority);
|
||||
};
|
||||
|
||||
const orderSubscriberCertificate = async (subscriberId: string) => {
|
||||
const subscriber = await pkiSubscriberDAL.findById(subscriberId);
|
||||
if (!subscriber.caId) {
|
||||
throw new BadRequestError({ message: "Subscriber does not have a CA" });
|
||||
}
|
||||
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
||||
if (!ca.externalCa || ca.externalCa.type !== CaType.ACME) {
|
||||
throw new BadRequestError({ message: "CA is not an ACME CA" });
|
||||
}
|
||||
|
||||
const acmeCa = castDbEntryToAcmeCertificateAuthority(ca);
|
||||
if (acmeCa.status !== CaStatus.ACTIVE) {
|
||||
throw new BadRequestError({ message: "CA is disabled" });
|
||||
}
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
let accountKey: Buffer | undefined;
|
||||
if (acmeCa.credentials) {
|
||||
const decryptedCredentials = await kmsDecryptor({
|
||||
cipherTextBlob: acmeCa.credentials as Buffer
|
||||
});
|
||||
|
||||
const parsedCredentials = await AcmeCertificateAuthorityCredentialsSchema.parseAsync(
|
||||
JSON.parse(decryptedCredentials.toString("utf8"))
|
||||
);
|
||||
|
||||
accountKey = Buffer.from(parsedCredentials.accountKey, "base64");
|
||||
}
|
||||
if (!accountKey) {
|
||||
accountKey = await acme.crypto.createPrivateRsaKey();
|
||||
const newCredentials = {
|
||||
accountKey: accountKey.toString("base64")
|
||||
};
|
||||
const { cipherTextBlob: encryptedNewCredentials } = await kmsEncryptor({
|
||||
plainText: Buffer.from(JSON.stringify(newCredentials))
|
||||
});
|
||||
await externalCertificateAuthorityDAL.update(
|
||||
{
|
||||
caId: acmeCa.id
|
||||
},
|
||||
{
|
||||
credentials: encryptedNewCredentials
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(acmeCa.configuration.directoryUrl);
|
||||
|
||||
const acmeClient = new acme.Client({
|
||||
directoryUrl: acmeCa.configuration.directoryUrl,
|
||||
accountKey
|
||||
});
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||
|
||||
const [, certificateCsr] = await acme.crypto.createCsr(
|
||||
{
|
||||
altNames: subscriber.subjectAlternativeNames,
|
||||
commonName: subscriber.commonName
|
||||
},
|
||||
skLeaf
|
||||
);
|
||||
|
||||
const appConnection = await appConnectionDAL.findById(acmeCa.configuration.dnsAppConnectionId);
|
||||
const connection = await decryptAppConnection(appConnection, kmsService);
|
||||
|
||||
const pem = await acmeClient.auto({
|
||||
csr: certificateCsr,
|
||||
email: acmeCa.configuration.accountEmail,
|
||||
challengePriority: ["dns-01"],
|
||||
termsOfServiceAgreed: true,
|
||||
|
||||
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
|
||||
if (challenge.type !== "dns-01") {
|
||||
throw new Error("Unsupported challenge type");
|
||||
}
|
||||
|
||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||
|
||||
if (acmeCa.configuration.dnsProviderConfig.provider === AcmeDnsProvider.Route53) {
|
||||
await route53InsertTxtRecord(
|
||||
connection as TAwsConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
}
|
||||
},
|
||||
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||
|
||||
if (acmeCa.configuration.dnsProviderConfig.provider === AcmeDnsProvider.Route53) {
|
||||
await route53DeleteTxtRecord(
|
||||
connection as TAwsConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const [leafCert, parentCert] = acme.crypto.splitPemChain(pem);
|
||||
const certObj = new x509.X509Certificate(leafCert);
|
||||
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(certObj.rawData))
|
||||
});
|
||||
|
||||
const certificateChainPem = parentCert.trim();
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(certificateChainPem)
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||
plainText: Buffer.from(skLeaf)
|
||||
});
|
||||
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
pkiSubscriberId: subscriber.id,
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: subscriber.commonName,
|
||||
commonName: subscriber.commonName,
|
||||
altNames: subscriber.subjectAlternativeNames.join(","),
|
||||
serialNumber: certObj.serialNumber,
|
||||
notBefore: certObj.notBefore,
|
||||
notAfter: certObj.notAfter,
|
||||
keyUsages: subscriber.keyUsages as CertKeyUsage[],
|
||||
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[],
|
||||
projectId: ca.projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateBodyDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateSecretDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedPrivateKey
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createCertificateAuthority,
|
||||
updateCertificateAuthority,
|
||||
listCertificateAuthorities,
|
||||
orderSubscriberCertificate
|
||||
};
|
||||
};
|
@ -0,0 +1,39 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificateAuthorities } from "@app/lib/api-docs/constants";
|
||||
|
||||
import { CaType } from "../certificate-authority-enums";
|
||||
import {
|
||||
BaseCertificateAuthoritySchema,
|
||||
GenericCreateCertificateAuthorityFieldsSchema,
|
||||
GenericUpdateCertificateAuthorityFieldsSchema
|
||||
} from "../certificate-authority-schemas";
|
||||
import { AcmeDnsProvider } from "./acme-certificate-authority-enums";
|
||||
|
||||
export const AcmeCertificateAuthorityConfigurationSchema = z.object({
|
||||
dnsAppConnectionId: z.string().uuid().trim().describe(CertificateAuthorities.CONFIGURATIONS.ACME.dnsAppConnectionId),
|
||||
// soon, differentiate via the provider property
|
||||
dnsProviderConfig: z.object({
|
||||
provider: z.nativeEnum(AcmeDnsProvider).describe(CertificateAuthorities.CONFIGURATIONS.ACME.provider),
|
||||
hostedZoneId: z.string().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.hostedZoneId)
|
||||
}),
|
||||
directoryUrl: z.string().url().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.directoryUrl),
|
||||
accountEmail: z.string().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.accountEmail)
|
||||
});
|
||||
|
||||
export const AcmeCertificateAuthorityCredentialsSchema = z.object({
|
||||
accountKey: z.string()
|
||||
});
|
||||
|
||||
export const AcmeCertificateAuthoritySchema = BaseCertificateAuthoritySchema.extend({
|
||||
type: z.literal(CaType.ACME),
|
||||
configuration: AcmeCertificateAuthorityConfigurationSchema
|
||||
});
|
||||
|
||||
export const CreateAcmeCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema(CaType.ACME).extend({
|
||||
configuration: AcmeCertificateAuthorityConfigurationSchema
|
||||
});
|
||||
|
||||
export const UpdateAcmeCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(CaType.ACME).extend({
|
||||
configuration: AcmeCertificateAuthorityConfigurationSchema.optional()
|
||||
});
|
@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
AcmeCertificateAuthoritySchema,
|
||||
CreateAcmeCertificateAuthoritySchema,
|
||||
UpdateAcmeCertificateAuthoritySchema
|
||||
} from "./acme-certificate-authority-schemas";
|
||||
|
||||
export type TAcmeCertificateAuthority = z.infer<typeof AcmeCertificateAuthoritySchema>;
|
||||
|
||||
export type TAcmeCertificateAuthorityInput = z.infer<typeof CreateAcmeCertificateAuthoritySchema>;
|
||||
|
||||
export type TCreateAcmeCertificateAuthorityDTO = z.infer<typeof CreateAcmeCertificateAuthoritySchema>;
|
||||
|
||||
export type TUpdateAcmeCertificateAuthorityDTO = z.infer<typeof UpdateAcmeCertificateAuthoritySchema>;
|
@ -1,13 +1,188 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { CertificateAuthoritiesSchema, TableName, TCertificateAuthorities } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { buildFindFilter, ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex";
|
||||
|
||||
export type TCertificateAuthorityDALFactory = ReturnType<typeof certificateAuthorityDALFactory>;
|
||||
|
||||
export type TCertificateAuthorityWithAssociatedCa = Awaited<
|
||||
ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>
|
||||
>;
|
||||
|
||||
export const certificateAuthorityDALFactory = (db: TDbClient) => {
|
||||
const caOrm = ormify(db, TableName.CertificateAuthority);
|
||||
|
||||
const findByNameAndProjectIdWithAssociatedCa = async (caName: string, projectId: string, tx?: Knex) => {
|
||||
const result = await (tx || db.replicaNode())(TableName.CertificateAuthority)
|
||||
.leftJoin(
|
||||
TableName.InternalCertificateAuthority,
|
||||
`${TableName.CertificateAuthority}.id`,
|
||||
`${TableName.InternalCertificateAuthority}.caId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.ExternalCertificateAuthority,
|
||||
`${TableName.CertificateAuthority}.id`,
|
||||
`${TableName.ExternalCertificateAuthority}.caId`
|
||||
)
|
||||
.where(`${TableName.CertificateAuthority}.name`, caName)
|
||||
.where(`${TableName.CertificateAuthority}.projectId`, projectId)
|
||||
.select(selectAllTableCols(TableName.CertificateAuthority))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.InternalCertificateAuthority).as("internalCaId"),
|
||||
db.ref("parentCaId").withSchema(TableName.InternalCertificateAuthority).as("internalParentCaId"),
|
||||
db.ref("type").withSchema(TableName.InternalCertificateAuthority).as("internalType"),
|
||||
db.ref("friendlyName").withSchema(TableName.InternalCertificateAuthority).as("internalFriendlyName"),
|
||||
db.ref("organization").withSchema(TableName.InternalCertificateAuthority).as("internalOrganization"),
|
||||
db.ref("ou").withSchema(TableName.InternalCertificateAuthority).as("internalOu"),
|
||||
db.ref("country").withSchema(TableName.InternalCertificateAuthority).as("internalCountry"),
|
||||
db.ref("province").withSchema(TableName.InternalCertificateAuthority).as("internalProvince"),
|
||||
db.ref("locality").withSchema(TableName.InternalCertificateAuthority).as("internalLocality"),
|
||||
db.ref("commonName").withSchema(TableName.InternalCertificateAuthority).as("internalCommonName"),
|
||||
db.ref("dn").withSchema(TableName.InternalCertificateAuthority).as("internalDn"),
|
||||
db.ref("serialNumber").withSchema(TableName.InternalCertificateAuthority).as("internalSerialNumber"),
|
||||
db.ref("maxPathLength").withSchema(TableName.InternalCertificateAuthority).as("internalMaxPathLength"),
|
||||
db.ref("keyAlgorithm").withSchema(TableName.InternalCertificateAuthority).as("internalKeyAlgorithm"),
|
||||
db.ref("notBefore").withSchema(TableName.InternalCertificateAuthority).as("internalNotBefore"),
|
||||
db.ref("notAfter").withSchema(TableName.InternalCertificateAuthority).as("internalNotAfter"),
|
||||
db.ref("activeCaCertId").withSchema(TableName.InternalCertificateAuthority).as("internalActiveCaCertId")
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ExternalCertificateAuthority).as("externalCaId"),
|
||||
db.ref("type").withSchema(TableName.ExternalCertificateAuthority).as("externalType"),
|
||||
db.ref("configuration").withSchema(TableName.ExternalCertificateAuthority).as("externalConfiguration"),
|
||||
db.ref("credentials").withSchema(TableName.ExternalCertificateAuthority).as("externalCredentials"),
|
||||
db
|
||||
.ref("dnsAppConnectionId")
|
||||
.withSchema(TableName.ExternalCertificateAuthority)
|
||||
.as("externalDnsAppConnectionId"),
|
||||
db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority).as("externalAppConnectionId")
|
||||
)
|
||||
.first();
|
||||
|
||||
const data = {
|
||||
...CertificateAuthoritiesSchema.parse(result),
|
||||
internalCa: result
|
||||
? {
|
||||
id: result.internalCaId,
|
||||
parentCaId: result.internalParentCaId,
|
||||
type: result.internalType,
|
||||
friendlyName: result.internalFriendlyName,
|
||||
organization: result.internalOrganization,
|
||||
ou: result.internalOu,
|
||||
country: result.internalCountry,
|
||||
province: result.internalProvince,
|
||||
locality: result.internalLocality,
|
||||
commonName: result.internalCommonName,
|
||||
dn: result.internalDn,
|
||||
serialNumber: result.internalSerialNumber,
|
||||
maxPathLength: result.internalMaxPathLength,
|
||||
keyAlgorithm: result.internalKeyAlgorithm,
|
||||
notBefore: result.internalNotBefore?.toISOString(),
|
||||
notAfter: result.internalNotAfter?.toISOString(),
|
||||
activeCaCertId: result.internalActiveCaCertId
|
||||
}
|
||||
: undefined,
|
||||
externalCa: result
|
||||
? {
|
||||
id: result.externalCaId,
|
||||
type: result.externalType,
|
||||
configuration: result.externalConfiguration,
|
||||
dnsAppConnectionId: result.externalDnsAppConnectionId,
|
||||
appConnectionId: result.externalAppConnectionId,
|
||||
credentials: result.externalCredentials
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const findByIdWithAssociatedCa = async (caId: string, tx?: Knex) => {
|
||||
const result = await (tx || db.replicaNode())(TableName.CertificateAuthority)
|
||||
.leftJoin(
|
||||
TableName.InternalCertificateAuthority,
|
||||
`${TableName.CertificateAuthority}.id`,
|
||||
`${TableName.InternalCertificateAuthority}.caId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.ExternalCertificateAuthority,
|
||||
`${TableName.CertificateAuthority}.id`,
|
||||
`${TableName.ExternalCertificateAuthority}.caId`
|
||||
)
|
||||
.where(`${TableName.CertificateAuthority}.id`, caId)
|
||||
.select(selectAllTableCols(TableName.CertificateAuthority))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.InternalCertificateAuthority).as("internalCaId"),
|
||||
db.ref("parentCaId").withSchema(TableName.InternalCertificateAuthority).as("internalParentCaId"),
|
||||
db.ref("type").withSchema(TableName.InternalCertificateAuthority).as("internalType"),
|
||||
db.ref("friendlyName").withSchema(TableName.InternalCertificateAuthority).as("internalFriendlyName"),
|
||||
db.ref("organization").withSchema(TableName.InternalCertificateAuthority).as("internalOrganization"),
|
||||
db.ref("ou").withSchema(TableName.InternalCertificateAuthority).as("internalOu"),
|
||||
db.ref("country").withSchema(TableName.InternalCertificateAuthority).as("internalCountry"),
|
||||
db.ref("province").withSchema(TableName.InternalCertificateAuthority).as("internalProvince"),
|
||||
db.ref("locality").withSchema(TableName.InternalCertificateAuthority).as("internalLocality"),
|
||||
db.ref("commonName").withSchema(TableName.InternalCertificateAuthority).as("internalCommonName"),
|
||||
db.ref("dn").withSchema(TableName.InternalCertificateAuthority).as("internalDn"),
|
||||
db.ref("serialNumber").withSchema(TableName.InternalCertificateAuthority).as("internalSerialNumber"),
|
||||
db.ref("maxPathLength").withSchema(TableName.InternalCertificateAuthority).as("internalMaxPathLength"),
|
||||
db.ref("keyAlgorithm").withSchema(TableName.InternalCertificateAuthority).as("internalKeyAlgorithm"),
|
||||
db.ref("notBefore").withSchema(TableName.InternalCertificateAuthority).as("internalNotBefore"),
|
||||
db.ref("notAfter").withSchema(TableName.InternalCertificateAuthority).as("internalNotAfter"),
|
||||
db.ref("activeCaCertId").withSchema(TableName.InternalCertificateAuthority).as("internalActiveCaCertId")
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ExternalCertificateAuthority).as("externalCaId"),
|
||||
db.ref("type").withSchema(TableName.ExternalCertificateAuthority).as("externalType"),
|
||||
db.ref("configuration").withSchema(TableName.ExternalCertificateAuthority).as("externalConfiguration"),
|
||||
db.ref("credentials").withSchema(TableName.ExternalCertificateAuthority).as("externalCredentials"),
|
||||
db
|
||||
.ref("dnsAppConnectionId")
|
||||
.withSchema(TableName.ExternalCertificateAuthority)
|
||||
.as("externalDnsAppConnectionId"),
|
||||
db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority).as("externalAppConnectionId")
|
||||
)
|
||||
.first();
|
||||
|
||||
const data = {
|
||||
...CertificateAuthoritiesSchema.parse(result),
|
||||
internalCa: result
|
||||
? {
|
||||
id: result.internalCaId,
|
||||
parentCaId: result.internalParentCaId,
|
||||
type: result.internalType,
|
||||
friendlyName: result.internalFriendlyName,
|
||||
organization: result.internalOrganization,
|
||||
ou: result.internalOu,
|
||||
country: result.internalCountry,
|
||||
province: result.internalProvince,
|
||||
locality: result.internalLocality,
|
||||
commonName: result.internalCommonName,
|
||||
dn: result.internalDn,
|
||||
serialNumber: result.internalSerialNumber,
|
||||
maxPathLength: result.internalMaxPathLength,
|
||||
keyAlgorithm: result.internalKeyAlgorithm,
|
||||
notBefore: result.internalNotBefore?.toISOString(),
|
||||
notAfter: result.internalNotAfter?.toISOString(),
|
||||
activeCaCertId: result.internalActiveCaCertId
|
||||
}
|
||||
: undefined,
|
||||
externalCa: result
|
||||
? {
|
||||
id: result.externalCaId,
|
||||
type: result.externalType,
|
||||
configuration: result.externalConfiguration,
|
||||
dnsAppConnectionId: result.externalDnsAppConnectionId,
|
||||
appConnectionId: result.externalAppConnectionId,
|
||||
credentials: result.externalCredentials
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// note: not used
|
||||
const buildCertificateChain = async (caId: string) => {
|
||||
try {
|
||||
@ -42,8 +217,113 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findWithAssociatedCa = async (
|
||||
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string },
|
||||
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TCertificateAuthorities> = {},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db.replicaNode())(TableName.CertificateAuthority)
|
||||
.leftJoin(
|
||||
TableName.InternalCertificateAuthority,
|
||||
`${TableName.CertificateAuthority}.id`,
|
||||
`${TableName.InternalCertificateAuthority}.caId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.ExternalCertificateAuthority,
|
||||
`${TableName.CertificateAuthority}.id`,
|
||||
`${TableName.ExternalCertificateAuthority}.caId`
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter(filter))
|
||||
.select(selectAllTableCols(TableName.CertificateAuthority))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.InternalCertificateAuthority).as("internalCaId"),
|
||||
db.ref("parentCaId").withSchema(TableName.InternalCertificateAuthority).as("internalParentCaId"),
|
||||
db.ref("type").withSchema(TableName.InternalCertificateAuthority).as("internalType"),
|
||||
db.ref("friendlyName").withSchema(TableName.InternalCertificateAuthority).as("internalFriendlyName"),
|
||||
db.ref("organization").withSchema(TableName.InternalCertificateAuthority).as("internalOrganization"),
|
||||
db.ref("ou").withSchema(TableName.InternalCertificateAuthority).as("internalOu"),
|
||||
db.ref("country").withSchema(TableName.InternalCertificateAuthority).as("internalCountry"),
|
||||
db.ref("province").withSchema(TableName.InternalCertificateAuthority).as("internalProvince"),
|
||||
db.ref("locality").withSchema(TableName.InternalCertificateAuthority).as("internalLocality"),
|
||||
db.ref("commonName").withSchema(TableName.InternalCertificateAuthority).as("internalCommonName"),
|
||||
db.ref("dn").withSchema(TableName.InternalCertificateAuthority).as("internalDn"),
|
||||
db.ref("serialNumber").withSchema(TableName.InternalCertificateAuthority).as("internalSerialNumber"),
|
||||
db.ref("maxPathLength").withSchema(TableName.InternalCertificateAuthority).as("internalMaxPathLength"),
|
||||
db.ref("keyAlgorithm").withSchema(TableName.InternalCertificateAuthority).as("internalKeyAlgorithm"),
|
||||
db.ref("notBefore").withSchema(TableName.InternalCertificateAuthority).as("internalNotBefore"),
|
||||
db.ref("notAfter").withSchema(TableName.InternalCertificateAuthority).as("internalNotAfter"),
|
||||
db.ref("activeCaCertId").withSchema(TableName.InternalCertificateAuthority).as("internalActiveCaCertId")
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ExternalCertificateAuthority).as("externalCaId"),
|
||||
db.ref("type").withSchema(TableName.ExternalCertificateAuthority).as("externalType"),
|
||||
db.ref("configuration").withSchema(TableName.ExternalCertificateAuthority).as("externalConfiguration"),
|
||||
db
|
||||
.ref("dnsAppConnectionId")
|
||||
.withSchema(TableName.ExternalCertificateAuthority)
|
||||
.as("externalDnsAppConnectionId"),
|
||||
db.ref("credentials").withSchema(TableName.ExternalCertificateAuthority).as("externalCredentials"),
|
||||
db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority).as("externalAppConnectionId")
|
||||
);
|
||||
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
void query.orderBy(
|
||||
sort.map(([column, order, nulls]) => ({
|
||||
column,
|
||||
order,
|
||||
nulls
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return (await query).map((ca) => ({
|
||||
...CertificateAuthoritiesSchema.parse(ca),
|
||||
internalCa: ca
|
||||
? {
|
||||
id: ca.internalCaId,
|
||||
parentCaId: ca.internalParentCaId,
|
||||
type: ca.internalType,
|
||||
friendlyName: ca.internalFriendlyName,
|
||||
organization: ca.internalOrganization,
|
||||
ou: ca.internalOu,
|
||||
country: ca.internalCountry,
|
||||
province: ca.internalProvince,
|
||||
locality: ca.internalLocality,
|
||||
commonName: ca.internalCommonName,
|
||||
dn: ca.internalDn,
|
||||
serialNumber: ca.internalSerialNumber,
|
||||
maxPathLength: ca.internalMaxPathLength,
|
||||
keyAlgorithm: ca.internalKeyAlgorithm,
|
||||
notBefore: ca.internalNotBefore?.toISOString(),
|
||||
notAfter: ca.internalNotAfter?.toISOString(),
|
||||
activeCaCertId: ca.internalActiveCaCertId
|
||||
}
|
||||
: undefined,
|
||||
externalCa: ca
|
||||
? {
|
||||
id: ca.externalCaId,
|
||||
type: ca.externalType,
|
||||
configuration: ca.externalConfiguration,
|
||||
dnsAppConnectionId: ca.externalDnsAppConnectionId,
|
||||
appConnectionId: ca.externalAppConnectionId,
|
||||
credentials: ca.externalCredentials
|
||||
}
|
||||
: undefined
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find - Certificate Authority" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...caOrm,
|
||||
buildCertificateChain
|
||||
findWithAssociatedCa,
|
||||
buildCertificateChain,
|
||||
findByIdWithAssociatedCa,
|
||||
findByNameAndProjectIdWithAssociatedCa
|
||||
};
|
||||
};
|
||||
|
@ -0,0 +1,19 @@
|
||||
export enum CaType {
|
||||
INTERNAL = "internal",
|
||||
ACME = "acme"
|
||||
}
|
||||
|
||||
export enum InternalCaType {
|
||||
ROOT = "root",
|
||||
INTERMEDIATE = "intermediate"
|
||||
}
|
||||
|
||||
export enum CaStatus {
|
||||
ACTIVE = "active",
|
||||
DISABLED = "disabled",
|
||||
PENDING_CERTIFICATE = "pending-certificate"
|
||||
}
|
||||
|
||||
export enum CaRenewalType {
|
||||
EXISTING = "existing"
|
||||
}
|
@ -5,13 +5,14 @@ import { NotFoundError } from "@app/lib/errors";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import {
|
||||
TDNParts,
|
||||
TGetCaCertChainDTO,
|
||||
TGetCaCertChainsDTO,
|
||||
TGetCaCredentialsDTO,
|
||||
TRebuildCaCrlDTO
|
||||
} from "./certificate-authority-types";
|
||||
} from "./internal/internal-certificate-authority-types";
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
export const createSerialNumber = () => {
|
||||
@ -112,8 +113,8 @@ export const getCaCredentials = async ({
|
||||
projectDAL,
|
||||
kmsService
|
||||
}: TGetCaCredentialsDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
|
||||
|
||||
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId });
|
||||
if (!caSecret) throw new NotFoundError({ message: `CA secret for CA with ID '${caId}' not found` });
|
||||
@ -131,7 +132,7 @@ export const getCaCredentials = async ({
|
||||
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
||||
const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" });
|
||||
const caPrivateKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
@ -255,12 +256,12 @@ export const rebuildCaCrl = async ({
|
||||
certificateDAL,
|
||||
kmsService
|
||||
}: TRebuildCaCrlDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
|
||||
|
||||
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
@ -287,7 +288,7 @@ export const rebuildCaCrl = async ({
|
||||
});
|
||||
|
||||
const crl = await x509.X509CrlGenerator.create({
|
||||
issuer: ca.dn,
|
||||
issuer: ca.internalCa.dn,
|
||||
thisUpdate: new Date(),
|
||||
nextUpdate: new Date("2025/12/12"),
|
||||
entries: revokedCerts.map((revokedCert) => {
|
||||
@ -318,3 +319,16 @@ export const rebuildCaCrl = async ({
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const expandInternalCa = (
|
||||
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
||||
) => {
|
||||
if (!ca.internalCa) {
|
||||
throw new Error("Internal CA must be defined");
|
||||
}
|
||||
return {
|
||||
...ca.internalCa,
|
||||
...ca,
|
||||
requireTemplateForIssuance: !ca.enableDirectIssuance
|
||||
} as const;
|
||||
};
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { CaType } from "./certificate-authority-enums";
|
||||
|
||||
export const CERTIFICATE_AUTHORITIES_TYPE_MAP: Record<CaType, string> = {
|
||||
[CaType.INTERNAL]: "Internal",
|
||||
[CaType.ACME]: "ACME"
|
||||
};
|
@ -1,9 +1,10 @@
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import crypto from "crypto";
|
||||
|
||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
@ -13,21 +14,43 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service";
|
||||
import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
|
||||
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
|
||||
import { SubscriberOperationStatus } from "../pki-subscriber/pki-subscriber-types";
|
||||
import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import { CaType } from "./certificate-authority-enums";
|
||||
import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||
import { TRotateCaCrlTriggerDTO } from "./certificate-authority-types";
|
||||
import { TExternalCertificateAuthorityDALFactory } from "./external-certificate-authority-dal";
|
||||
import {
|
||||
TOrderCertificateForSubscriberDTO,
|
||||
TRotateCaCrlTriggerDTO
|
||||
} from "./internal/internal-certificate-authority-types";
|
||||
|
||||
type TCertificateAuthorityQueueFactoryDep = {
|
||||
// TODO: Pick
|
||||
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
||||
certificateAuthorityCrlDAL: TCertificateAuthorityCrlDALFactory;
|
||||
certificateAuthoritySecretDAL: TCertificateAuthoritySecretDALFactory;
|
||||
certificateDAL: TCertificateDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
kmsService: Pick<
|
||||
TKmsServiceFactory,
|
||||
"generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "createCipherPairWithDataKey"
|
||||
>;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||
queueService: TQueueServiceFactory;
|
||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById" | "updateById">;
|
||||
};
|
||||
|
||||
export type TCertificateAuthorityQueueFactory = ReturnType<typeof certificateAuthorityQueueFactory>;
|
||||
|
||||
export const certificateAuthorityQueueFactory = ({
|
||||
@ -37,8 +60,28 @@ export const certificateAuthorityQueueFactory = ({
|
||||
certificateDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
queueService
|
||||
queueService,
|
||||
keyStore,
|
||||
appConnectionDAL,
|
||||
appConnectionService,
|
||||
externalCertificateAuthorityDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
pkiSubscriberDAL
|
||||
}: TCertificateAuthorityQueueFactoryDep) => {
|
||||
const acmeFns = AcmeCertificateAuthorityFns({
|
||||
appConnectionDAL,
|
||||
appConnectionService,
|
||||
certificateAuthorityDAL,
|
||||
externalCertificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
pkiSubscriberDAL,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
// TODO 1: auto-periodic rotation
|
||||
// TODO 2: manual rotation
|
||||
|
||||
@ -71,16 +114,76 @@ export const certificateAuthorityQueueFactory = ({
|
||||
);
|
||||
};
|
||||
|
||||
const orderCertificateForSubscriber = async ({ subscriberId, caType }: TOrderCertificateForSubscriberDTO) => {
|
||||
const entry = await keyStore.getItem(KeyStorePrefixes.CaOrderCertificateForSubscriberLock(subscriberId));
|
||||
if (entry) {
|
||||
throw new BadRequestError({ message: `Certificate order already in progress for subscriber ${subscriberId}` });
|
||||
}
|
||||
|
||||
await queueService.queue(
|
||||
QueueName.CaLifecycle,
|
||||
QueueJobs.CaOrderCertificateForSubscriber,
|
||||
{
|
||||
subscriberId,
|
||||
caType
|
||||
},
|
||||
{
|
||||
attempts: 1,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
queueService.start(QueueName.CaLifecycle, async (job) => {
|
||||
if (job.name === QueueJobs.CaOrderCertificateForSubscriber) {
|
||||
const { subscriberId, caType } = job.data;
|
||||
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>>;
|
||||
|
||||
try {
|
||||
lock = await keyStore.acquireLock(
|
||||
[KeyStorePrefixes.CaOrderCertificateForSubscriberLock(subscriberId)],
|
||||
5 * 60 * 1000
|
||||
);
|
||||
} catch (e) {
|
||||
logger.info(`CaOrderCertificate Failed to acquire lock [subscriberId=${subscriberId}] [job=${job.name}]`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (caType === CaType.ACME) {
|
||||
await acmeFns.orderSubscriberCertificate(subscriberId);
|
||||
await pkiSubscriberDAL.updateById(subscriberId, {
|
||||
lastOperationStatus: SubscriberOperationStatus.SUCCESS,
|
||||
lastOperationMessage: "Certificate ordered successfully",
|
||||
lastOperationAt: new Date()
|
||||
});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
await pkiSubscriberDAL.updateById(subscriberId, {
|
||||
lastOperationStatus: SubscriberOperationStatus.FAILED,
|
||||
lastOperationMessage: e.message,
|
||||
lastOperationAt: new Date()
|
||||
});
|
||||
}
|
||||
logger.error(e, `CaOrderCertificate Failed [subscriberId=${subscriberId}] [job=${job.name}]`);
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
queueService.start(QueueName.CaCrlRotation, async (job) => {
|
||||
const { caId } = job.data;
|
||||
logger.info(`secretReminderQueue.process: [secretDocument=${caId}]`);
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||
if (!ca.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
|
||||
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
@ -106,7 +209,7 @@ export const certificateAuthorityQueueFactory = ({
|
||||
});
|
||||
|
||||
const crl = await x509.X509CrlGenerator.create({
|
||||
issuer: ca.dn,
|
||||
issuer: ca.internalCa.dn,
|
||||
thisUpdate: new Date(),
|
||||
nextUpdate: new Date("2025/12/12"), // TODO: depends on configured rebuild interval
|
||||
entries: revokedCerts.map((revokedCert) => {
|
||||
@ -115,7 +218,7 @@ export const certificateAuthorityQueueFactory = ({
|
||||
revocationDate: new Date(revokedCert.revokedAt as Date),
|
||||
reason: revokedCert.revocationReason as number,
|
||||
invalidity: new Date("2022/01/01"),
|
||||
issuer: ca.dn
|
||||
issuer: ca.internalCa?.dn
|
||||
};
|
||||
}),
|
||||
signingAlgorithm: alg,
|
||||
@ -144,6 +247,7 @@ export const certificateAuthorityQueueFactory = ({
|
||||
});
|
||||
|
||||
return {
|
||||
setCaCrlRotationInterval
|
||||
setCaCrlRotationInterval,
|
||||
orderCertificateForSubscriber
|
||||
};
|
||||
};
|
||||
|
@ -0,0 +1,32 @@
|
||||
import z from "zod";
|
||||
|
||||
import { CertificateAuthoritiesSchema } from "@app/db/schemas";
|
||||
import { CertificateAuthorities } from "@app/lib/api-docs/constants";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
|
||||
import { CaStatus, CaType } from "./certificate-authority-enums";
|
||||
|
||||
export const BaseCertificateAuthoritySchema = CertificateAuthoritiesSchema.pick({
|
||||
projectId: true,
|
||||
enableDirectIssuance: true,
|
||||
name: true,
|
||||
id: true
|
||||
}).extend({
|
||||
status: z.nativeEnum(CaStatus)
|
||||
});
|
||||
|
||||
export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name),
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.CREATE(type).projectId),
|
||||
enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance),
|
||||
status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status)
|
||||
});
|
||||
|
||||
export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name),
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.UPDATE(type).projectId),
|
||||
enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance),
|
||||
status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status)
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -1,186 +1,18 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TAcmeCertificateAuthority, TAcmeCertificateAuthorityInput } from "./acme/acme-certificate-authority-types";
|
||||
import { CaType } from "./certificate-authority-enums";
|
||||
import {
|
||||
TInternalCertificateAuthority,
|
||||
TInternalCertificateAuthorityInput
|
||||
} from "./internal/internal-certificate-authority-types";
|
||||
|
||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificate/certificate-types";
|
||||
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||
export type TCertificateAuthority = TInternalCertificateAuthority | TAcmeCertificateAuthority;
|
||||
|
||||
export enum CaType {
|
||||
ROOT = "root",
|
||||
INTERMEDIATE = "intermediate"
|
||||
}
|
||||
export type TCertificateAuthorityInput = TInternalCertificateAuthorityInput | TAcmeCertificateAuthorityInput;
|
||||
|
||||
export enum CaStatus {
|
||||
ACTIVE = "active",
|
||||
DISABLED = "disabled",
|
||||
PENDING_CERTIFICATE = "pending-certificate"
|
||||
}
|
||||
export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "id">;
|
||||
|
||||
export enum CaRenewalType {
|
||||
EXISTING = "existing"
|
||||
}
|
||||
|
||||
export type TCreateCaDTO = {
|
||||
projectSlug: string;
|
||||
export type TUpdateCertificateAuthorityDTO = Partial<Omit<TCreateCertificateAuthorityDTO, "projectId">> & {
|
||||
type: CaType;
|
||||
friendlyName?: string;
|
||||
commonName: string;
|
||||
organization: string;
|
||||
ou: string;
|
||||
country: string;
|
||||
province: string;
|
||||
locality: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
maxPathLength: number;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
requireTemplateForIssuance: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCaDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateCaDTO = {
|
||||
caId: string;
|
||||
status?: CaStatus;
|
||||
requireTemplateForIssuance?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteCaDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCaCsrDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRenewCaCertDTO = {
|
||||
caId: string;
|
||||
notAfter: string;
|
||||
type: CaRenewalType;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCaCertsDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCaCertDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSignIntermediateDTO = {
|
||||
caId: string;
|
||||
csr: string;
|
||||
notBefore?: string;
|
||||
notAfter: string;
|
||||
maxPathLength: number;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TImportCertToCaDTO = {
|
||||
caId: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIssueCertFromCaDTO = {
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName: string;
|
||||
altNames: string;
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSignCertFromCaDTO =
|
||||
| {
|
||||
isInternal: true;
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames?: string;
|
||||
ttl?: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames: string;
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
||||
export type TGetCaCertificateTemplatesDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDNParts = {
|
||||
commonName?: string;
|
||||
organization?: string;
|
||||
ou?: string;
|
||||
country?: string;
|
||||
province?: string;
|
||||
locality?: string;
|
||||
};
|
||||
|
||||
export type TGetCaCredentialsDTO = {
|
||||
caId: string;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TGetCaCertChainsDTO = {
|
||||
caId: string;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TGetCaCertChainDTO = {
|
||||
caCertId: string;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TRebuildCaCrlDTO = {
|
||||
caId: string;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
|
||||
};
|
||||
|
||||
export type TRotateCaCrlTriggerDTO = {
|
||||
caId: string;
|
||||
rotationIntervalDays: number;
|
||||
caName: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
@ -15,7 +15,7 @@ export const validateAltNameField = z
|
||||
.trim()
|
||||
.refine(
|
||||
(name) => {
|
||||
return isFQDN(name) || z.string().email().safeParse(name).success || isValidIp(name);
|
||||
return isFQDN(name, { allow_wildcard: true }) || z.string().email().safeParse(name).success || isValidIp(name);
|
||||
},
|
||||
{
|
||||
message: "SAN must be a valid hostname, email address, or IP address"
|
||||
|
@ -0,0 +1,13 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TExternalCertificateAuthorityDALFactory = ReturnType<typeof externalCertificateAuthorityDALFactory>;
|
||||
|
||||
export const externalCertificateAuthorityDALFactory = (db: TDbClient) => {
|
||||
const caOrm = ormify(db, TableName.ExternalCertificateAuthority);
|
||||
|
||||
return {
|
||||
...caOrm
|
||||
};
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TInternalCertificateAuthorityDALFactory = ReturnType<typeof internalCertificateAuthorityDALFactory>;
|
||||
|
||||
export const internalCertificateAuthorityDALFactory = (db: TDbClient) => {
|
||||
const caOrm = ormify(db, TableName.InternalCertificateAuthority);
|
||||
|
||||
return {
|
||||
...caOrm
|
||||
};
|
||||
};
|
@ -0,0 +1,263 @@
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import { KeyObject } from "crypto";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TPkiSubscribers } from "@app/db/schemas";
|
||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertKeyAlgorithm,
|
||||
CertKeyUsage,
|
||||
CertStatus
|
||||
} from "@app/services/certificate/certificate-types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal";
|
||||
import { CaStatus } from "../certificate-authority-enums";
|
||||
import {
|
||||
createSerialNumber,
|
||||
getCaCertChain,
|
||||
getCaCredentials,
|
||||
keyAlgorithmToAlgCfg
|
||||
} from "../certificate-authority-fns";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
|
||||
|
||||
type TInternalCertificateAuthorityFnsDeps = {
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "transaction" | "findOne" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "encryptWithKmsKey" | "generateKmsKey">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||
};
|
||||
|
||||
export const InternalCertificateAuthorityFns = ({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL
|
||||
}: TInternalCertificateAuthorityFnsDeps) => {
|
||||
const issueCertificate = async (
|
||||
subscriber: TPkiSubscribers,
|
||||
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
||||
) => {
|
||||
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
|
||||
if (!ca.internalCa?.activeCaCertId)
|
||||
throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId);
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
|
||||
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||
const notBeforeDate = new Date();
|
||||
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl ?? "0"));
|
||||
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||
|
||||
// check not before constraint
|
||||
if (notBeforeDate < caCertNotBeforeDate) {
|
||||
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||
}
|
||||
|
||||
// check not after constraint
|
||||
if (notAfterDate > caCertNotAfterDate) {
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
||||
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
|
||||
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||
name: `CN=${subscriber.commonName}`,
|
||||
keys: leafKeys,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
// eslint-disable-next-line no-bitwise
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
|
||||
],
|
||||
attributes: [new x509.ChallengePasswordAttribute("password")]
|
||||
});
|
||||
|
||||
const { caPrivateKey, caSecret } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const appCfg = getConfig();
|
||||
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
|
||||
new x509.AuthorityInfoAccessExtension({
|
||||
caIssuers: new x509.GeneralName("url", caIssuerUrl)
|
||||
}),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
|
||||
];
|
||||
|
||||
const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[];
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
|
||||
if (keyUsagesBitValue) {
|
||||
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
|
||||
}
|
||||
|
||||
if (subscriber.extendedKeyUsages.length) {
|
||||
const extendedKeyUsagesExtension = new x509.ExtendedKeyUsageExtension(
|
||||
subscriber.extendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku as CertExtendedKeyUsage]),
|
||||
true
|
||||
);
|
||||
extensions.push(extendedKeyUsagesExtension);
|
||||
}
|
||||
|
||||
let altNamesArray: { type: "email" | "dns"; value: string }[] = [];
|
||||
|
||||
if (subscriber.subjectAlternativeNames?.length) {
|
||||
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
|
||||
if (z.string().email().safeParse(altName).success) {
|
||||
return { type: "email", value: altName };
|
||||
}
|
||||
|
||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||
return { type: "dns", value: altName };
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
|
||||
});
|
||||
|
||||
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||
extensions.push(altNamesExtension);
|
||||
}
|
||||
|
||||
const serialNumber = createSerialNumber();
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: csrObj.subject,
|
||||
issuer: caCertObj.subject,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
signingKey: caPrivateKey,
|
||||
publicKey: csrObj.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions
|
||||
});
|
||||
|
||||
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||
});
|
||||
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||
plainText: Buffer.from(skLeaf)
|
||||
});
|
||||
|
||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||
caCertId: caCert.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(certificateChainPem)
|
||||
});
|
||||
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
caCertId: caCert.id,
|
||||
pkiSubscriberId: subscriber.id,
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: subscriber.commonName,
|
||||
commonName: subscriber.commonName,
|
||||
altNames: subscriber.subjectAlternativeNames.join(","),
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
keyUsages: selectedKeyUsages,
|
||||
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[],
|
||||
projectId: ca.projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateBodyDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateSecretDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedPrivateKey
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: leafCert.toString("pem"),
|
||||
certificateChain: certificateChainPem,
|
||||
issuingCaCertificate,
|
||||
privateKey: skLeaf,
|
||||
serialNumber,
|
||||
ca,
|
||||
subscriber
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
issueCertificate
|
||||
};
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificateAuthorities } from "@app/lib/api-docs/constants";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
|
||||
import { CaType, InternalCaType } from "../certificate-authority-enums";
|
||||
import {
|
||||
BaseCertificateAuthoritySchema,
|
||||
GenericCreateCertificateAuthorityFieldsSchema,
|
||||
GenericUpdateCertificateAuthorityFieldsSchema
|
||||
} from "../certificate-authority-schemas";
|
||||
import { validateCaDateField } from "../certificate-authority-validators";
|
||||
|
||||
const InternalCertificateAuthorityConfigurationSchema = z
|
||||
.object({
|
||||
type: z.nativeEnum(InternalCaType).describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.type),
|
||||
friendlyName: z.string().optional().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.friendlyName),
|
||||
commonName: z.string().trim().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.commonName),
|
||||
organization: z.string().trim().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.organization),
|
||||
ou: z.string().trim().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.ou),
|
||||
country: z.string().trim().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.country),
|
||||
province: z.string().trim().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.province),
|
||||
locality: z.string().trim().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.locality),
|
||||
notBefore: validateCaDateField.optional().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.notBefore),
|
||||
notAfter: validateCaDateField.optional().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.notAfter),
|
||||
maxPathLength: z.number().min(-1).nullish().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.maxPathLength),
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.keyAlgorithm),
|
||||
dn: z.string().trim().nullish(),
|
||||
parentCaId: z.string().uuid().nullish(),
|
||||
serialNumber: z.string().trim().nullish(),
|
||||
activeCaCertId: z.string().uuid().nullish()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// Check that at least one of the specified fields is non-empty
|
||||
return [data.commonName, data.organization, data.ou, data.country, data.province, data.locality].some(
|
||||
(field) => field !== ""
|
||||
);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"At least one of the fields commonName, organization, ou, country, province, or locality must be non-empty",
|
||||
path: []
|
||||
}
|
||||
);
|
||||
|
||||
export const InternalCertificateAuthoritySchema = BaseCertificateAuthoritySchema.extend({
|
||||
type: z.literal(CaType.INTERNAL),
|
||||
configuration: InternalCertificateAuthorityConfigurationSchema
|
||||
});
|
||||
|
||||
export const CreateInternalCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema(
|
||||
CaType.INTERNAL
|
||||
).extend({
|
||||
configuration: InternalCertificateAuthorityConfigurationSchema
|
||||
});
|
||||
|
||||
export const UpdateInternalCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(
|
||||
CaType.INTERNAL
|
||||
).extend({
|
||||
configuration: InternalCertificateAuthorityConfigurationSchema.optional()
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,223 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal";
|
||||
import { CaRenewalType, CaStatus, CaType, InternalCaType } from "../certificate-authority-enums";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
|
||||
import {
|
||||
CreateInternalCertificateAuthoritySchema,
|
||||
InternalCertificateAuthoritySchema,
|
||||
UpdateInternalCertificateAuthoritySchema
|
||||
} from "./internal-certificate-authority-schemas";
|
||||
|
||||
export type TInternalCertificateAuthority = z.infer<typeof InternalCertificateAuthoritySchema>;
|
||||
|
||||
export type TInternalCertificateAuthorityInput = z.infer<typeof CreateInternalCertificateAuthoritySchema>;
|
||||
|
||||
export type TCreateInternalCertificateAuthorityDTO = z.infer<typeof CreateInternalCertificateAuthoritySchema>;
|
||||
|
||||
export type TUpdateInternalCertificateAuthorityDTO = z.infer<typeof UpdateInternalCertificateAuthoritySchema>;
|
||||
|
||||
export type TCreateCaDTO =
|
||||
| {
|
||||
isInternal: true;
|
||||
projectId: string;
|
||||
type: InternalCaType;
|
||||
friendlyName?: string;
|
||||
name?: string;
|
||||
commonName: string;
|
||||
organization: string;
|
||||
ou: string;
|
||||
country: string;
|
||||
province: string;
|
||||
locality: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
maxPathLength?: number | null;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
enableDirectIssuance: boolean;
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
projectSlug: string;
|
||||
type: InternalCaType;
|
||||
friendlyName?: string;
|
||||
name?: string;
|
||||
commonName: string;
|
||||
organization: string;
|
||||
ou: string;
|
||||
country: string;
|
||||
province: string;
|
||||
locality: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
maxPathLength?: number | null;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
enableDirectIssuance: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
||||
export type TGetCaDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateCaDTO =
|
||||
| {
|
||||
isInternal: true;
|
||||
caId: string;
|
||||
name?: string;
|
||||
status?: CaStatus;
|
||||
enableDirectIssuance?: boolean;
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
caId: string;
|
||||
name?: string;
|
||||
status?: CaStatus;
|
||||
enableDirectIssuance?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
||||
export type TDeleteCaDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCaCsrDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRenewCaCertDTO = {
|
||||
caId: string;
|
||||
notAfter: string;
|
||||
type: CaRenewalType;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCaCertsDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCaCertDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSignIntermediateDTO = {
|
||||
caId: string;
|
||||
csr: string;
|
||||
notBefore?: string;
|
||||
notAfter: string;
|
||||
maxPathLength: number;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TImportCertToCaDTO = {
|
||||
caId: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIssueCertFromCaDTO = {
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName: string;
|
||||
altNames: string;
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSignCertFromCaDTO =
|
||||
| {
|
||||
isInternal: true;
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames?: string;
|
||||
ttl?: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames: string;
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
||||
export type TGetCaCertificateTemplatesDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDNParts = {
|
||||
commonName?: string;
|
||||
organization?: string;
|
||||
ou?: string;
|
||||
country?: string;
|
||||
province?: string;
|
||||
locality?: string;
|
||||
};
|
||||
|
||||
export type TGetCaCredentialsDTO = {
|
||||
caId: string;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TGetCaCertChainsDTO = {
|
||||
caId: string;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TGetCaCertChainDTO = {
|
||||
caCertId: string;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TRebuildCaCrlDTO = {
|
||||
caId: string;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
|
||||
};
|
||||
|
||||
export type TRotateCaCrlTriggerDTO = {
|
||||
caId: string;
|
||||
rotationIntervalDays: number;
|
||||
};
|
||||
|
||||
export type TOrderCertificateForSubscriberDTO = {
|
||||
subscriberId: string;
|
||||
caType: CaType;
|
||||
};
|
@ -19,10 +19,15 @@ export const certificateTemplateDALFactory = (db: TDbClient) => {
|
||||
`${TableName.CertificateAuthority}.id`,
|
||||
`${TableName.CertificateTemplate}.caId`
|
||||
)
|
||||
.join(
|
||||
TableName.InternalCertificateAuthority,
|
||||
`${TableName.InternalCertificateAuthority}.caId`,
|
||||
`${TableName.CertificateAuthority}.id`
|
||||
)
|
||||
.where(`${TableName.CertificateAuthority}.projectId`, "=", projectId)
|
||||
.select(selectAllTableCols(TableName.CertificateTemplate))
|
||||
.select(
|
||||
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority),
|
||||
db.ref("friendlyName").as("caName").withSchema(TableName.InternalCertificateAuthority),
|
||||
db.ref("projectId").withSchema(TableName.CertificateAuthority)
|
||||
);
|
||||
|
||||
@ -41,11 +46,16 @@ export const certificateTemplateDALFactory = (db: TDbClient) => {
|
||||
`${TableName.CertificateTemplate}.caId`
|
||||
)
|
||||
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.CertificateAuthority}.projectId`)
|
||||
.join(
|
||||
TableName.InternalCertificateAuthority,
|
||||
`${TableName.InternalCertificateAuthority}.caId`,
|
||||
`${TableName.CertificateAuthority}.id`
|
||||
)
|
||||
.where(`${TableName.CertificateTemplate}.id`, "=", id)
|
||||
.select(selectAllTableCols(TableName.CertificateTemplate))
|
||||
.select(
|
||||
db.ref("projectId").withSchema(TableName.CertificateAuthority),
|
||||
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority),
|
||||
db.ref("friendlyName").as("caName").withSchema(TableName.InternalCertificateAuthority),
|
||||
db.ref("orgId").withSchema(TableName.Project)
|
||||
)
|
||||
.first();
|
||||
|
@ -3,11 +3,28 @@ import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
import { CertStatus } from "./certificate-types";
|
||||
|
||||
export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;
|
||||
|
||||
export const certificateDALFactory = (db: TDbClient) => {
|
||||
const certificateOrm = ormify(db, TableName.Certificate);
|
||||
|
||||
const findLatestActiveCertForSubscriber = async ({ subscriberId }: { subscriberId: string }) => {
|
||||
try {
|
||||
const cert = await db
|
||||
.replicaNode()(TableName.Certificate)
|
||||
.where({ pkiSubscriberId: subscriberId, status: CertStatus.ACTIVE })
|
||||
.where("notAfter", ">", new Date())
|
||||
.orderBy("notBefore", "desc")
|
||||
.first();
|
||||
|
||||
return cert;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find latest active certificate for subscriber" });
|
||||
}
|
||||
};
|
||||
|
||||
const countCertificatesInProject = async ({
|
||||
projectId,
|
||||
friendlyName,
|
||||
@ -65,6 +82,7 @@ export const certificateDALFactory = (db: TDbClient) => {
|
||||
return {
|
||||
...certificateOrm,
|
||||
countCertificatesInProject,
|
||||
countCertificatesForPkiSubscriber
|
||||
countCertificatesForPkiSubscriber,
|
||||
findLatestActiveCertForSubscriber
|
||||
};
|
||||
};
|
||||
|
@ -1,11 +1,12 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import RE2 from "re2";
|
||||
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
|
||||
import { CrlReason, TBuildCertificateChainDTO, TGetCertificateCredentialsDTO } from "./certificate-types";
|
||||
import { CrlReason, TGetCertificateCredentialsDTO } from "./certificate-types";
|
||||
|
||||
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
||||
switch (crlReason) {
|
||||
@ -52,6 +53,12 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
export const splitPemChain = (pemText: string) => {
|
||||
const re2Pattern = new RE2("-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----", "g");
|
||||
|
||||
return re2Pattern.match(pemText) || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the public and private key of certificate
|
||||
* Note: credentials are returned as PEM strings
|
||||
@ -95,29 +102,3 @@ export const getCertificateCredentials = async ({
|
||||
throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` });
|
||||
}
|
||||
};
|
||||
|
||||
// If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body
|
||||
// Otherwise we'll fallback to manually building the chain
|
||||
export const buildCertificateChain = async ({
|
||||
caCert,
|
||||
caCertChain,
|
||||
encryptedCertificateChain,
|
||||
kmsService,
|
||||
kmsId
|
||||
}: TBuildCertificateChainDTO) => {
|
||||
if (!encryptedCertificateChain && !caCert) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let certificateChain = `${caCert}\n${caCertChain}`.trim();
|
||||
|
||||
if (encryptedCertificateChain) {
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId });
|
||||
const decryptedCertChain = await kmsDecryptor({
|
||||
cipherTextBlob: encryptedCertificateChain
|
||||
});
|
||||
certificateChain = decryptedCertChain.toString();
|
||||
}
|
||||
|
||||
return certificateChain;
|
||||
};
|
||||
|
@ -1,45 +1,57 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import { createPrivateKey, createPublicKey, sign, verify } from "crypto";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
|
||||
import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns";
|
||||
import { expandInternalCa, getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
|
||||
import { getCertificateCredentials, revocationReasonToCrlCode, splitPemChain } from "./certificate-fns";
|
||||
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertExtendedKeyUsageOIDToName,
|
||||
CertKeyUsage,
|
||||
CertStatus,
|
||||
TDeleteCertDTO,
|
||||
TGetCertBodyDTO,
|
||||
TGetCertBundleDTO,
|
||||
TGetCertDTO,
|
||||
TGetCertPrivateKeyDTO,
|
||||
TImportCertDTO,
|
||||
TRevokeCertDTO
|
||||
} from "./certificate-types";
|
||||
|
||||
type TCertificateServiceFactoryDep = {
|
||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find" | "transaction" | "create">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">;
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
|
||||
projectDAL: Pick<
|
||||
TProjectDALFactory,
|
||||
"findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction" | "getProjectFromSplitId"
|
||||
>;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
@ -54,6 +66,8 @@ export const certificateServiceFactory = ({
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
@ -63,12 +77,11 @@ export const certificateServiceFactory = ({
|
||||
*/
|
||||
const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => {
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
@ -80,8 +93,7 @@ export const certificateServiceFactory = ({
|
||||
);
|
||||
|
||||
return {
|
||||
cert,
|
||||
ca
|
||||
cert
|
||||
};
|
||||
};
|
||||
|
||||
@ -96,12 +108,11 @@ export const certificateServiceFactory = ({
|
||||
actorOrgId
|
||||
}: TGetCertPrivateKeyDTO) => {
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
@ -114,14 +125,13 @@ export const certificateServiceFactory = ({
|
||||
|
||||
const { certPrivateKey } = await getCertificateCredentials({
|
||||
certId: cert.id,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
return {
|
||||
ca,
|
||||
cert,
|
||||
certPrivateKey
|
||||
};
|
||||
@ -132,12 +142,11 @@ export const certificateServiceFactory = ({
|
||||
*/
|
||||
const deleteCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => {
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
@ -151,8 +160,7 @@ export const certificateServiceFactory = ({
|
||||
const deletedCert = await certificateDAL.deleteById(cert.id);
|
||||
|
||||
return {
|
||||
deletedCert,
|
||||
ca
|
||||
deletedCert
|
||||
};
|
||||
};
|
||||
|
||||
@ -170,7 +178,20 @@ export const certificateServiceFactory = ({
|
||||
actorOrgId
|
||||
}: TRevokeCertDTO) => {
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
if (!cert.caId) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot revoke imported certificates"
|
||||
});
|
||||
}
|
||||
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
|
||||
|
||||
if (ca.externalCa?.id) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot revoke external certificates"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@ -211,7 +232,7 @@ export const certificateServiceFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
return { revokedAt, cert, ca };
|
||||
return { revokedAt, cert, ca: expandInternalCa(ca) };
|
||||
};
|
||||
|
||||
/**
|
||||
@ -220,12 +241,11 @@ export const certificateServiceFactory = ({
|
||||
*/
|
||||
const getCertBody = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => {
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
@ -239,7 +259,7 @@ export const certificateServiceFactory = ({
|
||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||
|
||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
@ -253,28 +273,259 @@ export const certificateServiceFactory = ({
|
||||
|
||||
const certObj = new x509.X509Certificate(decryptedCert);
|
||||
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caCertId: cert.caCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
let certificateChain = null;
|
||||
|
||||
const certificateChain = await buildCertificateChain({
|
||||
caCert,
|
||||
caCertChain,
|
||||
kmsId: certificateManagerKeyId,
|
||||
kmsService,
|
||||
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
|
||||
});
|
||||
// On newer certs the certBody.encryptedCertificateChain column will always exist.
|
||||
// Older certs will have a caCertId which will be used as a fallback mechanism for structuring the chain.
|
||||
if (certBody.encryptedCertificateChain) {
|
||||
const decryptedCertChain = await kmsDecryptor({
|
||||
cipherTextBlob: certBody.encryptedCertificateChain
|
||||
});
|
||||
certificateChain = decryptedCertChain.toString();
|
||||
} else if (cert.caCertId) {
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caCertId: cert.caCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
certificateChain = `${caCert}\n${caCertChain}`.trim();
|
||||
}
|
||||
|
||||
return {
|
||||
certificate: certObj.toString("pem"),
|
||||
certificateChain,
|
||||
serialNumber: certObj.serialNumber,
|
||||
cert,
|
||||
ca
|
||||
cert
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Import certificate
|
||||
*/
|
||||
const importCert = async ({
|
||||
projectSlug,
|
||||
pkiCollectionId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId,
|
||||
friendlyName,
|
||||
certificatePem,
|
||||
chainPem,
|
||||
privateKeyPem
|
||||
}: TImportCertDTO) => {
|
||||
const collectionId = pkiCollectionId;
|
||||
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
let projectId = project.id;
|
||||
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Create,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
// Check PKI collection
|
||||
if (collectionId) {
|
||||
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
|
||||
if (pkiCollection.projectId !== projectId) throw new BadRequestError({ message: "Invalid PKI collection" });
|
||||
}
|
||||
|
||||
const leafCert = new x509.X509Certificate(certificatePem);
|
||||
|
||||
// Verify the certificate chain
|
||||
const chainCerts = splitPemChain(chainPem).map((pem) => new x509.X509Certificate(pem));
|
||||
|
||||
// Remove leaf cert from the chain if it's present
|
||||
if (chainCerts[0].equal(leafCert)) {
|
||||
chainCerts.splice(0, 1);
|
||||
}
|
||||
|
||||
if (chainCerts.length === 0) {
|
||||
throw new BadRequestError({
|
||||
message: "Certificate chain must contain at least one issuer certificate"
|
||||
});
|
||||
}
|
||||
|
||||
// Verify leaf certificate is signed by the first certificate in the chain
|
||||
const isLeafVerified = await leafCert.verify({ publicKey: chainCerts[0].publicKey }).catch(() => false);
|
||||
if (!isLeafVerified) {
|
||||
throw new BadRequestError({ message: "Leaf certificate verification against chain failed" });
|
||||
}
|
||||
|
||||
// Verify the entire chain of trust
|
||||
const verificationPromises = chainCerts.slice(0, -1).map(async (currentCert, index) => {
|
||||
const issuerCert = chainCerts[index + 1];
|
||||
return currentCert.verify({ publicKey: issuerCert.publicKey }).catch(() => false);
|
||||
});
|
||||
|
||||
const verificationResults = await Promise.all(verificationPromises);
|
||||
|
||||
if (verificationResults.some((result) => !result)) {
|
||||
throw new BadRequestError({
|
||||
message: "Certificate chain verification failed: broken trust chain"
|
||||
});
|
||||
}
|
||||
|
||||
// Verify private key matches the certificate
|
||||
let privateKey;
|
||||
try {
|
||||
privateKey = createPrivateKey(privateKeyPem);
|
||||
} catch (err) {
|
||||
throw new BadRequestError({ message: "Invalid private key format" });
|
||||
}
|
||||
|
||||
try {
|
||||
const message = Buffer.from(Buffer.alloc(32));
|
||||
const publicKey = createPublicKey(certificatePem);
|
||||
const signature = sign(null, message, privateKey);
|
||||
const isValid = verify(null, message, publicKey, signature);
|
||||
|
||||
if (!isValid) {
|
||||
throw new BadRequestError({ message: "Private key does not match certificate" });
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestError) {
|
||||
throw err;
|
||||
}
|
||||
throw new BadRequestError({ message: "Error verifying private key against certificate" });
|
||||
}
|
||||
|
||||
// Get certificate attributes
|
||||
const commonName = Array.from(leafCert.subjectName.getField("CN")?.values() || [])[0] || "";
|
||||
|
||||
let altNames: undefined | string;
|
||||
const sanExtension = leafCert.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||
if (sanExtension) {
|
||||
const sanNames = new x509.GeneralNames(sanExtension.value);
|
||||
altNames = sanNames.items.map((name) => name.value).join(", ");
|
||||
}
|
||||
|
||||
const { serialNumber, notBefore, notAfter } = leafCert;
|
||||
|
||||
// Encrypt certificate for storage
|
||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||
projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKeyId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(certificatePem)
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||
plainText: Buffer.from(privateKeyPem)
|
||||
});
|
||||
|
||||
// Extract Key Usage
|
||||
const keyUsagesExt = leafCert.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
|
||||
|
||||
let keyUsages: CertKeyUsage[] = [];
|
||||
if (keyUsagesExt) {
|
||||
keyUsages = Object.values(CertKeyUsage).filter(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & keyUsagesExt.usages) !== 0
|
||||
);
|
||||
}
|
||||
|
||||
// Extract Extended Key Usage
|
||||
const extKeyUsageExt = leafCert.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
|
||||
let extendedKeyUsages: CertExtendedKeyUsage[] = [];
|
||||
if (extKeyUsageExt) {
|
||||
extendedKeyUsages = extKeyUsageExt.usages.map((ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]);
|
||||
}
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(chainPem)
|
||||
});
|
||||
|
||||
const cert = await certificateDAL.transaction(async (tx) => {
|
||||
try {
|
||||
const txCert = await certificateDAL.create(
|
||||
{
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: friendlyName || commonName,
|
||||
commonName,
|
||||
altNames,
|
||||
serialNumber,
|
||||
notBefore,
|
||||
notAfter,
|
||||
projectId,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateBodyDAL.create(
|
||||
{
|
||||
certId: txCert.id,
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateSecretDAL.create(
|
||||
{
|
||||
certId: txCert.id,
|
||||
encryptedPrivateKey
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (collectionId) {
|
||||
await pkiCollectionItemDAL.create(
|
||||
{
|
||||
pkiCollectionId: collectionId,
|
||||
certId: txCert.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return txCert;
|
||||
} catch (error) {
|
||||
// @ts-expect-error We're expecting a database error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (error?.error?.code === "23505") {
|
||||
throw new BadRequestError({ message: "Certificate serial already exists in your project" });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: certificatePem,
|
||||
certificateChain: chainPem,
|
||||
privateKey: privateKeyPem,
|
||||
serialNumber,
|
||||
cert
|
||||
};
|
||||
};
|
||||
|
||||
@ -284,12 +535,11 @@ export const certificateServiceFactory = ({
|
||||
*/
|
||||
const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => {
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
@ -307,7 +557,7 @@ export const certificateServiceFactory = ({
|
||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||
|
||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
@ -322,27 +572,32 @@ export const certificateServiceFactory = ({
|
||||
const certObj = new x509.X509Certificate(decryptedCert);
|
||||
const certificate = certObj.toString("pem");
|
||||
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caCertId: cert.caCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
let certificateChain = null;
|
||||
|
||||
const certificateChain = await buildCertificateChain({
|
||||
caCert,
|
||||
caCertChain,
|
||||
kmsId: certificateManagerKeyId,
|
||||
kmsService,
|
||||
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
|
||||
});
|
||||
// On newer certs the certBody.encryptedCertificateChain column will always exist.
|
||||
// Older certs will have a caCertId which will be used as a fallback mechanism for structuring the chain.
|
||||
if (certBody.encryptedCertificateChain) {
|
||||
const decryptedCertChain = await kmsDecryptor({
|
||||
cipherTextBlob: certBody.encryptedCertificateChain
|
||||
});
|
||||
certificateChain = decryptedCertChain.toString();
|
||||
} else if (cert.caCertId) {
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caCertId: cert.caCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
certificateChain = `${caCert}\n${caCertChain}`.trim();
|
||||
}
|
||||
|
||||
let privateKey: string | null = null;
|
||||
try {
|
||||
const { certPrivateKey } = await getCertificateCredentials({
|
||||
certId: cert.id,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
@ -360,8 +615,7 @@ export const certificateServiceFactory = ({
|
||||
certificateChain,
|
||||
privateKey,
|
||||
serialNumber,
|
||||
cert,
|
||||
ca
|
||||
cert
|
||||
};
|
||||
};
|
||||
|
||||
@ -371,6 +625,7 @@ export const certificateServiceFactory = ({
|
||||
deleteCert,
|
||||
revokeCert,
|
||||
getCertBody,
|
||||
importCert,
|
||||
getCertBundle
|
||||
};
|
||||
};
|
||||
|
@ -78,6 +78,17 @@ export type TGetCertBodyDTO = {
|
||||
serialNumber: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TImportCertDTO = {
|
||||
projectSlug: string;
|
||||
|
||||
friendlyName?: string;
|
||||
pkiCollectionId?: string;
|
||||
|
||||
certificatePem: string;
|
||||
privateKeyPem: string;
|
||||
chainPem: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCertPrivateKeyDTO = {
|
||||
serialNumber: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
@ -93,11 +104,3 @@ export type TGetCertificateCredentialsDTO = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TBuildCertificateChainDTO = {
|
||||
caCert?: string;
|
||||
caCertChain?: string;
|
||||
encryptedCertificateChain?: Buffer;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey">;
|
||||
kmsId: string;
|
||||
};
|
||||
|
@ -96,10 +96,15 @@ export const identityAccessTokenServiceFactory = ({
|
||||
}
|
||||
await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses });
|
||||
|
||||
const { accessTokenMaxTTL, createdAt: accessTokenCreatedAt, accessTokenTTL } = identityAccessToken;
|
||||
const {
|
||||
accessTokenMaxTTL,
|
||||
createdAt: accessTokenCreatedAt,
|
||||
accessTokenTTL,
|
||||
accessTokenPeriod
|
||||
} = identityAccessToken;
|
||||
|
||||
// max ttl checks - will it go above max ttl
|
||||
if (Number(accessTokenMaxTTL) > 0) {
|
||||
// Only enforce Max TTL for non-periodic tokens
|
||||
if (Number(accessTokenMaxTTL) > 0 && Number(accessTokenPeriod) === 0) {
|
||||
const accessTokenCreated = new Date(accessTokenCreatedAt);
|
||||
const ttlInMilliseconds = Number(accessTokenMaxTTL) * 1000;
|
||||
const currentDate = new Date();
|
||||
@ -125,6 +130,18 @@ export const identityAccessTokenServiceFactory = ({
|
||||
accessTokenLastRenewedAt: new Date()
|
||||
});
|
||||
|
||||
const ttl = Number(accessTokenTTL);
|
||||
const period = Number(accessTokenPeriod);
|
||||
|
||||
let expiresIn: number | undefined;
|
||||
if (period > 0) {
|
||||
expiresIn = period;
|
||||
} else if (ttl > 0) {
|
||||
expiresIn = ttl;
|
||||
} else {
|
||||
expiresIn = undefined;
|
||||
}
|
||||
|
||||
const renewedToken = jwt.sign(
|
||||
{
|
||||
identityId: decodedToken.identityId,
|
||||
@ -133,12 +150,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||
Number(identityAccessToken.accessTokenTTL) === 0
|
||||
? undefined
|
||||
: {
|
||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||
}
|
||||
expiresIn !== undefined ? { expiresIn } : undefined
|
||||
);
|
||||
|
||||
return { accessToken: renewedToken, identityAccessToken: updatedIdentityAccessToken };
|
||||
|
@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import https from "https";
|
||||
import jwt from "jsonwebtoken";
|
||||
import RE2 from "re2";
|
||||
|
||||
import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas";
|
||||
import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
|
||||
@ -185,7 +186,13 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const [k8sHost, k8sPort] = identityKubernetesAuth.kubernetesHost.split(":");
|
||||
let { kubernetesHost } = identityKubernetesAuth;
|
||||
|
||||
if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) {
|
||||
kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, "");
|
||||
}
|
||||
|
||||
const [k8sHost, k8sPort] = kubernetesHost.split(":");
|
||||
|
||||
const data = identityKubernetesAuth.gatewayId
|
||||
? await $gatewayProxyWrapper(
|
||||
|
@ -63,6 +63,18 @@ export type TCreateTokenReviewResponse = {
|
||||
status: TCreateTokenReviewSuccessResponse | TCreateTokenReviewErrorResponse;
|
||||
};
|
||||
|
||||
export type TKubernetesTokenRequest = {
|
||||
apiVersion: "authentication.k8s.io/v1";
|
||||
kind: "TokenRequest";
|
||||
spec: {
|
||||
audiences: string[];
|
||||
expirationSeconds: number;
|
||||
};
|
||||
status: {
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TRevokeKubernetesAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@ -114,21 +114,36 @@ export const identityUaServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const accessTokenTTLParams =
|
||||
Number(identityUa.accessTokenPeriod) === 0
|
||||
? {
|
||||
accessTokenTTL: identityUa.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityUa.accessTokenMaxTTL
|
||||
}
|
||||
: {
|
||||
accessTokenTTL: identityUa.accessTokenPeriod,
|
||||
// Setting Max TTL to 2 × period ensures that clients can always renew their token
|
||||
// at least once, and matches client logic that checks if renewing would exceed Max TTL.
|
||||
accessTokenMaxTTL: 2 * identityUa.accessTokenPeriod
|
||||
};
|
||||
|
||||
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
|
||||
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
|
||||
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityUa.identityId,
|
||||
isAccessTokenRevoked: false,
|
||||
identityUAClientSecretId: uaClientSecretDoc.id,
|
||||
accessTokenTTL: identityUa.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityUa.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
|
||||
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH
|
||||
accessTokenPeriod: identityUa.accessTokenPeriod,
|
||||
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
|
||||
...accessTokenTTLParams
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return newToken;
|
||||
});
|
||||
|
||||
@ -149,7 +164,14 @@ export const identityUaServiceFactory = ({
|
||||
}
|
||||
);
|
||||
|
||||
return { accessToken, identityUa, validClientSecretInfo, identityAccessToken, identityMembershipOrg };
|
||||
return {
|
||||
accessToken,
|
||||
identityUa,
|
||||
validClientSecretInfo,
|
||||
identityAccessToken,
|
||||
identityMembershipOrg,
|
||||
...accessTokenTTLParams
|
||||
};
|
||||
};
|
||||
|
||||
const attachUniversalAuth = async ({
|
||||
@ -163,7 +185,8 @@ export const identityUaServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId,
|
||||
isActorSuperAdmin
|
||||
isActorSuperAdmin,
|
||||
accessTokenPeriod
|
||||
}: TAttachUaDTO) => {
|
||||
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
|
||||
|
||||
@ -232,7 +255,8 @@ export const identityUaServiceFactory = ({
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
|
||||
accessTokenPeriod
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -248,6 +272,7 @@ export const identityUaServiceFactory = ({
|
||||
accessTokenTTL,
|
||||
accessTokenTrustedIps,
|
||||
clientSecretTrustedIps,
|
||||
accessTokenPeriod,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
@ -324,6 +349,7 @@ export const identityUaServiceFactory = ({
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenPeriod,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
: undefined
|
||||
|
@ -5,6 +5,7 @@ export type TAttachUaDTO = {
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenPeriod: number;
|
||||
clientSecretTrustedIps: { ipAddress: string }[];
|
||||
accessTokenTrustedIps: { ipAddress: string }[];
|
||||
isActorSuperAdmin?: boolean;
|
||||
@ -15,6 +16,7 @@ export type TUpdateUaDTO = {
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenPeriod?: number;
|
||||
clientSecretTrustedIps?: { ipAddress: string }[];
|
||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@ -31,12 +31,13 @@ export const pkiAlertDALFactory = (db: TDbClient) => {
|
||||
.select(
|
||||
db.raw("? as type", [PkiItemType.CA]),
|
||||
`${PkiItemType.CA}.id`,
|
||||
`${PkiItemType.CA}.notAfter as expiryDate`,
|
||||
`${PkiItemType.CA}.serialNumber`,
|
||||
`${PkiItemType.CA}.friendlyName`,
|
||||
"ic.notAfter as expiryDate",
|
||||
"ic.serialNumber",
|
||||
"ic.friendlyName",
|
||||
"pci.pkiCollectionId"
|
||||
)
|
||||
.from(`${TableName.CertificateAuthority} as ${PkiItemType.CA}`)
|
||||
.join(`${TableName.InternalCertificateAuthority} as ic`, `${PkiItemType.CA}.id`, "ic.caId")
|
||||
.join(`${TableName.PkiCollectionItem} as pci`, `${PkiItemType.CA}.id`, "pci.caId")
|
||||
.unionAll((qb) => {
|
||||
void qb
|
||||
|
@ -27,13 +27,13 @@ export const pkiCollectionItemDALFactory = (db: TDbClient) => {
|
||||
.select(
|
||||
"pki_collection_items.*",
|
||||
db.raw(
|
||||
`COALESCE("${TableName.CertificateAuthority}"."notBefore", "${TableName.Certificate}"."notBefore") as "notBefore"`
|
||||
`COALESCE("${TableName.InternalCertificateAuthority}"."notBefore", "${TableName.Certificate}"."notBefore") as "notBefore"`
|
||||
),
|
||||
db.raw(
|
||||
`COALESCE("${TableName.CertificateAuthority}"."notAfter", "${TableName.Certificate}"."notAfter") as "notAfter"`
|
||||
`COALESCE("${TableName.InternalCertificateAuthority}"."notAfter", "${TableName.Certificate}"."notAfter") as "notAfter"`
|
||||
),
|
||||
db.raw(
|
||||
`COALESCE("${TableName.CertificateAuthority}"."friendlyName", "${TableName.Certificate}"."friendlyName") as "friendlyName"`
|
||||
`COALESCE("${TableName.InternalCertificateAuthority}"."friendlyName", "${TableName.Certificate}"."friendlyName") as "friendlyName"`
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
@ -41,6 +41,11 @@ export const pkiCollectionItemDALFactory = (db: TDbClient) => {
|
||||
`${TableName.PkiCollectionItem}.caId`,
|
||||
`${TableName.CertificateAuthority}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.InternalCertificateAuthority,
|
||||
`${TableName.PkiCollectionItem}.caId`,
|
||||
`${TableName.InternalCertificateAuthority}.caId`
|
||||
)
|
||||
.leftJoin(TableName.Certificate, `${TableName.PkiCollectionItem}.certId`, `${TableName.Certificate}.id`)
|
||||
.where((builder) => {
|
||||
void builder.where(`${TableName.PkiCollectionItem}.pkiCollectionId`, collectionId);
|
||||
|
@ -269,14 +269,8 @@ export const pkiCollectionServiceFactory = ({
|
||||
});
|
||||
if (isCertAdded) throw new BadRequestError({ message: "Certificate already part of the PKI collection" });
|
||||
|
||||
// validate that there exists a certificate in same project as PKI collection
|
||||
const cas = await certificateAuthorityDAL.find({ projectId: pkiCollection.projectId });
|
||||
|
||||
// TODO: consider making this more efficient
|
||||
const [certificate] = await certificateDAL.find({
|
||||
$in: {
|
||||
caId: cas.map((ca) => ca.id)
|
||||
},
|
||||
projectId: pkiCollection.projectId,
|
||||
id: itemId
|
||||
});
|
||||
if (!certificate) throw new NotFoundError({ message: `Certificate with ID '${itemId}' not found` });
|
||||
|
187
backend/src/services/pki-subscriber/pki-subscriber-queue.ts
Normal file
187
backend/src/services/pki-subscriber/pki-subscriber-queue.ts
Normal file
@ -0,0 +1,187 @@
|
||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import { CaStatus, CaType } from "../certificate-authority/certificate-authority-enums";
|
||||
import { TCertificateAuthorityQueueFactory } from "../certificate-authority/certificate-authority-queue";
|
||||
import { InternalCertificateAuthorityFns } from "../certificate-authority/internal/internal-certificate-authority-fns";
|
||||
import { TPkiSubscriberDALFactory } from "./pki-subscriber-dal";
|
||||
import { PkiSubscriberStatus, SubscriberOperationStatus } from "./pki-subscriber-types";
|
||||
|
||||
type TPkiSubscriberQueueServiceFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
pkiSubscriberDAL: TPkiSubscriberDALFactory;
|
||||
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
||||
certificateAuthorityQueue: TCertificateAuthorityQueueFactory;
|
||||
internalCaFns: ReturnType<typeof InternalCertificateAuthorityFns>;
|
||||
certificateDAL: TCertificateDALFactory;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
};
|
||||
|
||||
export const pkiSubscriberQueueServiceFactory = ({
|
||||
queueService,
|
||||
pkiSubscriberDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityQueue,
|
||||
internalCaFns,
|
||||
certificateDAL,
|
||||
auditLogService
|
||||
}: TPkiSubscriberQueueServiceFactoryDep) => {
|
||||
queueService.start(QueueName.PkiSubscriber, async (job) => {
|
||||
if (job.name === QueueJobs.PkiSubscriberDailyAutoRenewal) {
|
||||
logger.info(`${QueueJobs.PkiSubscriberDailyAutoRenewal}: queue task started`);
|
||||
|
||||
const BATCH_SIZE = 100;
|
||||
let offset = 0;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
// fetch PKI subscribers with auto renewal enabled in batches
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const pkiSubscribers = await pkiSubscriberDAL.find(
|
||||
{
|
||||
enableAutoRenewal: true,
|
||||
$notNull: ["autoRenewalPeriodInDays"],
|
||||
status: PkiSubscriberStatus.ACTIVE
|
||||
},
|
||||
{
|
||||
limit: BATCH_SIZE,
|
||||
offset
|
||||
}
|
||||
);
|
||||
|
||||
if (pkiSubscribers.length === 0) {
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Process each subscriber in the batch concurrently
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(
|
||||
pkiSubscribers.map(async (subscriber) => {
|
||||
try {
|
||||
const cert = await certificateDAL.findLatestActiveCertForSubscriber({ subscriberId: subscriber.id });
|
||||
let shouldRenew = false;
|
||||
if (!cert || !cert.notAfter) {
|
||||
shouldRenew = true;
|
||||
} else {
|
||||
const now = new Date();
|
||||
const expiry = new Date(cert.notAfter);
|
||||
const daysUntilExpiry = (expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
|
||||
shouldRenew = daysUntilExpiry <= subscriber.autoRenewalPeriodInDays!;
|
||||
}
|
||||
|
||||
if (shouldRenew) {
|
||||
// Get the CA for the subscriber
|
||||
if (!subscriber.caId) {
|
||||
await pkiSubscriberDAL.updateById(subscriber.id, {
|
||||
lastOperationStatus: SubscriberOperationStatus.FAILED,
|
||||
lastOperationMessage: "No CA assigned to subscriber",
|
||||
lastOperationAt: new Date()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
||||
if (!ca) {
|
||||
await pkiSubscriberDAL.updateById(subscriber.id, {
|
||||
lastOperationStatus: SubscriberOperationStatus.FAILED,
|
||||
lastOperationMessage: "CA not found",
|
||||
lastOperationAt: new Date()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if CA is active
|
||||
if (ca.status !== CaStatus.ACTIVE) {
|
||||
await pkiSubscriberDAL.updateById(subscriber.id, {
|
||||
lastOperationStatus: SubscriberOperationStatus.FAILED,
|
||||
lastOperationMessage: "CA is not active",
|
||||
lastOperationAt: new Date()
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Order new certificate based on CA type
|
||||
if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) {
|
||||
await certificateAuthorityQueue.orderCertificateForSubscriber({
|
||||
subscriberId: subscriber.id,
|
||||
caType: ca.externalCa.type
|
||||
});
|
||||
} else if (ca.internalCa?.id) {
|
||||
// For internal CAs, we can issue certificates directly
|
||||
await internalCaFns.issueCertificate(subscriber, ca);
|
||||
}
|
||||
|
||||
// Update last auto-renew timestamp
|
||||
await pkiSubscriberDAL.updateById(subscriber.id, {
|
||||
lastAutoRenewAt: new Date(),
|
||||
lastOperationStatus: SubscriberOperationStatus.SUCCESS,
|
||||
lastOperationMessage: "Triggered certificate auto-renewal",
|
||||
lastOperationAt: new Date()
|
||||
});
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: subscriber.projectId,
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
event: {
|
||||
type: EventType.AUTOMATED_RENEW_SUBSCRIBER_CERT,
|
||||
metadata: {
|
||||
subscriberId: subscriber.id,
|
||||
name: subscriber.name
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Log error and update subscriber status
|
||||
logger.error(error, `Failed to auto-renew certificate for subscriber ${subscriber.id}`);
|
||||
await pkiSubscriberDAL.updateById(subscriber.id, {
|
||||
lastOperationStatus: SubscriberOperationStatus.FAILED,
|
||||
lastOperationMessage: error instanceof Error ? error.message : "Unknown error",
|
||||
lastOperationAt: new Date()
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
offset += BATCH_SIZE;
|
||||
}
|
||||
|
||||
logger.info(`${QueueJobs.PkiSubscriberDailyAutoRenewal}: queue task completed`);
|
||||
}
|
||||
});
|
||||
|
||||
// we do a repeat cron job in utc timezone at 12 Midnight each day
|
||||
const startDailyAutoRenewalJob = async () => {
|
||||
// clear previous job
|
||||
await queueService.stopRepeatableJob(
|
||||
QueueName.PkiSubscriber,
|
||||
QueueJobs.PkiSubscriberDailyAutoRenewal,
|
||||
{ pattern: "0 0 * * *", utc: true },
|
||||
// { pattern: "*/30 * * * * *", utc: true } // for testing
|
||||
QueueName.PkiSubscriber // just a job id
|
||||
);
|
||||
|
||||
await queueService.queue(QueueName.PkiSubscriber, QueueJobs.PkiSubscriberDailyAutoRenewal, undefined, {
|
||||
delay: 5000,
|
||||
jobId: QueueName.PkiSubscriber,
|
||||
// { pattern: "*/30 * * * * *", utc: true } // for testing
|
||||
repeat: { pattern: "0 0 * * *", utc: true }
|
||||
});
|
||||
};
|
||||
|
||||
queueService.listen(QueueName.PkiSubscriber, "failed", (_, err) => {
|
||||
logger.error(err, `${QueueName.PkiSubscriber}: failed`);
|
||||
});
|
||||
|
||||
return {
|
||||
startDailyAutoRenewalJob
|
||||
};
|
||||
};
|
@ -1,3 +1,5 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PkiSubscribersSchema } from "@app/db/schemas";
|
||||
|
||||
export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({
|
||||
@ -10,5 +12,13 @@ export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({
|
||||
subjectAlternativeNames: true,
|
||||
ttl: true,
|
||||
keyUsages: true,
|
||||
extendedKeyUsages: true
|
||||
extendedKeyUsages: true,
|
||||
lastOperationStatus: true,
|
||||
lastOperationMessage: true,
|
||||
lastOperationAt: true,
|
||||
enableAutoRenewal: true,
|
||||
autoRenewalPeriodInDays: true,
|
||||
lastAutoRenewAt: true
|
||||
}).extend({
|
||||
supportsImmediateCertIssuance: z.boolean().optional()
|
||||
});
|
||||
|
@ -1,23 +1,20 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import crypto, { KeyObject } from "crypto";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertExtendedKeyUsageOIDToName,
|
||||
@ -27,27 +24,34 @@ import {
|
||||
} from "@app/services/certificate/certificate-types";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import {
|
||||
createSerialNumber,
|
||||
expandInternalCa,
|
||||
getCaCertChain,
|
||||
getCaCredentials,
|
||||
keyAlgorithmToAlgCfg,
|
||||
parseDistinguishedName
|
||||
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
import { getCertificateCredentials } from "../certificate/certificate-fns";
|
||||
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||
import { TCertificateAuthorityQueueFactory } from "../certificate-authority/certificate-authority-queue";
|
||||
import { InternalCertificateAuthorityFns } from "../certificate-authority/internal/internal-certificate-authority-fns";
|
||||
import {
|
||||
PkiSubscriberStatus,
|
||||
TCreatePkiSubscriberDTO,
|
||||
TDeletePkiSubscriberDTO,
|
||||
TGetPkiSubscriberDTO,
|
||||
TGetSubscriberActiveCertBundleDTO,
|
||||
TIssuePkiSubscriberCertDTO,
|
||||
TListPkiSubscriberCertsDTO,
|
||||
TOrderPkiSubscriberCertDTO,
|
||||
TSignPkiSubscriberCertDTO,
|
||||
TUpdatePkiSubscriberDTO
|
||||
} from "./pki-subscriber-types";
|
||||
@ -57,16 +61,24 @@ type TPkiSubscriberServiceFactoryDep = {
|
||||
TPkiSubscriberDALFactory,
|
||||
"create" | "findById" | "updateById" | "deleteById" | "transaction" | "find" | "findOne"
|
||||
>;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityDAL: Pick<
|
||||
TCertificateAuthorityDALFactory,
|
||||
"findByIdWithAssociatedCa" | "findById" | "transaction" | "create" | "updateById" | "findWithAssociatedCa"
|
||||
>;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
certificateAuthorityQueue: Pick<TCertificateAuthorityQueueFactory, "orderCertificateForSubscriber">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "countCertificatesForPkiSubscriber" | "find">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||
certificateDAL: Pick<
|
||||
TCertificateDALFactory,
|
||||
"create" | "transaction" | "countCertificatesForPkiSubscriber" | "findLatestActiveCertForSubscriber" | "find"
|
||||
>;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create" | "findOne">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create" | "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById" | "find">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
internalCaFns: ReturnType<typeof InternalCertificateAuthorityFns>;
|
||||
};
|
||||
|
||||
export type TPkiSubscriberServiceFactory = ReturnType<typeof pkiSubscriberServiceFactory>;
|
||||
@ -78,11 +90,13 @@ export const pkiSubscriberServiceFactory = ({
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateBodyDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
permissionService,
|
||||
certificateAuthorityQueue,
|
||||
internalCaFns
|
||||
}: TPkiSubscriberServiceFactoryDep) => {
|
||||
const createSubscriber = async ({
|
||||
name,
|
||||
@ -93,6 +107,8 @@ export const pkiSubscriberServiceFactory = ({
|
||||
subjectAlternativeNames,
|
||||
keyUsages,
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays,
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@ -115,6 +131,12 @@ export const pkiSubscriberServiceFactory = ({
|
||||
})
|
||||
);
|
||||
|
||||
if (enableAutoRenewal) {
|
||||
if (!autoRenewalPeriodInDays) {
|
||||
throw new BadRequestError({ message: "autoRenewalPeriodInDays is required when enableAutoRenewal is true" });
|
||||
}
|
||||
}
|
||||
|
||||
const newSubscriber = await pkiSubscriberDAL.create({
|
||||
caId,
|
||||
projectId,
|
||||
@ -124,7 +146,9 @@ export const pkiSubscriberServiceFactory = ({
|
||||
ttl,
|
||||
subjectAlternativeNames,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays
|
||||
});
|
||||
|
||||
return newSubscriber;
|
||||
@ -161,7 +185,18 @@ export const pkiSubscriberServiceFactory = ({
|
||||
})
|
||||
);
|
||||
|
||||
return subscriber;
|
||||
let supportsImmediateCertIssuance = false;
|
||||
if (subscriber.caId) {
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
||||
if (ca.internalCa?.id) {
|
||||
supportsImmediateCertIssuance = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...subscriber,
|
||||
supportsImmediateCertIssuance
|
||||
};
|
||||
};
|
||||
|
||||
const updateSubscriber = async ({
|
||||
@ -175,6 +210,8 @@ export const pkiSubscriberServiceFactory = ({
|
||||
subjectAlternativeNames,
|
||||
keyUsages,
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
@ -202,6 +239,12 @@ export const pkiSubscriberServiceFactory = ({
|
||||
})
|
||||
);
|
||||
|
||||
if (enableAutoRenewal) {
|
||||
if (!autoRenewalPeriodInDays && !subscriber.autoRenewalPeriodInDays) {
|
||||
throw new BadRequestError({ message: "autoRenewalPeriodInDays is required when enableAutoRenewal is true" });
|
||||
}
|
||||
}
|
||||
|
||||
const updatedSubscriber = await pkiSubscriberDAL.updateById(subscriber.id, {
|
||||
caId,
|
||||
name,
|
||||
@ -210,7 +253,9 @@ export const pkiSubscriberServiceFactory = ({
|
||||
ttl,
|
||||
subjectAlternativeNames,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays
|
||||
});
|
||||
|
||||
return updatedSubscriber;
|
||||
@ -251,28 +296,26 @@ export const pkiSubscriberServiceFactory = ({
|
||||
return subscriber;
|
||||
};
|
||||
|
||||
const issueSubscriberCert = async ({
|
||||
const orderSubscriberCert = async ({
|
||||
subscriberName,
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TIssuePkiSubscriberCertDTO) => {
|
||||
}: TOrderPkiSubscriberCertDTO) => {
|
||||
const subscriber = await pkiSubscriberDAL.findOne({
|
||||
name: subscriberName,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
|
||||
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(subscriber.caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
projectId: subscriber.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
@ -287,200 +330,69 @@ export const pkiSubscriberServiceFactory = ({
|
||||
|
||||
if (subscriber.status !== PkiSubscriberStatus.ACTIVE)
|
||||
throw new BadRequestError({ message: "Subscriber is not active" });
|
||||
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
|
||||
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
if (ca.requireTemplateForIssuance) {
|
||||
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
||||
}
|
||||
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
|
||||
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||
const notBeforeDate = new Date();
|
||||
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl));
|
||||
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||
|
||||
// check not before constraint
|
||||
if (notBeforeDate < caCertNotBeforeDate) {
|
||||
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
||||
if (ca.internalCa?.id) {
|
||||
throw new BadRequestError({ message: "CA does not support ordering of certificates" });
|
||||
}
|
||||
|
||||
// check not after constraint
|
||||
if (notAfterDate > caCertNotAfterDate) {
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
if (ca.status !== CaStatus.ACTIVE) {
|
||||
throw new BadRequestError({ message: "CA is disabled" });
|
||||
}
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
|
||||
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||
name: `CN=${subscriber.commonName}`,
|
||||
keys: leafKeys,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
// eslint-disable-next-line no-bitwise
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
|
||||
],
|
||||
attributes: [new x509.ChallengePasswordAttribute("password")]
|
||||
});
|
||||
|
||||
const { caPrivateKey, caSecret } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const appCfg = getConfig();
|
||||
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
|
||||
new x509.AuthorityInfoAccessExtension({
|
||||
caIssuers: new x509.GeneralName("url", caIssuerUrl)
|
||||
}),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
|
||||
];
|
||||
|
||||
const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[];
|
||||
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
|
||||
if (keyUsagesBitValue) {
|
||||
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
|
||||
}
|
||||
|
||||
if (subscriber.extendedKeyUsages.length) {
|
||||
const extendedKeyUsagesExtension = new x509.ExtendedKeyUsageExtension(
|
||||
subscriber.extendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku as CertExtendedKeyUsage]),
|
||||
true
|
||||
);
|
||||
extensions.push(extendedKeyUsagesExtension);
|
||||
}
|
||||
|
||||
let altNamesArray: { type: "email" | "dns"; value: string }[] = [];
|
||||
|
||||
if (subscriber.subjectAlternativeNames?.length) {
|
||||
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
|
||||
if (z.string().email().safeParse(altName).success) {
|
||||
return { type: "email", value: altName };
|
||||
}
|
||||
|
||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||
return { type: "dns", value: altName };
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
|
||||
if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) {
|
||||
await certificateAuthorityQueue.orderCertificateForSubscriber({
|
||||
subscriberId: subscriber.id,
|
||||
caType: ca.externalCa.type
|
||||
});
|
||||
|
||||
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||
extensions.push(altNamesExtension);
|
||||
return subscriber;
|
||||
}
|
||||
|
||||
const serialNumber = createSerialNumber();
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: csrObj.subject,
|
||||
issuer: caCertObj.subject,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
signingKey: caPrivateKey,
|
||||
publicKey: csrObj.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions
|
||||
throw new BadRequestError({ message: "Unsupported CA type" });
|
||||
};
|
||||
|
||||
const issueSubscriberCert = async ({
|
||||
subscriberName,
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TIssuePkiSubscriberCertDTO) => {
|
||||
const subscriber = await pkiSubscriberDAL.findOne({
|
||||
name: subscriberName,
|
||||
projectId
|
||||
});
|
||||
|
||||
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
|
||||
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||
});
|
||||
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||
plainText: Buffer.from(skLeaf)
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: subscriber.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||
caCertId: caCert.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSubscriberActions.IssueCert,
|
||||
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||
name: subscriber.name
|
||||
})
|
||||
);
|
||||
|
||||
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
|
||||
if (subscriber.status !== PkiSubscriberStatus.ACTIVE)
|
||||
throw new BadRequestError({ message: "Subscriber is not active" });
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(certificateChainPem)
|
||||
});
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
||||
if (ca.internalCa?.id) {
|
||||
return internalCaFns.issueCertificate(subscriber, ca);
|
||||
}
|
||||
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
caCertId: caCert.id,
|
||||
pkiSubscriberId: subscriber.id,
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: subscriber.commonName,
|
||||
commonName: subscriber.commonName,
|
||||
altNames: subscriber.subjectAlternativeNames.join(","),
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
keyUsages: selectedKeyUsages,
|
||||
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateBodyDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateSecretDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedPrivateKey
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: leafCert.toString("pem"),
|
||||
certificateChain: certificateChainPem,
|
||||
issuingCaCertificate,
|
||||
privateKey: skLeaf,
|
||||
serialNumber,
|
||||
ca,
|
||||
subscriber
|
||||
};
|
||||
throw new BadRequestError({ message: "CA does not support immediate issuance of certificates" });
|
||||
};
|
||||
|
||||
const signSubscriberCert = async ({
|
||||
@ -500,8 +412,8 @@ export const pkiSubscriberServiceFactory = ({
|
||||
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
|
||||
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(subscriber.caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
||||
if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@ -522,11 +434,10 @@ export const pkiSubscriberServiceFactory = ({
|
||||
if (subscriber.status !== PkiSubscriberStatus.ACTIVE)
|
||||
throw new BadRequestError({ message: "Subscriber is not active" });
|
||||
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
|
||||
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
if (ca.requireTemplateForIssuance) {
|
||||
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
||||
}
|
||||
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
|
||||
if (!ca.internalCa?.activeCaCertId)
|
||||
throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId);
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
@ -543,7 +454,7 @@ export const pkiSubscriberServiceFactory = ({
|
||||
|
||||
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||
const notBeforeDate = new Date();
|
||||
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl));
|
||||
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl ?? "0"));
|
||||
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||
|
||||
@ -557,7 +468,7 @@ export const pkiSubscriberServiceFactory = ({
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
||||
|
||||
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||
|
||||
@ -691,7 +602,7 @@ export const pkiSubscriberServiceFactory = ({
|
||||
});
|
||||
|
||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||
caCertId: ca.activeCaCertId,
|
||||
caCertId: ca.internalCa.activeCaCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
@ -718,7 +629,8 @@ export const pkiSubscriberServiceFactory = ({
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
keyUsages: selectedKeyUsages,
|
||||
extendedKeyUsages: selectedExtendedKeyUsages
|
||||
extendedKeyUsages: selectedExtendedKeyUsages,
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -740,7 +652,7 @@ export const pkiSubscriberServiceFactory = ({
|
||||
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||
issuingCaCertificate,
|
||||
serialNumber,
|
||||
ca,
|
||||
ca: expandInternalCa(ca),
|
||||
commonName: subscriber.commonName,
|
||||
subscriber
|
||||
};
|
||||
@ -793,6 +705,114 @@ export const pkiSubscriberServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getSubscriberActiveCertBundle = async ({
|
||||
subscriberName,
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TGetSubscriberActiveCertBundleDTO) => {
|
||||
const subscriber = await pkiSubscriberDAL.findOne({
|
||||
name: subscriberName,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!subscriber) {
|
||||
throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: subscriber.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSubscriberActions.ListCerts,
|
||||
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||
name: subscriber.name
|
||||
})
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
const cert = await certificateDAL.findLatestActiveCertForSubscriber({
|
||||
subscriberId: subscriber.id
|
||||
});
|
||||
|
||||
if (!cert) {
|
||||
throw new NotFoundError({ message: "No active certificate found for subscriber" });
|
||||
}
|
||||
|
||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||
|
||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: cert.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKeyId
|
||||
});
|
||||
const decryptedCert = await kmsDecryptor({
|
||||
cipherTextBlob: certBody.encryptedCertificate
|
||||
});
|
||||
|
||||
const certObj = new x509.X509Certificate(decryptedCert);
|
||||
const certificate = certObj.toString("pem");
|
||||
|
||||
let certificateChain = null;
|
||||
|
||||
// On newer certs the certBody.encryptedCertificateChain column will always exist.
|
||||
// Older certs will have a caCertId which will be used as a fallback mechanism for structuring the chain.
|
||||
if (certBody.encryptedCertificateChain) {
|
||||
const decryptedCertChain = await kmsDecryptor({
|
||||
cipherTextBlob: certBody.encryptedCertificateChain
|
||||
});
|
||||
certificateChain = decryptedCertChain.toString();
|
||||
} else if (cert.caCertId) {
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caCertId: cert.caCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
certificateChain = `${caCert}\n${caCertChain}`.trim();
|
||||
}
|
||||
|
||||
const { certPrivateKey } = await getCertificateCredentials({
|
||||
certId: cert.id,
|
||||
projectId: cert.projectId,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey: certPrivateKey,
|
||||
serialNumber: cert.serialNumber,
|
||||
cert,
|
||||
subscriber
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createSubscriber,
|
||||
getSubscriber,
|
||||
@ -800,6 +820,8 @@ export const pkiSubscriberServiceFactory = ({
|
||||
deleteSubscriber,
|
||||
issueSubscriberCert,
|
||||
signSubscriberCert,
|
||||
listSubscriberCerts
|
||||
listSubscriberCerts,
|
||||
orderSubscriberCert,
|
||||
getSubscriberActiveCertBundle
|
||||
};
|
||||
};
|
||||
|
@ -12,10 +12,12 @@ export type TCreatePkiSubscriberDTO = {
|
||||
name: string;
|
||||
commonName: string;
|
||||
status: PkiSubscriberStatus;
|
||||
ttl: string;
|
||||
ttl?: string;
|
||||
subjectAlternativeNames: string[];
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
enableAutoRenewal?: boolean;
|
||||
autoRenewalPeriodInDays?: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetPkiSubscriberDTO = {
|
||||
@ -32,6 +34,8 @@ export type TUpdatePkiSubscriberDTO = {
|
||||
subjectAlternativeNames?: string[];
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
enableAutoRenewal?: boolean;
|
||||
autoRenewalPeriodInDays?: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TDeletePkiSubscriberDTO = {
|
||||
@ -42,6 +46,10 @@ export type TIssuePkiSubscriberCertDTO = {
|
||||
subscriberName: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TOrderPkiSubscriberCertDTO = {
|
||||
subscriberName: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TSignPkiSubscriberCertDTO = {
|
||||
subscriberName: string;
|
||||
csr: string;
|
||||
@ -52,3 +60,12 @@ export type TListPkiSubscriberCertsDTO = {
|
||||
offset: number;
|
||||
limit: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetSubscriberActiveCertBundleDTO = {
|
||||
subscriberName: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export enum SubscriberOperationStatus {
|
||||
SUCCESS = "success",
|
||||
FAILED = "failed"
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
ProjectMembershipRole,
|
||||
ProjectType,
|
||||
ProjectVersion,
|
||||
TableName,
|
||||
TProjectEnvironments
|
||||
} from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
@ -41,6 +42,7 @@ import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subsc
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import { expandInternalCa } from "../certificate-authority/certificate-authority-fns";
|
||||
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
|
||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
@ -149,7 +151,7 @@ type TProjectServiceFactoryDep = {
|
||||
>;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findWithAssociatedCa">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
||||
@ -914,17 +916,20 @@ export const projectServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const cas = await certificateAuthorityDAL.find(
|
||||
const cas = await certificateAuthorityDAL.findWithAssociatedCa(
|
||||
{
|
||||
projectId,
|
||||
...(status && { status }),
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
|
||||
$notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"],
|
||||
...(status && { [`${TableName.CertificateAuthority}.status` as "status"]: status }),
|
||||
...(friendlyName && {
|
||||
[`${TableName.InternalCertificateAuthority}.friendlyName` as "friendlyName"]: friendlyName
|
||||
}),
|
||||
...(commonName && { [`${TableName.InternalCertificateAuthority}.commonName` as "commonName"]: commonName })
|
||||
},
|
||||
{ offset, limit, sort: [["updatedAt", "desc"]] }
|
||||
);
|
||||
|
||||
return cas;
|
||||
return cas.map((ca) => expandInternalCa(ca));
|
||||
};
|
||||
|
||||
/**
|
||||
@ -965,13 +970,9 @@ export const projectServiceFactory = ({
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
const cas = await certificateAuthorityDAL.find({ projectId });
|
||||
|
||||
const certificates = await certificateDAL.find(
|
||||
{
|
||||
$in: {
|
||||
caId: cas.map((ca) => ca.id)
|
||||
},
|
||||
projectId,
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
},
|
||||
|
@ -115,8 +115,6 @@ export const secretSharingServiceFactory = ({
|
||||
|
||||
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||
|
||||
let salt: string | undefined;
|
||||
let encryptedSalt: Buffer | undefined;
|
||||
const orgEmails = [];
|
||||
|
||||
if (emails && emails.length > 0) {
|
||||
@ -133,10 +131,6 @@ export const secretSharingServiceFactory = ({
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Generate salt for signing email hashes (if emails are provided)
|
||||
salt = crypto.randomBytes(32).toString("hex");
|
||||
encryptedSalt = encryptWithRoot(Buffer.from(salt));
|
||||
}
|
||||
|
||||
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||
@ -158,14 +152,13 @@ export const secretSharingServiceFactory = ({
|
||||
userId: actorId,
|
||||
orgId,
|
||||
accessType,
|
||||
authorizedEmails: emails && emails.length > 0 ? JSON.stringify(emails) : undefined,
|
||||
encryptedSalt
|
||||
authorizedEmails: emails && emails.length > 0 ? JSON.stringify(emails) : undefined
|
||||
});
|
||||
|
||||
const idToReturn = `${Buffer.from(newSharedSecret.identifier!, "hex").toString("base64url")}`;
|
||||
|
||||
// Loop through recipients and send out emails with unique access links
|
||||
if (emails && salt) {
|
||||
if (emails) {
|
||||
const user = await userDAL.findById(actorId);
|
||||
|
||||
if (!user) {
|
||||
@ -174,9 +167,6 @@ export const secretSharingServiceFactory = ({
|
||||
|
||||
for await (const email of emails) {
|
||||
try {
|
||||
const hmac = crypto.createHmac("sha256", salt).update(email);
|
||||
const hash = hmac.digest("hex");
|
||||
|
||||
// Only show the username to emails which are part of the organization
|
||||
const respondentUsername = orgEmails.includes(email) ? user.username : undefined;
|
||||
|
||||
@ -186,7 +176,7 @@ export const secretSharingServiceFactory = ({
|
||||
substitutions: {
|
||||
name,
|
||||
respondentUsername,
|
||||
secretRequestUrl: `${appCfg.SITE_URL}/shared/secret/${idToReturn}?email=${encodeURIComponent(email)}&hash=${hash}`
|
||||
secretRequestUrl: `${appCfg.SITE_URL}/shared/secret/${idToReturn}`
|
||||
},
|
||||
template: SmtpTemplates.SecretRequestCompleted
|
||||
});
|
||||
@ -474,9 +464,8 @@ export const secretSharingServiceFactory = ({
|
||||
sharedSecretId,
|
||||
hashedHex,
|
||||
orgId,
|
||||
password,
|
||||
email,
|
||||
hash
|
||||
actorId,
|
||||
password
|
||||
}: TGetActiveSharedSecretByIdDTO) => {
|
||||
const sharedSecret = isUuidV4(sharedSecretId)
|
||||
? await secretSharingDAL.findOne({
|
||||
@ -506,6 +495,17 @@ export const secretSharingServiceFactory = ({
|
||||
throw new ForbiddenRequestError();
|
||||
}
|
||||
|
||||
// If the secret was shared with specific emails, verify that the current user's session email is authorized
|
||||
if (sharedSecret.authorizedEmails && (sharedSecret.authorizedEmails as string[]).length > 0) {
|
||||
if (!actorId) throw new UnauthorizedError();
|
||||
|
||||
const user = await userDAL.findById(actorId);
|
||||
if (!user || !user.email) throw new UnauthorizedError();
|
||||
|
||||
if (!(sharedSecret.authorizedEmails as string[]).includes(user.email))
|
||||
throw new UnauthorizedError({ message: "Email not authorized to view secret" });
|
||||
}
|
||||
|
||||
// all secrets pass through here, meaning we check if its expired first and then check if it needs verification
|
||||
// or can be safely sent to the client.
|
||||
if (expiresAt !== null && expiresAt < new Date()) {
|
||||
@ -524,31 +524,6 @@ export const secretSharingServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
|
||||
if (sharedSecret.authorizedEmails && sharedSecret.encryptedSalt) {
|
||||
// Verify both params were passed
|
||||
if (!email || !hash) {
|
||||
throw new BadRequestError({
|
||||
message: "This secret is email protected. Parameters must include email and hash."
|
||||
});
|
||||
|
||||
// Verify that email is authorized to view shared secret
|
||||
} else if (!(sharedSecret.authorizedEmails as string[]).includes(email)) {
|
||||
throw new UnauthorizedError({ message: "Email not authorized to view secret" });
|
||||
|
||||
// Verify that hash matches
|
||||
} else {
|
||||
const salt = decryptWithRoot(sharedSecret.encryptedSalt).toString();
|
||||
const hmac = crypto.createHmac("sha256", salt).update(email);
|
||||
const rebuiltHash = hmac.digest("hex");
|
||||
|
||||
if (rebuiltHash !== hash) {
|
||||
throw new UnauthorizedError({ message: "Email not authorized to view secret" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Password checks
|
||||
const isPasswordProtected = Boolean(sharedSecret.password);
|
||||
const hasProvidedPassword = Boolean(password);
|
||||
@ -561,6 +536,8 @@ export const secretSharingServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
|
||||
// If encryptedSecret is set, we know that this secret has been encrypted using KMS, and we can therefore do server-side decryption.
|
||||
let decryptedSecretValue: Buffer | undefined;
|
||||
if (sharedSecret.encryptedSecret) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user