mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-25 14:07:47 +00:00
Compare commits
115 Commits
docs/ident
...
doc/extern
Author | SHA1 | Date | |
---|---|---|---|
|
9f73c77624 | ||
|
5b7afea3f5 | ||
|
fe9318cf8d | ||
|
c5a9f36a0c | ||
|
5bd6a193f4 | ||
|
9ac17718b3 | ||
|
b9f35d16a5 | ||
|
5e9929a9d5 | ||
|
c2870dffcd | ||
|
e9ee38fb54 | ||
|
9d88caf66b | ||
|
7ef4b68503 | ||
|
d2456b5bd8 | ||
|
1b64cdf09c | ||
|
73a00df439 | ||
|
9f87689a8f | ||
|
5d6bbdfd24 | ||
|
f1b5e6104c | ||
|
0f7e055981 | ||
|
2045305127 | ||
|
6b6f8f5523 | ||
|
9860d15d33 | ||
|
166de417f1 | ||
|
65f416378a | ||
|
de0b179b0c | ||
|
8b0c62fbdb | ||
|
0d512f041f | ||
|
9efeb8926f | ||
|
389bbfcade | ||
|
0b8427a004 | ||
|
eb03fa4d4e | ||
|
8a470772e3 | ||
|
853f3c40bc | ||
|
fed44f328d | ||
|
0a7a9b6c37 | ||
|
a1d00f2c41 | ||
|
95a68f2c2d | ||
|
db7c0c45f6 | ||
|
a1bfbdf32e | ||
|
82bca03162 | ||
|
043c04778f | ||
|
a07983ddc8 | ||
|
b9d5330db6 | ||
|
538ca972e6 | ||
|
560cd81a1c | ||
|
df3a87fabf | ||
|
6eae98c1d4 | ||
|
6ceeccf583 | ||
|
9b0b14b847 | ||
|
78f4c0f002 | ||
|
6cff2f0437 | ||
|
6cefb180d6 | ||
|
59a44155c5 | ||
|
d0ad9c6b17 | ||
|
58a406b114 | ||
|
8a85695dc5 | ||
|
7ed8feee6f | ||
|
de67c0ad9f | ||
|
b8d11d31a6 | ||
|
d630ceaffe | ||
|
a89e60f296 | ||
|
a5d9abf1c8 | ||
|
d97dea2573 | ||
|
bc58f6b988 | ||
|
9cce604ca8 | ||
|
ed8e3f34fb | ||
|
91315c88c3 | ||
|
9267f881d6 | ||
|
c2ddb7e2fe | ||
|
c90ecd336c | ||
|
d8b1da3ddd | ||
|
58e86382fe | ||
|
2080c4419e | ||
|
b582a4a06d | ||
|
a5c6a864de | ||
|
5082c1ba3b | ||
|
cceb08b1b5 | ||
|
4c34e58945 | ||
|
72de1901a1 | ||
|
65fefcdd87 | ||
|
8e753eda72 | ||
|
7137c94fa2 | ||
|
52ea7dfa61 | ||
|
093925ed0e | ||
|
356afd18c4 | ||
|
4491f2d8f1 | ||
|
4a401957c7 | ||
|
539785acae | ||
|
3c63346d3a | ||
|
0c673f6cca | ||
|
10f4cbf11f | ||
|
a6a8c32326 | ||
|
99a474dba7 | ||
|
e439f4e5aa | ||
|
ae2ecf1540 | ||
|
10214ea5dc | ||
|
918cd414a8 | ||
|
f9a125acee | ||
|
ef5bcac925 | ||
|
6cbeb29b4e | ||
|
fbe344c0df | ||
|
5821f65a63 | ||
|
93af7573ac | ||
|
cddda1148e | ||
|
9c37eeeda6 | ||
|
eadf5bef77 | ||
|
c501c85eb8 | ||
|
5d4c7c2cbf | ||
|
08f0bf9c67 | ||
|
654dd97793 | ||
|
2e7baf8c89 | ||
|
7ca7a95070 | ||
|
71c49c8b90 | ||
|
9832915eba | ||
|
b98c8629e5 |
1062
backend/package-lock.json
generated
1062
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -106,6 +106,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-iam": "^3.525.0",
|
||||
"@aws-sdk/client-kms": "^3.609.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||
"@aws-sdk/client-sts": "^3.600.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -9,6 +9,7 @@ import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream
|
||||
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
|
||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
||||
@@ -163,6 +164,7 @@ declare module "fastify" {
|
||||
secretSharing: TSecretSharingServiceFactory;
|
||||
rateLimit: TRateLimitServiceFactory;
|
||||
userEngagement: TUserEngagementServiceFactory;
|
||||
externalKms: TExternalKmsServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@@ -59,6 +59,9 @@ import {
|
||||
TDynamicSecrets,
|
||||
TDynamicSecretsInsert,
|
||||
TDynamicSecretsUpdate,
|
||||
TExternalKms,
|
||||
TExternalKmsInsert,
|
||||
TExternalKmsUpdate,
|
||||
TGitAppInstallSessions,
|
||||
TGitAppInstallSessionsInsert,
|
||||
TGitAppInstallSessionsUpdate,
|
||||
@@ -125,6 +128,9 @@ import {
|
||||
TIntegrations,
|
||||
TIntegrationsInsert,
|
||||
TIntegrationsUpdate,
|
||||
TInternalKms,
|
||||
TInternalKmsInsert,
|
||||
TInternalKmsUpdate,
|
||||
TKmsKeys,
|
||||
TKmsKeysInsert,
|
||||
TKmsKeysUpdate,
|
||||
@@ -656,6 +662,8 @@ declare module "knex/types/tables" {
|
||||
TKmsRootConfigInsert,
|
||||
TKmsRootConfigUpdate
|
||||
>;
|
||||
[TableName.InternalKms]: KnexOriginal.CompositeTableType<TInternalKms, TInternalKmsInsert, TInternalKmsUpdate>;
|
||||
[TableName.ExternalKms]: KnexOriginal.CompositeTableType<TExternalKms, TExternalKmsInsert, TExternalKmsUpdate>;
|
||||
[TableName.KmsKey]: KnexOriginal.CompositeTableType<TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate>;
|
||||
[TableName.KmsKeyVersion]: KnexOriginal.CompositeTableType<
|
||||
TKmsKeyVersions,
|
||||
|
256
backend/src/db/migrations/20240708100026_external-kms.ts
Normal file
256
backend/src/db/migrations/20240708100026_external-kms.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const createInternalKmsTableAndBackfillData = async (knex: Knex) => {
|
||||
const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey);
|
||||
const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms);
|
||||
|
||||
// building the internal kms table by filling from old kms table
|
||||
if (doesOldKmsKeyTableExist && !doesInternalKmsTableExist) {
|
||||
await knex.schema.createTable(TableName.InternalKms, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
tb.binary("encryptedKey").notNullable();
|
||||
tb.string("encryptionAlgorithm").notNullable();
|
||||
tb.integer("version").defaultTo(1).notNullable();
|
||||
tb.uuid("kmsKeyId").unique().notNullable();
|
||||
tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
// copy the old kms and backfill
|
||||
const oldKmsKey = await knex(TableName.KmsKey).select("version", "encryptedKey", "encryptionAlgorithm", "id");
|
||||
if (oldKmsKey.length) {
|
||||
await knex(TableName.InternalKms).insert(
|
||||
oldKmsKey.map((el) => ({
|
||||
encryptionAlgorithm: el.encryptionAlgorithm,
|
||||
encryptedKey: el.encryptedKey,
|
||||
kmsKeyId: el.id,
|
||||
version: el.version
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renameKmsKeyVersionTableAsInternalKmsKeyVersion = async (knex: Knex) => {
|
||||
const doesOldKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.KmsKeyVersion);
|
||||
const doesNewKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.InternalKmsKeyVersion);
|
||||
|
||||
if (doesOldKmsKeyVersionTableExist && !doesNewKmsKeyVersionTableExist) {
|
||||
// because we haven't started using versioning for kms thus no data exist
|
||||
await knex.schema.renameTable(TableName.KmsKeyVersion, TableName.InternalKmsKeyVersion);
|
||||
const hasKmsKeyIdColumn = await knex.schema.hasColumn(TableName.InternalKmsKeyVersion, "kmsKeyId");
|
||||
const hasInternalKmsIdColumn = await knex.schema.hasColumn(TableName.InternalKmsKeyVersion, "internalKmsId");
|
||||
|
||||
await knex.schema.alterTable(TableName.InternalKmsKeyVersion, (tb) => {
|
||||
if (hasKmsKeyIdColumn) tb.dropColumn("kmsKeyId");
|
||||
if (!hasInternalKmsIdColumn) {
|
||||
tb.uuid("internalKmsId").notNullable();
|
||||
tb.foreign("internalKmsId").references("id").inTable(TableName.InternalKms).onDelete("CASCADE");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createExternalKmsKeyTable = async (knex: Knex) => {
|
||||
const doesExternalKmsServiceExist = await knex.schema.hasTable(TableName.ExternalKms);
|
||||
if (!doesExternalKmsServiceExist) {
|
||||
await knex.schema.createTable(TableName.ExternalKms, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
tb.string("provider").notNullable();
|
||||
tb.binary("encryptedProviderInputs").notNullable();
|
||||
tb.string("status");
|
||||
tb.string("statusDetails");
|
||||
tb.uuid("kmsKeyId").unique().notNullable();
|
||||
tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeNonRequiredFieldsFromKmsKeyTableAndBackfillRequiredData = async (knex: Knex) => {
|
||||
const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey);
|
||||
|
||||
// building the internal kms table by filling from old kms table
|
||||
if (doesOldKmsKeyTableExist) {
|
||||
const hasSlugColumn = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||
const hasEncryptedKeyColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptedKey");
|
||||
const hasEncryptionAlgorithmColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptionAlgorithm");
|
||||
const hasVersionColumn = await knex.schema.hasColumn(TableName.KmsKey, "version");
|
||||
const hasTimestamps = await knex.schema.hasColumn(TableName.KmsKey, "createdAt");
|
||||
const hasProjectId = await knex.schema.hasColumn(TableName.KmsKey, "projectId");
|
||||
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
||||
|
||||
await knex.schema.alterTable(TableName.KmsKey, (tb) => {
|
||||
if (!hasSlugColumn) tb.string("slug", 32);
|
||||
if (hasEncryptedKeyColumn) tb.dropColumn("encryptedKey");
|
||||
if (hasEncryptionAlgorithmColumn) tb.dropColumn("encryptionAlgorithm");
|
||||
if (hasVersionColumn) tb.dropColumn("version");
|
||||
if (!hasTimestamps) tb.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
// backfill all org id in kms key because its gonna be changed to non nullable
|
||||
if (hasProjectId && hasOrgId) {
|
||||
await knex(TableName.KmsKey)
|
||||
.whereNull("orgId")
|
||||
.update({
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore because generate schema happens after this
|
||||
orgId: knex(TableName.Project)
|
||||
.select("orgId")
|
||||
.where("id", knex.raw("??", [`${TableName.KmsKey}.projectId`]))
|
||||
});
|
||||
}
|
||||
|
||||
// backfill slugs in kms
|
||||
const missingSlugs = await knex(TableName.KmsKey).whereNull("slug").select("id");
|
||||
if (missingSlugs.length) {
|
||||
await knex(TableName.KmsKey)
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore because generate schema happens after this
|
||||
.insert(missingSlugs.map(({ id }) => ({ id, slug: slugify(alphaNumericNanoId(8).toLowerCase()) })))
|
||||
.onConflict("id")
|
||||
.merge();
|
||||
}
|
||||
|
||||
await knex.schema.alterTable(TableName.KmsKey, (tb) => {
|
||||
if (hasOrgId) tb.uuid("orgId").notNullable().alter();
|
||||
tb.string("slug", 32).notNullable().alter();
|
||||
if (hasProjectId) tb.dropColumn("projectId");
|
||||
if (hasOrgId) tb.unique(["orgId", "slug"]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* The goal for this migration is split the existing kms key into three table
|
||||
* the kms-key table would be a container table that contains
|
||||
* the internal kms key table and external kms table
|
||||
*/
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await createInternalKmsTableAndBackfillData(knex);
|
||||
await renameKmsKeyVersionTableAsInternalKmsKeyVersion(knex);
|
||||
await removeNonRequiredFieldsFromKmsKeyTableAndBackfillRequiredData(knex);
|
||||
await createExternalKmsKeyTable(knex);
|
||||
|
||||
const doesOrgKmsKeyExist = await knex.schema.hasColumn(TableName.Organization, "kmsDefaultKeyId");
|
||||
if (!doesOrgKmsKeyExist) {
|
||||
await knex.schema.alterTable(TableName.Organization, (tb) => {
|
||||
tb.uuid("kmsDefaultKeyId").nullable();
|
||||
tb.foreign("kmsDefaultKeyId").references("id").inTable(TableName.KmsKey);
|
||||
});
|
||||
}
|
||||
|
||||
const doesProjectKmsSecretManagerKeyExist = await knex.schema.hasColumn(TableName.Project, "kmsSecretManagerKeyId");
|
||||
if (!doesProjectKmsSecretManagerKeyExist) {
|
||||
await knex.schema.alterTable(TableName.Project, (tb) => {
|
||||
tb.uuid("kmsSecretManagerKeyId").nullable();
|
||||
tb.foreign("kmsSecretManagerKeyId").references("id").inTable(TableName.KmsKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const renameInternalKmsKeyVersionBackToKmsKeyVersion = async (knex: Knex) => {
|
||||
const doesInternalKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.InternalKmsKeyVersion);
|
||||
const doesKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.KmsKeyVersion);
|
||||
if (doesInternalKmsKeyVersionTableExist && !doesKmsKeyVersionTableExist) {
|
||||
// because we haven't started using versioning for kms thus no data exist
|
||||
await knex.schema.renameTable(TableName.InternalKmsKeyVersion, TableName.KmsKeyVersion);
|
||||
const hasInternalKmsIdColumn = await knex.schema.hasColumn(TableName.KmsKeyVersion, "internalKmsId");
|
||||
const hasKmsKeyIdColumn = await knex.schema.hasColumn(TableName.KmsKeyVersion, "kmsKeyId");
|
||||
|
||||
await knex.schema.alterTable(TableName.KmsKeyVersion, (tb) => {
|
||||
if (hasInternalKmsIdColumn) tb.dropColumn("internalKmsId");
|
||||
if (!hasKmsKeyIdColumn) {
|
||||
tb.uuid("kmsKeyId").notNullable();
|
||||
tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const bringBackKmsKeyFields = async (knex: Knex) => {
|
||||
const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey);
|
||||
const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms);
|
||||
if (doesOldKmsKeyTableExist && doesInternalKmsTableExist) {
|
||||
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||
const hasEncryptedKeyColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptedKey");
|
||||
const hasEncryptionAlgorithmColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptionAlgorithm");
|
||||
const hasVersionColumn = await knex.schema.hasColumn(TableName.KmsKey, "version");
|
||||
const hasNullableOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
||||
const hasProjectIdColumn = await knex.schema.hasColumn(TableName.KmsKey, "projectId");
|
||||
|
||||
await knex.schema.alterTable(TableName.KmsKey, (tb) => {
|
||||
if (!hasEncryptedKeyColumn) tb.binary("encryptedKey");
|
||||
if (!hasEncryptionAlgorithmColumn) tb.string("encryptionAlgorithm");
|
||||
if (!hasVersionColumn) tb.integer("version").defaultTo(1);
|
||||
if (hasNullableOrgId) tb.uuid("orgId").nullable().alter();
|
||||
if (!hasProjectIdColumn) {
|
||||
tb.string("projectId");
|
||||
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
}
|
||||
if (hasSlug) tb.dropColumn("slug");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const backfillKmsKeyFromInternalKmsTable = async (knex: Knex) => {
|
||||
const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey);
|
||||
const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms);
|
||||
if (doesInternalKmsTableExist && doesOldKmsKeyTableExist) {
|
||||
// backfill kms key with internal kms data
|
||||
await knex(TableName.KmsKey).update({
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore because generate schema happens after this
|
||||
encryptedKey: knex(TableName.InternalKms)
|
||||
.select("encryptedKey")
|
||||
.where("kmsKeyId", knex.raw("??", [`${TableName.KmsKey}.id`])),
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore because generate schema happens after this
|
||||
encryptionAlgorithm: knex(TableName.InternalKms)
|
||||
.select("encryptionAlgorithm")
|
||||
.where("kmsKeyId", knex.raw("??", [`${TableName.KmsKey}.id`])),
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore because generate schema happens after this
|
||||
projectId: knex(TableName.Project)
|
||||
.select("id")
|
||||
.where("kmsCertificateKeyId", knex.raw("??", [`${TableName.KmsKey}.id`]))
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesOrgKmsKeyExist = await knex.schema.hasColumn(TableName.Organization, "kmsDefaultKeyId");
|
||||
if (doesOrgKmsKeyExist) {
|
||||
await knex.schema.alterTable(TableName.Organization, (tb) => {
|
||||
tb.dropColumn("kmsDefaultKeyId");
|
||||
});
|
||||
}
|
||||
|
||||
const doesProjectKmsSecretManagerKeyExist = await knex.schema.hasColumn(TableName.Project, "kmsSecretManagerKeyId");
|
||||
if (doesProjectKmsSecretManagerKeyExist) {
|
||||
await knex.schema.alterTable(TableName.Project, (tb) => {
|
||||
tb.dropColumn("kmsSecretManagerKeyId");
|
||||
});
|
||||
}
|
||||
|
||||
await renameInternalKmsKeyVersionBackToKmsKeyVersion(knex);
|
||||
await bringBackKmsKeyFields(knex);
|
||||
await backfillKmsKeyFromInternalKmsTable(knex);
|
||||
|
||||
const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey);
|
||||
if (doesOldKmsKeyTableExist) {
|
||||
await knex.schema.alterTable(TableName.KmsKey, (tb) => {
|
||||
tb.binary("encryptedKey").notNullable().alter();
|
||||
tb.string("encryptionAlgorithm").notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms);
|
||||
if (doesInternalKmsTableExist) await knex.schema.dropTable(TableName.InternalKms);
|
||||
|
||||
const doesExternalKmsServiceExist = await knex.schema.hasTable(TableName.ExternalKms);
|
||||
if (doesExternalKmsServiceExist) await knex.schema.dropTable(TableName.ExternalKms);
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.OrgMembership)) {
|
||||
const doesUserIdExist = await knex.schema.hasColumn(TableName.OrgMembership, "userId");
|
||||
const doesOrgIdExist = await knex.schema.hasColumn(TableName.OrgMembership, "orgId");
|
||||
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
|
||||
t.boolean("isActive").notNullable().defaultTo(true);
|
||||
if (doesUserIdExist && doesOrgIdExist) t.index(["userId", "orgId"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.OrgMembership)) {
|
||||
const doesUserIdExist = await knex.schema.hasColumn(TableName.OrgMembership, "userId");
|
||||
const doesOrgIdExist = await knex.schema.hasColumn(TableName.OrgMembership, "orgId");
|
||||
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
|
||||
t.dropColumn("isActive");
|
||||
if (doesUserIdExist && doesOrgIdExist) t.dropIndex(["userId", "orgId"]);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasKmsDataKeyCol = await knex.schema.hasColumn(TableName.Organization, "kmsEncryptedDataKey");
|
||||
await knex.schema.alterTable(TableName.Organization, (tb) => {
|
||||
if (!hasKmsDataKeyCol) {
|
||||
tb.binary("kmsEncryptedDataKey");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasKmsDataKeyCol = await knex.schema.hasColumn(TableName.Organization, "kmsEncryptedDataKey");
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
if (hasKmsDataKeyCol) {
|
||||
t.dropColumn("kmsEncryptedDataKey");
|
||||
}
|
||||
});
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasKmsSecretManagerEncryptedDataKey = await knex.schema.hasColumn(
|
||||
TableName.Project,
|
||||
"kmsSecretManagerEncryptedDataKey"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Project, (tb) => {
|
||||
if (!hasKmsSecretManagerEncryptedDataKey) {
|
||||
tb.binary("kmsSecretManagerEncryptedDataKey");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasKmsSecretManagerEncryptedDataKey = await knex.schema.hasColumn(
|
||||
TableName.Project,
|
||||
"kmsSecretManagerEncryptedDataKey"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||
if (hasKmsSecretManagerEncryptedDataKey) {
|
||||
t.dropColumn("kmsSecretManagerEncryptedDataKey");
|
||||
}
|
||||
});
|
||||
}
|
23
backend/src/db/schemas/external-kms.ts
Normal file
23
backend/src/db/schemas/external-kms.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ExternalKmsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
provider: z.string(),
|
||||
encryptedProviderInputs: zodBuffer,
|
||||
status: z.string().nullable().optional(),
|
||||
statusDetails: z.string().nullable().optional(),
|
||||
kmsKeyId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TExternalKms = z.infer<typeof ExternalKmsSchema>;
|
||||
export type TExternalKmsInsert = Omit<z.input<typeof ExternalKmsSchema>, TImmutableDBKeys>;
|
||||
export type TExternalKmsUpdate = Partial<Omit<z.input<typeof ExternalKmsSchema>, TImmutableDBKeys>>;
|
@@ -17,6 +17,7 @@ export * from "./certificate-secrets";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
export * from "./external-kms";
|
||||
export * from "./git-app-install-sessions";
|
||||
export * from "./git-app-org";
|
||||
export * from "./group-project-membership-roles";
|
||||
@@ -39,6 +40,7 @@ export * from "./identity-universal-auths";
|
||||
export * from "./incident-contacts";
|
||||
export * from "./integration-auths";
|
||||
export * from "./integrations";
|
||||
export * from "./internal-kms";
|
||||
export * from "./kms-key-versions";
|
||||
export * from "./kms-keys";
|
||||
export * from "./kms-root-config";
|
||||
|
21
backend/src/db/schemas/internal-kms-key-version.ts
Normal file
21
backend/src/db/schemas/internal-kms-key-version.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const InternalKmsKeyVersionSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedKey: zodBuffer,
|
||||
version: z.number(),
|
||||
internalKmsId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TInternalKmsKeyVersion = z.infer<typeof InternalKmsKeyVersionSchema>;
|
||||
export type TInternalKmsKeyVersionInsert = Omit<z.input<typeof InternalKmsKeyVersionSchema>, TImmutableDBKeys>;
|
||||
export type TInternalKmsKeyVersionUpdate = Partial<Omit<z.input<typeof InternalKmsKeyVersionSchema>, TImmutableDBKeys>>;
|
22
backend/src/db/schemas/internal-kms.ts
Normal file
22
backend/src/db/schemas/internal-kms.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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 InternalKmsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedKey: zodBuffer,
|
||||
encryptionAlgorithm: z.string(),
|
||||
version: z.number().default(1),
|
||||
kmsKeyId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TInternalKms = z.infer<typeof InternalKmsSchema>;
|
||||
export type TInternalKmsInsert = Omit<z.input<typeof InternalKmsSchema>, TImmutableDBKeys>;
|
||||
export type TInternalKmsUpdate = Partial<Omit<z.input<typeof InternalKmsSchema>, TImmutableDBKeys>>;
|
@@ -5,20 +5,17 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const KmsKeysSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedKey: zodBuffer,
|
||||
encryptionAlgorithm: z.string(),
|
||||
version: z.number().default(1),
|
||||
description: z.string().nullable().optional(),
|
||||
isDisabled: z.boolean().default(false).nullable().optional(),
|
||||
isReserved: z.boolean().default(true).nullable().optional(),
|
||||
projectId: z.string().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional()
|
||||
orgId: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||
|
@@ -96,6 +96,10 @@ export enum TableName {
|
||||
// KMS Service
|
||||
KmsServerRootConfig = "kms_root_config",
|
||||
KmsKey = "kms_keys",
|
||||
ExternalKms = "external_kms",
|
||||
InternalKms = "internal_kms",
|
||||
InternalKmsKeyVersion = "internal_kms_key_version",
|
||||
// @depreciated
|
||||
KmsKeyVersion = "kms_key_versions"
|
||||
}
|
||||
|
||||
|
@@ -17,7 +17,8 @@ export const OrgMembershipsSchema = z.object({
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
orgId: z.string().uuid(),
|
||||
roleId: z.string().uuid().nullable().optional(),
|
||||
projectFavorites: z.string().array().nullable().optional()
|
||||
projectFavorites: z.string().array().nullable().optional(),
|
||||
isActive: z.boolean()
|
||||
});
|
||||
|
||||
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;
|
||||
|
@@ -5,6 +5,8 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const OrganizationsSchema = z.object({
|
||||
@@ -15,7 +17,9 @@ export const OrganizationsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
authEnforced: z.boolean().default(false).nullable().optional(),
|
||||
scimEnabled: z.boolean().default(false).nullable().optional()
|
||||
scimEnabled: z.boolean().default(false).nullable().optional(),
|
||||
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
|
||||
kmsEncryptedDataKey: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
@@ -5,6 +5,8 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ProjectsSchema = z.object({
|
||||
@@ -19,7 +21,9 @@ export const ProjectsSchema = z.object({
|
||||
upgradeStatus: z.string().nullable().optional(),
|
||||
pitVersionLimit: z.number().default(10),
|
||||
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
||||
auditLogsRetentionDays: z.number().nullable().optional()
|
||||
auditLogsRetentionDays: z.number().nullable().optional(),
|
||||
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
|
||||
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||
|
@@ -29,7 +29,8 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
role: OrgMembershipRole.Admin,
|
||||
orgId: org.id,
|
||||
status: OrgMembershipStatus.Accepted,
|
||||
userId: user.id
|
||||
userId: user.id,
|
||||
isActive: true
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
289
backend/src/ee/routes/v1/external-kms-router.ts
Normal file
289
backend/src/ee/routes/v1/external-kms-router.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
ExternalKmsAwsSchema,
|
||||
ExternalKmsInputSchema,
|
||||
ExternalKmsInputUpdateSchema
|
||||
} from "@app/ee/services/external-kms/providers/model";
|
||||
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";
|
||||
|
||||
const sanitizedExternalSchema = KmsKeysSchema.extend({
|
||||
external: ExternalKmsSchema.pick({
|
||||
id: true,
|
||||
status: true,
|
||||
statusDetails: true,
|
||||
provider: true
|
||||
})
|
||||
});
|
||||
|
||||
const sanitizedExternalSchemaForGetAll = KmsKeysSchema.pick({
|
||||
id: true,
|
||||
description: true,
|
||||
isDisabled: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
slug: true
|
||||
})
|
||||
.extend({
|
||||
externalKms: ExternalKmsSchema.pick({
|
||||
provider: true,
|
||||
status: true,
|
||||
statusDetails: true
|
||||
})
|
||||
})
|
||||
.array();
|
||||
|
||||
const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({
|
||||
external: ExternalKmsSchema.pick({
|
||||
id: true,
|
||||
status: true,
|
||||
statusDetails: true,
|
||||
provider: true
|
||||
}).extend({
|
||||
providerInput: ExternalKmsAwsSchema
|
||||
})
|
||||
});
|
||||
|
||||
export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
slug: z.string().min(1).trim().toLowerCase(),
|
||||
description: z.string().min(1).trim().optional(),
|
||||
provider: ExternalKmsInputSchema
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
externalKms: sanitizedExternalSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKms = await server.services.externalKms.create({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
slug: req.body.slug,
|
||||
provider: req.body.provider,
|
||||
description: req.body.description
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.CREATE_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
provider: req.body.provider.type,
|
||||
slug: req.body.slug,
|
||||
description: req.body.description
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { externalKms };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().trim().min(1)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z.string().min(1).trim().toLowerCase().optional(),
|
||||
description: z.string().min(1).trim().optional(),
|
||||
provider: ExternalKmsInputUpdateSchema
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
externalKms: sanitizedExternalSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKms = await server.services.externalKms.updateById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
slug: req.body.slug,
|
||||
provider: req.body.provider,
|
||||
description: req.body.description,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
provider: req.body.provider.type,
|
||||
slug: req.body.slug,
|
||||
description: req.body.description
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { externalKms };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().trim().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
externalKms: sanitizedExternalSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKms = await server.services.externalKms.deleteById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.DELETE_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
slug: externalKms.slug
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { externalKms };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().trim().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
externalKms: sanitizedExternalSchemaForGetById
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKms = await server.services.externalKms.findById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.GET_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
slug: externalKms.slug
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { externalKms };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
externalKmsList: sanitizedExternalSchemaForGetAll
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKmsList = await server.services.externalKms.list({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return { externalKmsList };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/slug/:slug",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
externalKms: sanitizedExternalSchemaForGetById
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKms = await server.services.externalKms.findBySlug({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
slug: req.params.slug
|
||||
});
|
||||
return { externalKms };
|
||||
}
|
||||
});
|
||||
};
|
@@ -4,6 +4,7 @@ import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||
import { registerExternalKmsRouter } from "./external-kms-router";
|
||||
import { registerGroupRouter } from "./group-router";
|
||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||
import { registerLdapRouter } from "./ldap-router";
|
||||
@@ -87,4 +88,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
{ prefix: "/additional-privilege" }
|
||||
);
|
||||
|
||||
await server.register(registerExternalKmsRouter, {
|
||||
prefix: "/external-kms"
|
||||
});
|
||||
};
|
||||
|
@@ -4,7 +4,7 @@ import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
|
||||
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
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";
|
||||
|
||||
@@ -171,4 +171,178 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async () => ({ actors: [] })
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/kms",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretManagerKmsKey: z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
isExternal: z.boolean()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const kmsKeys = await server.services.project.getProjectKmsKeys({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
||||
return kmsKeys;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:workspaceId/kms",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
secretManagerKmsKeyId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretManagerKmsKey: z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
isExternal: z.boolean()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { secretManagerKmsKey } = await server.services.project.updateProjectKmsKey({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PROJECT_KMS,
|
||||
metadata: {
|
||||
secretManagerKmsKey: {
|
||||
id: secretManagerKmsKey.id,
|
||||
slug: secretManagerKmsKey.slug
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
secretManagerKmsKey
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/kms/backup",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretManager: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const backup = await server.services.project.getProjectKmsBackup({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.GET_PROJECT_KMS_BACKUP,
|
||||
metadata: {}
|
||||
}
|
||||
});
|
||||
|
||||
return backup;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:workspaceId/kms/backup",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
backup: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretManagerKmsKey: z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
isExternal: z.boolean()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const backup = await server.services.project.loadProjectKmsBackup({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
backup: req.body.backup
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.LOAD_PROJECT_KMS_BACKUP,
|
||||
metadata: {}
|
||||
}
|
||||
});
|
||||
|
||||
return backup;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -186,7 +186,13 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
),
|
||||
displayName: z.string().trim(),
|
||||
active: z.boolean()
|
||||
active: z.boolean(),
|
||||
groups: z.array(
|
||||
z.object({
|
||||
value: z.string().trim(),
|
||||
display: z.string().trim()
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -475,10 +481,13 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
z.object({
|
||||
op: z.literal("add"),
|
||||
value: z.object({
|
||||
value: z.string().trim(),
|
||||
display: z.string().trim().optional()
|
||||
})
|
||||
path: z.string().trim(),
|
||||
value: z.array(
|
||||
z.object({
|
||||
value: z.string().trim(),
|
||||
display: z.string().trim().optional()
|
||||
})
|
||||
)
|
||||
})
|
||||
])
|
||||
)
|
||||
@@ -569,7 +578,13 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
),
|
||||
displayName: z.string().trim(),
|
||||
active: z.boolean()
|
||||
active: z.boolean(),
|
||||
groups: z.array(
|
||||
z.object({
|
||||
value: z.string().trim(),
|
||||
display: z.string().trim()
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
@@ -138,7 +138,14 @@ export enum EventType {
|
||||
GET_CERT = "get-cert",
|
||||
DELETE_CERT = "delete-cert",
|
||||
REVOKE_CERT = "revoke-cert",
|
||||
GET_CERT_BODY = "get-cert-body"
|
||||
GET_CERT_BODY = "get-cert-body",
|
||||
CREATE_KMS = "create-kms",
|
||||
UPDATE_KMS = "update-kms",
|
||||
DELETE_KMS = "delete-kms",
|
||||
GET_KMS = "get-kms",
|
||||
UPDATE_PROJECT_KMS = "update-project-kms",
|
||||
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
|
||||
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@@ -1164,6 +1171,62 @@ interface GetCertBody {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateKmsEvent {
|
||||
type: EventType.CREATE_KMS;
|
||||
metadata: {
|
||||
kmsId: string;
|
||||
provider: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteKmsEvent {
|
||||
type: EventType.DELETE_KMS;
|
||||
metadata: {
|
||||
kmsId: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateKmsEvent {
|
||||
type: EventType.UPDATE_KMS;
|
||||
metadata: {
|
||||
kmsId: string;
|
||||
provider: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetKmsEvent {
|
||||
type: EventType.GET_KMS;
|
||||
metadata: {
|
||||
kmsId: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateProjectKmsEvent {
|
||||
type: EventType.UPDATE_PROJECT_KMS;
|
||||
metadata: {
|
||||
secretManagerKmsKey: {
|
||||
id: string;
|
||||
slug: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectKmsBackupEvent {
|
||||
type: EventType.GET_PROJECT_KMS_BACKUP;
|
||||
metadata: Record<string, string>; // no metadata yet
|
||||
}
|
||||
|
||||
interface LoadProjectKmsBackupEvent {
|
||||
type: EventType.LOAD_PROJECT_KMS_BACKUP;
|
||||
metadata: Record<string, string>; // no metadata yet
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -1264,4 +1327,11 @@ export type Event =
|
||||
| GetCert
|
||||
| DeleteCert
|
||||
| RevokeCert
|
||||
| GetCertBody;
|
||||
| GetCertBody
|
||||
| CreateKmsEvent
|
||||
| UpdateKmsEvent
|
||||
| DeleteKmsEvent
|
||||
| GetKmsEvent
|
||||
| UpdateProjectKmsEvent
|
||||
| GetProjectKmsBackupEvent
|
||||
| LoadProjectKmsBackupEvent;
|
||||
|
@@ -17,7 +17,7 @@ type TCertificateAuthorityCrlServiceFactoryDep = {
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decrypt" | "generateKmsKey">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
@@ -68,11 +68,11 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const decryptedCrl = await kmsService.decrypt({
|
||||
kmsId: keyId,
|
||||
cipherTextBlob: caCrl.encryptedCrl
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
|
||||
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
|
||||
const crl = new x509.X509Crl(decryptedCrl);
|
||||
|
||||
const base64crl = crl.toString("base64");
|
||||
|
49
backend/src/ee/services/external-kms/external-kms-dal.ts
Normal file
49
backend/src/ee/services/external-kms/external-kms-dal.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TKmsKeys } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TExternalKmsDALFactory = ReturnType<typeof externalKmsDALFactory>;
|
||||
|
||||
export const externalKmsDALFactory = (db: TDbClient) => {
|
||||
const externalKmsOrm = ormify(db, TableName.ExternalKms);
|
||||
|
||||
const find = async (filter: Partial<TKmsKeys>, tx?: Knex) => {
|
||||
try {
|
||||
const result = await (tx || db.replicaNode())(TableName.ExternalKms)
|
||||
.join(TableName.KmsKey, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`)
|
||||
.where(filter)
|
||||
.select(selectAllTableCols(TableName.KmsKey))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ExternalKms).as("externalKmsId"),
|
||||
db.ref("provider").withSchema(TableName.ExternalKms).as("externalKmsProvider"),
|
||||
db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"),
|
||||
db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"),
|
||||
db.ref("statusDetails").withSchema(TableName.ExternalKms).as("externalKmsStatusDetails")
|
||||
);
|
||||
|
||||
return result.map((el) => ({
|
||||
id: el.id,
|
||||
description: el.description,
|
||||
isDisabled: el.isDisabled,
|
||||
isReserved: el.isReserved,
|
||||
orgId: el.orgId,
|
||||
slug: el.slug,
|
||||
createdAt: el.createdAt,
|
||||
updatedAt: el.updatedAt,
|
||||
externalKms: {
|
||||
id: el.externalKmsId,
|
||||
provider: el.externalKmsProvider,
|
||||
status: el.externalKmsStatus,
|
||||
statusDetails: el.externalKmsStatusDetails
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...externalKmsOrm, find };
|
||||
};
|
338
backend/src/ee/services/external-kms/external-kms-service.ts
Normal file
338
backend/src/ee/services/external-kms/external-kms-service.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TExternalKmsDALFactory } from "./external-kms-dal";
|
||||
import {
|
||||
TCreateExternalKmsDTO,
|
||||
TDeleteExternalKmsDTO,
|
||||
TGetExternalKmsByIdDTO,
|
||||
TGetExternalKmsBySlugDTO,
|
||||
TListExternalKmsDTO,
|
||||
TUpdateExternalKmsDTO
|
||||
} from "./external-kms-types";
|
||||
import { AwsKmsProviderFactory } from "./providers/aws-kms";
|
||||
import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model";
|
||||
|
||||
type TExternalKmsServiceFactoryDep = {
|
||||
externalKmsDAL: TExternalKmsDALFactory;
|
||||
kmsService: Pick<
|
||||
TKmsServiceFactory,
|
||||
"getOrgKmsKeyId" | "decryptWithInputKey" | "encryptWithInputKey" | "getOrgKmsDataKey"
|
||||
>;
|
||||
kmsDAL: Pick<TKmsKeyDALFactory, "create" | "updateById" | "findById" | "deleteById" | "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TExternalKmsServiceFactory = ReturnType<typeof externalKmsServiceFactory>;
|
||||
|
||||
export const externalKmsServiceFactory = ({
|
||||
externalKmsDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService,
|
||||
kmsDAL
|
||||
}: TExternalKmsServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
provider,
|
||||
description,
|
||||
actor,
|
||||
slug,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TCreateExternalKmsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.externalKms) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create external KMS due to plan restriction. Upgrade to the Enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
const kmsSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||
|
||||
let sanitizedProviderInput = "";
|
||||
switch (provider.type) {
|
||||
case KmsProviders.Aws:
|
||||
{
|
||||
const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs });
|
||||
// if missing kms key this generate a new kms key id and returns new provider input
|
||||
const newProviderInput = await externalKms.generateInputKmsKey();
|
||||
sanitizedProviderInput = JSON.stringify(newProviderInput);
|
||||
|
||||
await externalKms.validateConnection();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
||||
}
|
||||
|
||||
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(actorOrgId);
|
||||
const kmsEncryptor = await kmsService.encryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedProviderInputs } = kmsEncryptor({
|
||||
plainText: Buffer.from(sanitizedProviderInput, "utf8")
|
||||
});
|
||||
|
||||
const externalKms = await externalKmsDAL.transaction(async (tx) => {
|
||||
const kms = await kmsDAL.create(
|
||||
{
|
||||
isReserved: false,
|
||||
description,
|
||||
slug: kmsSlug,
|
||||
orgId: actorOrgId
|
||||
},
|
||||
tx
|
||||
);
|
||||
const externalKmsCfg = await externalKmsDAL.create(
|
||||
{
|
||||
provider: provider.type,
|
||||
encryptedProviderInputs,
|
||||
kmsKeyId: kms.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
return { ...kms, external: externalKmsCfg };
|
||||
});
|
||||
|
||||
return externalKms;
|
||||
};
|
||||
|
||||
const updateById = async ({
|
||||
provider,
|
||||
description,
|
||||
actor,
|
||||
id: kmsId,
|
||||
slug,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TUpdateExternalKmsDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
kmsDoc.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||
|
||||
const plan = await licenseService.getPlan(kmsDoc.orgId);
|
||||
if (!plan.externalKms) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update external KMS due to plan restriction. Upgrade to the Enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
const kmsSlug = slug ? slugify(slug) : undefined;
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
|
||||
let sanitizedProviderInput = "";
|
||||
if (provider) {
|
||||
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId);
|
||||
const kmsDecryptor = await kmsService.decryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
});
|
||||
|
||||
const decryptedProviderInputBlob = kmsDecryptor({
|
||||
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
||||
});
|
||||
|
||||
switch (provider.type) {
|
||||
case KmsProviders.Aws:
|
||||
{
|
||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
||||
);
|
||||
const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs };
|
||||
const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput });
|
||||
await externalKms.validateConnection();
|
||||
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
||||
}
|
||||
}
|
||||
|
||||
let encryptedProviderInputs: Buffer | undefined;
|
||||
if (sanitizedProviderInput) {
|
||||
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(actorOrgId);
|
||||
const kmsEncryptor = await kmsService.encryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
});
|
||||
const { cipherTextBlob } = kmsEncryptor({
|
||||
plainText: Buffer.from(sanitizedProviderInput, "utf8")
|
||||
});
|
||||
encryptedProviderInputs = cipherTextBlob;
|
||||
}
|
||||
|
||||
const externalKms = await externalKmsDAL.transaction(async (tx) => {
|
||||
const kms = await kmsDAL.updateById(
|
||||
kmsDoc.id,
|
||||
{
|
||||
description,
|
||||
slug: kmsSlug
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (encryptedProviderInputs) {
|
||||
const externalKmsCfg = await externalKmsDAL.updateById(
|
||||
externalKmsDoc.id,
|
||||
{
|
||||
encryptedProviderInputs
|
||||
},
|
||||
tx
|
||||
);
|
||||
return { ...kms, external: externalKmsCfg };
|
||||
}
|
||||
return { ...kms, external: externalKmsDoc };
|
||||
});
|
||||
|
||||
return externalKms;
|
||||
};
|
||||
|
||||
const deleteById = async ({ actor, id: kmsId, actorId, actorOrgId, actorAuthMethod }: TDeleteExternalKmsDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
kmsDoc.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
|
||||
const externalKms = await externalKmsDAL.transaction(async (tx) => {
|
||||
const kms = await kmsDAL.deleteById(kmsDoc.id, tx);
|
||||
return { ...kms, external: externalKmsDoc };
|
||||
});
|
||||
|
||||
return externalKms;
|
||||
};
|
||||
|
||||
const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListExternalKmsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId });
|
||||
|
||||
return externalKmsDocs;
|
||||
};
|
||||
|
||||
const findById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id: kmsId }: TGetExternalKmsByIdDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
kmsDoc.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
|
||||
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId);
|
||||
const kmsDecryptor = await kmsService.decryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
});
|
||||
|
||||
const decryptedProviderInputBlob = kmsDecryptor({
|
||||
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
||||
});
|
||||
switch (externalKmsDoc.provider) {
|
||||
case KmsProviders.Aws: {
|
||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
||||
);
|
||||
return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } };
|
||||
}
|
||||
default:
|
||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
||||
}
|
||||
};
|
||||
|
||||
const findBySlug = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
slug: kmsSlug
|
||||
}: TGetExternalKmsBySlugDTO) => {
|
||||
const kmsDoc = await kmsDAL.findOne({ slug: kmsSlug, orgId: actorOrgId });
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
kmsDoc.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
|
||||
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId);
|
||||
const kmsDecryptor = await kmsService.decryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
});
|
||||
|
||||
const decryptedProviderInputBlob = kmsDecryptor({
|
||||
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
||||
});
|
||||
|
||||
switch (externalKmsDoc.provider) {
|
||||
case KmsProviders.Aws: {
|
||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
||||
);
|
||||
return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } };
|
||||
}
|
||||
default:
|
||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
updateById,
|
||||
deleteById,
|
||||
list,
|
||||
findById,
|
||||
findBySlug
|
||||
};
|
||||
};
|
30
backend/src/ee/services/external-kms/external-kms-types.ts
Normal file
30
backend/src/ee/services/external-kms/external-kms-types.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
import { TExternalKmsInputSchema, TExternalKmsInputUpdateSchema } from "./providers/model";
|
||||
|
||||
export type TCreateExternalKmsDTO = {
|
||||
slug?: string;
|
||||
description?: string;
|
||||
provider: TExternalKmsInputSchema;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TUpdateExternalKmsDTO = {
|
||||
id: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
provider?: TExternalKmsInputUpdateSchema;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TDeleteExternalKmsDTO = {
|
||||
id: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TListExternalKmsDTO = Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetExternalKmsByIdDTO = {
|
||||
id: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetExternalKmsBySlugDTO = {
|
||||
slug: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
111
backend/src/ee/services/external-kms/providers/aws-kms.ts
Normal file
111
backend/src/ee/services/external-kms/providers/aws-kms.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { CreateKeyCommand, DecryptCommand, DescribeKeyCommand, EncryptCommand, KMSClient } from "@aws-sdk/client-kms";
|
||||
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
import { ExternalKmsAwsSchema, KmsAwsCredentialType, TExternalKmsAwsSchema, TExternalKmsProviderFns } from "./model";
|
||||
|
||||
const getAwsKmsClient = async (providerInputs: TExternalKmsAwsSchema) => {
|
||||
if (providerInputs.credential.type === KmsAwsCredentialType.AssumeRole) {
|
||||
const awsCredential = providerInputs.credential.data;
|
||||
const stsClient = new STSClient({
|
||||
region: providerInputs.awsRegion
|
||||
});
|
||||
const command = new AssumeRoleCommand({
|
||||
RoleArn: awsCredential.assumeRoleArn,
|
||||
RoleSessionName: `infisical-kms-${randomUUID()}`,
|
||||
DurationSeconds: 900, // 15mins
|
||||
ExternalId: awsCredential.externalId
|
||||
});
|
||||
const response = await stsClient.send(command);
|
||||
if (!response.Credentials?.AccessKeyId || !response.Credentials?.SecretAccessKey)
|
||||
throw new Error("Failed to assume role");
|
||||
|
||||
const kmsClient = new KMSClient({
|
||||
region: providerInputs.awsRegion,
|
||||
credentials: {
|
||||
accessKeyId: response.Credentials.AccessKeyId,
|
||||
secretAccessKey: response.Credentials.SecretAccessKey,
|
||||
sessionToken: response.Credentials.SessionToken,
|
||||
expiration: response.Credentials.Expiration
|
||||
}
|
||||
});
|
||||
return kmsClient;
|
||||
}
|
||||
const awsCredential = providerInputs.credential.data;
|
||||
const kmsClient = new KMSClient({
|
||||
region: providerInputs.awsRegion,
|
||||
credentials: {
|
||||
accessKeyId: awsCredential.accessKey,
|
||||
secretAccessKey: awsCredential.secretKey
|
||||
}
|
||||
});
|
||||
return kmsClient;
|
||||
};
|
||||
|
||||
type AwsKmsProviderArgs = {
|
||||
inputs: unknown;
|
||||
};
|
||||
type TAwsKmsProviderFactoryReturn = TExternalKmsProviderFns & {
|
||||
generateInputKmsKey: () => Promise<TExternalKmsAwsSchema>;
|
||||
};
|
||||
|
||||
export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Promise<TAwsKmsProviderFactoryReturn> => {
|
||||
let providerInputs = await ExternalKmsAwsSchema.parseAsync(inputs);
|
||||
let awsClient = await getAwsKmsClient(providerInputs);
|
||||
|
||||
const generateInputKmsKey = async () => {
|
||||
if (providerInputs.kmsKeyId) return providerInputs;
|
||||
|
||||
const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] });
|
||||
const kmsKey = await awsClient.send(command);
|
||||
|
||||
if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key");
|
||||
|
||||
const updatedProviderInputs = await ExternalKmsAwsSchema.parseAsync({
|
||||
...providerInputs,
|
||||
kmsKeyId: kmsKey.KeyMetadata?.KeyId
|
||||
});
|
||||
|
||||
providerInputs = updatedProviderInputs;
|
||||
awsClient = await getAwsKmsClient(providerInputs);
|
||||
|
||||
return updatedProviderInputs;
|
||||
};
|
||||
|
||||
const validateConnection = async () => {
|
||||
const command = new DescribeKeyCommand({
|
||||
KeyId: providerInputs.kmsKeyId
|
||||
});
|
||||
const isConnected = await awsClient.send(command).then(() => true);
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const encrypt = async (data: Buffer) => {
|
||||
const command = new EncryptCommand({
|
||||
KeyId: providerInputs.kmsKeyId,
|
||||
Plaintext: data
|
||||
});
|
||||
const encryptionCommand = await awsClient.send(command);
|
||||
if (!encryptionCommand.CiphertextBlob) throw new Error("encryption failed");
|
||||
|
||||
return { encryptedBlob: Buffer.from(encryptionCommand.CiphertextBlob) };
|
||||
};
|
||||
|
||||
const decrypt = async (encryptedBlob: Buffer) => {
|
||||
const command = new DecryptCommand({
|
||||
KeyId: providerInputs.kmsKeyId,
|
||||
CiphertextBlob: encryptedBlob
|
||||
});
|
||||
const decryptionCommand = await awsClient.send(command);
|
||||
if (!decryptionCommand.Plaintext) throw new Error("decryption failed");
|
||||
|
||||
return { data: Buffer.from(decryptionCommand.Plaintext) };
|
||||
};
|
||||
|
||||
return {
|
||||
generateInputKmsKey,
|
||||
validateConnection,
|
||||
encrypt,
|
||||
decrypt
|
||||
};
|
||||
};
|
61
backend/src/ee/services/external-kms/providers/model.ts
Normal file
61
backend/src/ee/services/external-kms/providers/model.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export enum KmsProviders {
|
||||
Aws = "aws"
|
||||
}
|
||||
|
||||
export enum KmsAwsCredentialType {
|
||||
AssumeRole = "assume-role",
|
||||
AccessKey = "access-key"
|
||||
}
|
||||
|
||||
export const ExternalKmsAwsSchema = z.object({
|
||||
credential: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal(KmsAwsCredentialType.AccessKey),
|
||||
data: z.object({
|
||||
accessKey: z.string().trim().min(1).describe("AWS user account access key"),
|
||||
secretKey: z.string().trim().min(1).describe("AWS user account secret key")
|
||||
})
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(KmsAwsCredentialType.AssumeRole),
|
||||
data: z.object({
|
||||
assumeRoleArn: z.string().trim().min(1).describe("AWS user role to be assumed by infisical"),
|
||||
externalId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe("AWS assume role external id for furthur security in authentication")
|
||||
})
|
||||
})
|
||||
])
|
||||
.describe("AWS credential information to connect"),
|
||||
awsRegion: z.string().min(1).trim().describe("AWS region to connect"),
|
||||
kmsKeyId: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.describe("A pre existing AWS KMS key id to be used for encryption. If not provided a kms key will be generated.")
|
||||
});
|
||||
export type TExternalKmsAwsSchema = z.infer<typeof ExternalKmsAwsSchema>;
|
||||
|
||||
// The root schema of the JSON
|
||||
export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema })
|
||||
]);
|
||||
export type TExternalKmsInputSchema = z.infer<typeof ExternalKmsInputSchema>;
|
||||
|
||||
export const ExternalKmsInputUpdateSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema.partial() })
|
||||
]);
|
||||
export type TExternalKmsInputUpdateSchema = z.infer<typeof ExternalKmsInputUpdateSchema>;
|
||||
|
||||
// generic function shared by all provider
|
||||
export type TExternalKmsProviderFns = {
|
||||
validateConnection: () => Promise<boolean>;
|
||||
encrypt: (data: Buffer) => Promise<{ encryptedBlob: Buffer }>;
|
||||
decrypt: (encryptedBlob: Buffer) => Promise<{ data: Buffer }>;
|
||||
};
|
@@ -162,11 +162,26 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findUserGroupMembershipsInOrg = async (userId: string, orgId: string) => {
|
||||
try {
|
||||
const docs = await db
|
||||
.replicaNode()(TableName.UserGroupMembership)
|
||||
.join(TableName.Groups, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`)
|
||||
.where(`${TableName.UserGroupMembership}.userId`, userId)
|
||||
.where(`${TableName.Groups}.orgId`, orgId);
|
||||
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "findTest" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...userGroupMembershipOrm,
|
||||
filterProjectsByUserMembership,
|
||||
findUserGroupMembershipsInProject,
|
||||
findGroupMembersNotInProject,
|
||||
deletePendingUserGroupMembershipsByUserIds
|
||||
deletePendingUserGroupMembershipsByUserIds,
|
||||
findUserGroupMembershipsInOrg
|
||||
};
|
||||
};
|
||||
|
@@ -449,7 +449,8 @@ export const ldapConfigServiceFactory = ({
|
||||
userId: userAlias.userId,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
status: OrgMembershipStatus.Accepted,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -534,7 +535,8 @@ export const ldapConfigServiceFactory = ({
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@@ -38,7 +38,9 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
has_used_trial: true,
|
||||
secretApproval: false,
|
||||
secretRotation: true,
|
||||
caCrl: false
|
||||
caCrl: false,
|
||||
instanceUserManagement: false,
|
||||
externalKms: false
|
||||
});
|
||||
|
||||
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
@@ -218,6 +218,8 @@ export const licenseServiceFactory = ({
|
||||
} else if (instanceType === InstanceType.EnterpriseOnPrem) {
|
||||
const usedSeats = await licenseDAL.countOfOrgMembers(null, tx);
|
||||
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx);
|
||||
onPremFeatures.membersUsed = usedSeats;
|
||||
onPremFeatures.identitiesUsed = usedIdentitySeats;
|
||||
await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, {
|
||||
usedSeats,
|
||||
usedIdentitySeats
|
||||
|
@@ -30,9 +30,9 @@ export type TFeatureSet = {
|
||||
workspacesUsed: 0;
|
||||
dynamicSecret: false;
|
||||
memberLimit: null;
|
||||
membersUsed: 0;
|
||||
membersUsed: number;
|
||||
identityLimit: null;
|
||||
identitiesUsed: 0;
|
||||
identitiesUsed: number;
|
||||
environmentLimit: null;
|
||||
environmentsUsed: 0;
|
||||
secretVersioning: true;
|
||||
@@ -56,6 +56,8 @@ export type TFeatureSet = {
|
||||
secretApproval: false;
|
||||
secretRotation: true;
|
||||
caCrl: false;
|
||||
instanceUserManagement: false;
|
||||
externalKms: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@@ -193,7 +193,8 @@ export const oidcConfigServiceFactory = ({
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -266,7 +267,8 @@ export const oidcConfigServiceFactory = ({
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@@ -21,7 +21,8 @@ export enum OrgPermissionSubjects {
|
||||
Groups = "groups",
|
||||
Billing = "billing",
|
||||
SecretScanning = "secret-scanning",
|
||||
Identity = "identity"
|
||||
Identity = "identity",
|
||||
Kms = "kms"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
@@ -37,7 +38,8 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
||||
|
||||
const buildAdminPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
@@ -100,6 +102,11 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||
|
||||
return build({ conditionsMatcher });
|
||||
};
|
||||
|
||||
|
@@ -109,6 +109,9 @@ export const permissionServiceFactory = ({
|
||||
authMethod: ActorAuthMethod,
|
||||
userOrgId?: string
|
||||
) => {
|
||||
// when token is scoped, ensure the passed org id is same as user org id
|
||||
if (userOrgId && userOrgId !== orgId)
|
||||
throw new BadRequestError({ message: "Invalid user token. Scoped to different organization." });
|
||||
const membership = await permissionDAL.getOrgPermission(userId, orgId);
|
||||
if (!membership) throw new UnauthorizedError({ name: "User not in org" });
|
||||
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
|
||||
|
@@ -28,7 +28,8 @@ export enum ProjectPermissionSub {
|
||||
SecretRotation = "secret-rotation",
|
||||
Identity = "identity",
|
||||
CertificateAuthorities = "certificate-authorities",
|
||||
Certificates = "certificates"
|
||||
Certificates = "certificates",
|
||||
Kms = "kms"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
@@ -60,7 +61,8 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||
|
||||
const buildAdminPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
@@ -157,6 +159,8 @@ const buildAdminPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
@@ -370,7 +370,8 @@ export const samlConfigServiceFactory = ({
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -457,7 +458,8 @@ export const samlConfigServiceFactory = ({
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@@ -32,12 +32,19 @@ export const parseScimFilter = (filterToParse: string | undefined) => {
|
||||
return { [attributeName]: parsedValue.replace(/"/g, "") };
|
||||
};
|
||||
|
||||
export function extractScimValueFromPath(path: string): string | null {
|
||||
const regex = /members\[value eq "([^"]+)"\]/;
|
||||
const match = path.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
export const buildScimUser = ({
|
||||
orgMembershipId,
|
||||
username,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
groups = [],
|
||||
active
|
||||
}: {
|
||||
orgMembershipId: string;
|
||||
@@ -45,6 +52,10 @@ export const buildScimUser = ({
|
||||
email?: string | null;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
groups?: {
|
||||
value: string;
|
||||
display: string;
|
||||
}[];
|
||||
active: boolean;
|
||||
}): TScimUser => {
|
||||
const scimUser = {
|
||||
@@ -67,7 +78,7 @@ export const buildScimUser = ({
|
||||
]
|
||||
: [],
|
||||
active,
|
||||
groups: [],
|
||||
groups,
|
||||
meta: {
|
||||
resourceType: "User",
|
||||
location: null
|
||||
|
@@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas";
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
@@ -30,7 +30,14 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns";
|
||||
import {
|
||||
buildScimGroup,
|
||||
buildScimGroupList,
|
||||
buildScimUser,
|
||||
buildScimUserList,
|
||||
extractScimValueFromPath,
|
||||
parseScimFilter
|
||||
} from "./scim-fns";
|
||||
import {
|
||||
TCreateScimGroupDTO,
|
||||
TCreateScimTokenDTO,
|
||||
@@ -61,17 +68,22 @@ type TScimServiceFactoryDep = {
|
||||
TOrgDALFactory,
|
||||
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" | "updateMembershipById"
|
||||
>;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
|
||||
groupDAL: Pick<
|
||||
TGroupDALFactory,
|
||||
"create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups" | "transaction"
|
||||
"create" | "findOne" | "findAllGroupMembers" | "delete" | "findGroups" | "transaction" | "updateById" | "update"
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
"find" | "transaction" | "insertMany" | "filterProjectsByUserMembership" | "delete"
|
||||
| "find"
|
||||
| "transaction"
|
||||
| "insertMany"
|
||||
| "filterProjectsByUserMembership"
|
||||
| "delete"
|
||||
| "findUserGroupMembershipsInOrg"
|
||||
>;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
@@ -197,14 +209,14 @@ export const scimServiceFactory = ({
|
||||
findOpts
|
||||
);
|
||||
|
||||
const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email }) =>
|
||||
const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email, isActive }) =>
|
||||
buildScimUser({
|
||||
orgMembershipId: id ?? "",
|
||||
username: externalId ?? username,
|
||||
firstName: firstName ?? "",
|
||||
lastName: lastName ?? "",
|
||||
email,
|
||||
active: true
|
||||
active: isActive
|
||||
})
|
||||
);
|
||||
|
||||
@@ -240,13 +252,19 @@ export const scimServiceFactory = ({
|
||||
status: 403
|
||||
});
|
||||
|
||||
const groupMembershipsInOrg = await userGroupMembershipDAL.findUserGroupMembershipsInOrg(membership.userId, orgId);
|
||||
|
||||
return buildScimUser({
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
email: membership.email ?? "",
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
active: true
|
||||
active: membership.isActive,
|
||||
groups: groupMembershipsInOrg.map((group) => ({
|
||||
value: group.groupId,
|
||||
display: group.name
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
@@ -296,7 +314,8 @@ export const scimServiceFactory = ({
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -364,7 +383,8 @@ export const scimServiceFactory = ({
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -401,7 +421,7 @@ export const scimServiceFactory = ({
|
||||
firstName: createdUser.firstName as string,
|
||||
lastName: createdUser.lastName as string,
|
||||
email: createdUser.email ?? "",
|
||||
active: true
|
||||
active: createdOrgMembership.isActive
|
||||
});
|
||||
};
|
||||
|
||||
@@ -445,14 +465,8 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
|
||||
if (!active) {
|
||||
await deleteOrgMembershipFn({
|
||||
orgMembershipId: membership.id,
|
||||
orgId: membership.orgId,
|
||||
orgDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userAliasDAL,
|
||||
licenseService
|
||||
await orgMembershipDAL.updateById(membership.id, {
|
||||
isActive: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -491,17 +505,11 @@ export const scimServiceFactory = ({
|
||||
status: 403
|
||||
});
|
||||
|
||||
if (!active) {
|
||||
await deleteOrgMembershipFn({
|
||||
orgMembershipId: membership.id,
|
||||
orgId: membership.orgId,
|
||||
orgDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userAliasDAL,
|
||||
licenseService
|
||||
});
|
||||
}
|
||||
await orgMembershipDAL.updateById(membership.id, {
|
||||
isActive: active
|
||||
});
|
||||
|
||||
const groupMembershipsInOrg = await userGroupMembershipDAL.findUserGroupMembershipsInOrg(membership.userId, orgId);
|
||||
|
||||
return buildScimUser({
|
||||
orgMembershipId: membership.id,
|
||||
@@ -509,7 +517,11 @@ export const scimServiceFactory = ({
|
||||
email: membership.email,
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
active
|
||||
active,
|
||||
groups: groupMembershipsInOrg.map((group) => ({
|
||||
value: group.groupId,
|
||||
display: group.name
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
@@ -817,7 +829,6 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: add support for add/remove op
|
||||
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
@@ -840,27 +851,60 @@ export const scimServiceFactory = ({
|
||||
status: 403
|
||||
});
|
||||
|
||||
let group: TGroups | undefined;
|
||||
let group = await groupDAL.findOne({
|
||||
id: groupId,
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
for await (const operation of operations) {
|
||||
switch (operation.op) {
|
||||
case "replace": {
|
||||
await groupDAL.update(
|
||||
{
|
||||
id: groupId,
|
||||
orgId
|
||||
},
|
||||
{
|
||||
name: operation.value.displayName
|
||||
}
|
||||
);
|
||||
group = await groupDAL.updateById(group.id, {
|
||||
name: operation.value.displayName
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "add": {
|
||||
// TODO
|
||||
const orgMemberships = await orgMembershipDAL.find({
|
||||
$in: {
|
||||
id: operation.value.map((member) => member.value)
|
||||
}
|
||||
});
|
||||
|
||||
await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: orgMemberships.map((membership) => membership.userId as string),
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
// TODO
|
||||
const orgMembershipId = extractScimValueFromPath(operation.path);
|
||||
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
|
||||
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
|
||||
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
|
||||
await removeUsersFromGroupByUserIds({
|
||||
group,
|
||||
userIds: [orgMembership.userId as string],
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -872,13 +916,6 @@ export const scimServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (!group) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
return buildScimGroup({
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
|
@@ -125,10 +125,11 @@ type TRemoveOp = {
|
||||
|
||||
type TAddOp = {
|
||||
op: "add";
|
||||
path: string;
|
||||
value: {
|
||||
value: string;
|
||||
display?: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
export type TDeleteScimGroupDTO = {
|
||||
@@ -157,7 +158,10 @@ export type TScimUser = {
|
||||
type: string;
|
||||
}[];
|
||||
active: boolean;
|
||||
groups: string[];
|
||||
groups: {
|
||||
value: string;
|
||||
display: string;
|
||||
}[];
|
||||
meta: {
|
||||
resourceType: string;
|
||||
location: null;
|
||||
|
@@ -6,7 +6,15 @@ export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
||||
|
||||
// all the key prefixes used must be set here to avoid conflict
|
||||
export enum KeyStorePrefixes {
|
||||
SecretReplication = "secret-replication-import-lock"
|
||||
SecretReplication = "secret-replication-import-lock",
|
||||
KmsProjectDataKeyCreation = "kms-project-data-key-creation-lock",
|
||||
KmsProjectKeyCreation = "kms-project-key-creation-lock",
|
||||
WaitUntilReadyKmsProjectDataKeyCreation = "wait-until-ready-kms-project-data-key-creation-",
|
||||
WaitUntilReadyKmsProjectKeyCreation = "wait-until-ready-kms-project-key-creation-",
|
||||
KmsOrgKeyCreation = "kms-org-key-creation-lock",
|
||||
KmsOrgDataKeyCreation = "kms-org-data-key-creation-lock",
|
||||
WaitUntilReadyKmsOrgKeyCreation = "wait-until-ready-kms-org-key-creation-",
|
||||
WaitUntilReadyKmsOrgDataKeyCreation = "wait-until-ready-kms-org-data-key-creation-"
|
||||
}
|
||||
|
||||
type TWaitTillReady = {
|
||||
|
@@ -515,6 +515,9 @@ export const FOLDERS = {
|
||||
path: "The path to list folders from.",
|
||||
directory: "The directory to list folders from. (Deprecated in favor of path)"
|
||||
},
|
||||
GET_BY_ID: {
|
||||
folderId: "The id of the folder to get details."
|
||||
},
|
||||
CREATE: {
|
||||
workspaceId: "The ID of the project to create the folder in.",
|
||||
environment: "The slug of the environment to create the folder in.",
|
||||
|
@@ -116,6 +116,8 @@ export const decryptAsymmetric = ({ ciphertext, nonce, publicKey, privateKey }:
|
||||
|
||||
export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64");
|
||||
|
||||
export const generateHash = (value: string) => crypto.createHash("sha256").update(value).digest("hex");
|
||||
|
||||
export const generateAsymmetricKeyPair = () => {
|
||||
const pair = nacl.box.keyPair();
|
||||
|
||||
|
@@ -22,6 +22,8 @@ import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/pro
|
||||
import { dynamicSecretLeaseDALFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||
import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue";
|
||||
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal";
|
||||
import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { groupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
@@ -116,7 +118,8 @@ import { integrationDALFactory } from "@app/services/integration/integration-dal
|
||||
import { integrationServiceFactory } from "@app/services/integration/integration-service";
|
||||
import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal";
|
||||
import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||
import { kmsDALFactory } from "@app/services/kms/kms-dal";
|
||||
import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
||||
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
||||
@@ -288,7 +291,9 @@ export const registerRoutes = async (
|
||||
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
||||
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
||||
|
||||
const kmsDAL = kmsDALFactory(db);
|
||||
const kmsDAL = kmskeyDALFactory(db);
|
||||
const internalKmsDAL = internalKmsDALFactory(db);
|
||||
const externalKmsDAL = externalKmsDALFactory(db);
|
||||
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
@@ -302,7 +307,17 @@ export const registerRoutes = async (
|
||||
const kmsService = kmsServiceFactory({
|
||||
kmsRootConfigDAL,
|
||||
keyStore,
|
||||
kmsDAL
|
||||
kmsDAL,
|
||||
internalKmsDAL,
|
||||
orgDAL,
|
||||
projectDAL
|
||||
});
|
||||
const externalKmsService = externalKmsServiceFactory({
|
||||
kmsDAL,
|
||||
kmsService,
|
||||
permissionService,
|
||||
externalKmsDAL,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const trustedIpService = trustedIpServiceFactory({
|
||||
@@ -331,7 +346,7 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
secretApprovalPolicyDAL
|
||||
});
|
||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
|
||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
|
||||
|
||||
const samlService = samlConfigServiceFactory({
|
||||
permissionService,
|
||||
@@ -469,7 +484,8 @@ export const registerRoutes = async (
|
||||
authService: loginService,
|
||||
serverCfgDAL: superAdminDAL,
|
||||
orgService,
|
||||
keyStore
|
||||
keyStore,
|
||||
licenseService
|
||||
});
|
||||
const rateLimitService = rateLimitServiceFactory({
|
||||
rateLimitDAL,
|
||||
@@ -609,7 +625,8 @@ export const registerRoutes = async (
|
||||
certificateDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
identityProjectMembershipRoleDAL,
|
||||
keyStore
|
||||
keyStore,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const projectEnvService = projectEnvServiceFactory({
|
||||
@@ -644,7 +661,8 @@ export const registerRoutes = async (
|
||||
const webhookService = webhookServiceFactory({
|
||||
permissionService,
|
||||
webhookDAL,
|
||||
projectEnvDAL
|
||||
projectEnvDAL,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService });
|
||||
@@ -1029,7 +1047,8 @@ export const registerRoutes = async (
|
||||
projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService,
|
||||
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
|
||||
secretSharing: secretSharingService,
|
||||
userEngagement: userEngagementService
|
||||
userEngagement: userEngagementService,
|
||||
externalKms: externalKmsService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
@@ -100,6 +100,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenTTL must have a non zero number"
|
||||
})
|
||||
@@ -108,6 +109,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
@@ -182,11 +184,12 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(AWS_AUTH.UPDATE.accessTokenTrustedIps),
|
||||
accessTokenTTL: z.number().int().min(0).optional().describe(AWS_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AWS_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AWS_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
|
@@ -90,6 +90,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenTTL must have a non zero number"
|
||||
})
|
||||
@@ -98,6 +99,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
@@ -173,11 +175,12 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(AZURE_AUTH.UPDATE.accessTokenTrustedIps),
|
||||
accessTokenTTL: z.number().int().min(0).optional().describe(AZURE_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AZURE_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AZURE_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
|
@@ -91,6 +91,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenTTL must have a non zero number"
|
||||
})
|
||||
@@ -99,6 +100,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
@@ -175,11 +177,12 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(GCP_AUTH.UPDATE.accessTokenTrustedIps),
|
||||
accessTokenTTL: z.number().int().min(0).optional().describe(GCP_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(GCP_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(GCP_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
|
@@ -106,6 +106,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenTTL must have a non zero number"
|
||||
})
|
||||
@@ -114,6 +115,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
@@ -196,7 +198,13 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(KUBERNETES_AUTH.UPDATE.accessTokenTrustedIps),
|
||||
accessTokenTTL: z.number().int().min(0).optional().describe(KUBERNETES_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.optional()
|
||||
.describe(KUBERNETES_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenNumUsesLimit: z
|
||||
.number()
|
||||
.int()
|
||||
@@ -206,6 +214,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
|
@@ -106,6 +106,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenTTL must have a non zero number"
|
||||
})
|
||||
@@ -114,6 +115,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
@@ -201,6 +203,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenTTL must have a non zero number"
|
||||
})
|
||||
@@ -209,6 +212,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
|
@@ -39,6 +39,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenTTL must have a non zero number"
|
||||
})
|
||||
@@ -47,6 +48,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
@@ -117,11 +119,12 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(TOKEN_AUTH.UPDATE.accessTokenTrustedIps),
|
||||
accessTokenTTL: z.number().int().min(0).optional().describe(TOKEN_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(TOKEN_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(TOKEN_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
|
@@ -107,6 +107,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenTTL must have a non zero number"
|
||||
})
|
||||
@@ -115,6 +116,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
@@ -196,7 +198,13 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenTrustedIps),
|
||||
accessTokenTTL: z.number().int().min(0).optional().describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.optional()
|
||||
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenNumUsesLimit: z
|
||||
.number()
|
||||
.int()
|
||||
@@ -206,6 +214,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
@@ -362,7 +371,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
description: z.string().trim().default("").describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.description),
|
||||
numUsesLimit: z.number().min(0).default(0).describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.numUsesLimit),
|
||||
ttl: z.number().min(0).default(0).describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.ttl)
|
||||
ttl: z.number().min(0).max(315360000).default(0).describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.ttl)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -292,4 +292,39 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
return { folders };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get folder by id",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string().trim().describe(FOLDERS.GET_BY_ID.folderId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
folder: SecretFoldersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const folder = await server.services.folder.getFolderById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
return { folder };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -161,7 +161,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional()
|
||||
.describe(PROJECTS.CREATE.slug)
|
||||
.describe(PROJECTS.CREATE.slug),
|
||||
kmsKeyId: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -177,7 +178,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
workspaceName: req.body.projectName,
|
||||
slug: req.body.slug
|
||||
slug: req.body.slug,
|
||||
kmsKeyId: req.body.kmsKeyId
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
|
@@ -4,7 +4,8 @@ import bcrypt from "bcrypt";
|
||||
|
||||
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
|
||||
import { AuthModeJwtTokenPayload } from "../auth/auth-type";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
@@ -14,6 +15,7 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo
|
||||
type TAuthTokenServiceFactoryDep = {
|
||||
tokenDAL: TTokenDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "findById" | "transaction">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOne">;
|
||||
};
|
||||
|
||||
export type TAuthTokenServiceFactory = ReturnType<typeof tokenServiceFactory>;
|
||||
@@ -67,7 +69,7 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const tokenServiceFactory = ({ tokenDAL, userDAL }: TAuthTokenServiceFactoryDep) => {
|
||||
export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => {
|
||||
const createTokenForUser = async ({ type, userId, orgId }: TCreateTokenForUserDTO) => {
|
||||
const { token, ...tkCfg } = getTokenConfig(type);
|
||||
const appCfg = getConfig();
|
||||
@@ -154,6 +156,16 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL }: TAuthTokenServiceFact
|
||||
const user = await userDAL.findById(session.userId);
|
||||
if (!user || !user.isAccepted) throw new UnauthorizedError({ name: "Token user not found" });
|
||||
|
||||
if (token.organizationId) {
|
||||
const orgMembership = await orgMembershipDAL.findOne({
|
||||
userId: user.id,
|
||||
orgId: token.organizationId
|
||||
});
|
||||
|
||||
if (!orgMembership) throw new ForbiddenRequestError({ message: "User not member of organization" });
|
||||
if (!orgMembership.isActive) throw new ForbiddenRequestError({ message: "User not active in organization" });
|
||||
}
|
||||
|
||||
return { user, tokenVersionId: token.tokenVersionId, orgId: token.organizationId };
|
||||
};
|
||||
|
||||
|
@@ -75,8 +75,10 @@ export const getCaCredentials = async ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const decryptedPrivateKey = await kmsService.decrypt({
|
||||
kmsId: keyId,
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
const decryptedPrivateKey = await kmsDecryptor({
|
||||
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||
});
|
||||
|
||||
@@ -123,15 +125,17 @@ export const getCaCertChain = async ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const decryptedCaCert = await kmsService.decrypt({
|
||||
kmsId: keyId,
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
|
||||
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||
|
||||
const decryptedChain = await kmsService.decrypt({
|
||||
kmsId: keyId,
|
||||
const decryptedChain = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificateChain
|
||||
});
|
||||
|
||||
@@ -168,8 +172,11 @@ export const rebuildCaCrl = async ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const privateKey = await kmsService.decrypt({
|
||||
kmsId: keyId,
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
|
||||
const privateKey = await kmsDecryptor({
|
||||
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||
});
|
||||
|
||||
@@ -200,8 +207,10 @@ export const rebuildCaCrl = async ({
|
||||
signingKey: sk
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
|
||||
kmsId: keyId,
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||
});
|
||||
|
||||
|
@@ -25,7 +25,7 @@ type TCertificateAuthorityQueueFactoryDep = {
|
||||
certificateAuthoritySecretDAL: TCertificateAuthoritySecretDALFactory;
|
||||
certificateDAL: TCertificateDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encrypt" | "decrypt">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
queueService: TQueueServiceFactory;
|
||||
};
|
||||
export type TCertificateAuthorityQueueFactory = ReturnType<typeof certificateAuthorityQueueFactory>;
|
||||
@@ -88,8 +88,10 @@ export const certificateAuthorityQueueFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const privateKey = await kmsService.decrypt({
|
||||
kmsId: keyId,
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
const privateKey = await kmsDecryptor({
|
||||
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||
});
|
||||
|
||||
@@ -120,8 +122,10 @@ export const certificateAuthorityQueueFactory = ({
|
||||
signingKey: sk
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
|
||||
kmsId: keyId,
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||
});
|
||||
|
||||
|
@@ -53,7 +53,7 @@ type TCertificateAuthorityServiceFactoryDep = {
|
||||
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encrypt" | "decrypt">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
@@ -154,11 +154,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: project.id,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
if (type === CaType.ROOT) {
|
||||
// note: create self-signed cert only applicable for root CA
|
||||
@@ -178,13 +181,11 @@ export const certificateAuthorityServiceFactory = ({
|
||||
]
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({
|
||||
kmsId: keyId,
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(cert.rawData))
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({
|
||||
kmsId: keyId,
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.alloc(0)
|
||||
});
|
||||
|
||||
@@ -208,8 +209,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
signingKey: keys.privateKey
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
|
||||
kmsId: keyId,
|
||||
const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||
});
|
||||
|
||||
@@ -224,8 +224,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
|
||||
const skObj = KeyObject.from(keys.privateKey);
|
||||
|
||||
const { cipherTextBlob: encryptedPrivateKey } = await kmsService.encrypt({
|
||||
kmsId: keyId,
|
||||
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||
plainText: skObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
@@ -449,15 +448,17 @@ export const certificateAuthorityServiceFactory = ({
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
const decryptedCaCert = await kmsService.decrypt({
|
||||
kmsId: keyId,
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
|
||||
@@ -605,19 +606,20 @@ export const certificateAuthorityServiceFactory = ({
|
||||
dn: parentCertSubject
|
||||
});
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({
|
||||
kmsId: keyId,
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(certObj.rawData))
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({
|
||||
kmsId: keyId,
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(certificateChain)
|
||||
});
|
||||
|
||||
@@ -682,14 +684,16 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const decryptedCaCert = await kmsService.decrypt({
|
||||
kmsId: keyId,
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
|
||||
@@ -796,8 +800,10 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({
|
||||
kmsId: keyId,
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||
});
|
||||
|
||||
|
@@ -95,7 +95,7 @@ export type TGetCaCredentialsDTO = {
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decrypt" | "generateKmsKey">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TGetCaCertChainDTO = {
|
||||
@@ -103,7 +103,7 @@ export type TGetCaCertChainDTO = {
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decrypt" | "generateKmsKey">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TRebuildCaCrlDTO = {
|
||||
@@ -113,7 +113,7 @@ export type TRebuildCaCrlDTO = {
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decrypt" | "encrypt">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
|
||||
};
|
||||
|
||||
export type TRotateCaCrlTriggerDTO = {
|
||||
|
@@ -25,7 +25,7 @@ type TCertificateServiceFactoryDep = {
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encrypt" | "decrypt">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
@@ -164,14 +164,16 @@ export const certificateServiceFactory = ({
|
||||
|
||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const decryptedCert = await kmsService.decrypt({
|
||||
kmsId: keyId,
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKeyId
|
||||
});
|
||||
const decryptedCert = await kmsDecryptor({
|
||||
cipherTextBlob: certBody.encryptedCertificate
|
||||
});
|
||||
|
||||
|
@@ -78,7 +78,10 @@ export const identityAwsAuthServiceFactory = ({
|
||||
.map((accountId) => accountId.trim())
|
||||
.some((accountId) => accountId === Account);
|
||||
|
||||
if (!isAccountAllowed) throw new UnauthorizedError();
|
||||
if (!isAccountAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: AWS account ID not allowed."
|
||||
});
|
||||
}
|
||||
|
||||
if (identityAwsAuth.allowedPrincipalArns) {
|
||||
@@ -94,7 +97,10 @@ export const identityAwsAuthServiceFactory = ({
|
||||
return regex.test(extractPrincipalArn(Arn));
|
||||
});
|
||||
|
||||
if (!isArnAllowed) throw new UnauthorizedError();
|
||||
if (!isArnAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: AWS principal ARN not allowed."
|
||||
});
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => {
|
||||
|
@@ -17,6 +17,7 @@ export const validateAzureIdentity = async ({
|
||||
const jwksUri = `https://login.microsoftonline.com/${tenantId}/discovery/keys`;
|
||||
|
||||
const decodedJwt = jwt.decode(azureJwt, { complete: true }) as TDecodedAzureAuthJwt;
|
||||
|
||||
const { kid } = decodedJwt.header;
|
||||
|
||||
const { data }: { data: TAzureJwksUriResponse } = await axios.get(jwksUri);
|
||||
@@ -27,6 +28,13 @@ export const validateAzureIdentity = async ({
|
||||
|
||||
const publicKey = `-----BEGIN CERTIFICATE-----\n${signingKey.x5c[0]}\n-----END CERTIFICATE-----`;
|
||||
|
||||
// Case: This can happen when the user uses a custom resource (such as https://management.azure.com&client_id=value).
|
||||
// In this case, the audience in the decoded JWT will not have a trailing slash, but the resource will.
|
||||
if (!decodedJwt.payload.aud.endsWith("/") && resource.endsWith("/")) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
resource = resource.slice(0, -1);
|
||||
}
|
||||
|
||||
return jwt.verify(azureJwt, publicKey, {
|
||||
audience: resource,
|
||||
issuer: `https://sts.windows.net/${tenantId}/`
|
||||
|
@@ -81,7 +81,10 @@ export const identityGcpAuthServiceFactory = ({
|
||||
.map((serviceAccount) => serviceAccount.trim())
|
||||
.some((serviceAccount) => serviceAccount === gcpIdentityDetails.email);
|
||||
|
||||
if (!isServiceAccountAllowed) throw new UnauthorizedError();
|
||||
if (!isServiceAccountAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: GCP service account not allowed."
|
||||
});
|
||||
}
|
||||
|
||||
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedProjects && gcpIdentityDetails.computeEngineDetails) {
|
||||
@@ -92,7 +95,10 @@ export const identityGcpAuthServiceFactory = ({
|
||||
.map((project) => project.trim())
|
||||
.some((project) => project === gcpIdentityDetails.computeEngineDetails?.project_id);
|
||||
|
||||
if (!isProjectAllowed) throw new UnauthorizedError();
|
||||
if (!isProjectAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: GCP project not allowed."
|
||||
});
|
||||
}
|
||||
|
||||
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedZones && gcpIdentityDetails.computeEngineDetails) {
|
||||
@@ -101,7 +107,10 @@ export const identityGcpAuthServiceFactory = ({
|
||||
.map((zone) => zone.trim())
|
||||
.some((zone) => zone === gcpIdentityDetails.computeEngineDetails?.zone);
|
||||
|
||||
if (!isZoneAllowed) throw new UnauthorizedError();
|
||||
if (!isZoneAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: GCP zone not allowed."
|
||||
});
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => {
|
||||
|
@@ -139,7 +139,10 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
.map((namespace) => namespace.trim())
|
||||
.some((namespace) => namespace === targetNamespace);
|
||||
|
||||
if (!isNamespaceAllowed) throw new UnauthorizedError();
|
||||
if (!isNamespaceAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: K8s namespace not allowed."
|
||||
});
|
||||
}
|
||||
|
||||
if (identityKubernetesAuth.allowedNames) {
|
||||
@@ -150,7 +153,10 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
.map((name) => name.trim())
|
||||
.some((name) => name === targetName);
|
||||
|
||||
if (!isNameAllowed) throw new UnauthorizedError();
|
||||
if (!isNameAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: K8s name not allowed."
|
||||
});
|
||||
}
|
||||
|
||||
if (identityKubernetesAuth.allowedAudience) {
|
||||
@@ -159,7 +165,10 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
(audience) => audience === identityKubernetesAuth.allowedAudience
|
||||
);
|
||||
|
||||
if (!isAudienceAllowed) throw new UnauthorizedError();
|
||||
if (!isAudienceAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: K8s audience not allowed."
|
||||
});
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => {
|
||||
|
@@ -124,13 +124,17 @@ export const identityOidcAuthServiceFactory = ({
|
||||
|
||||
if (identityOidcAuth.boundSubject) {
|
||||
if (tokenData.sub !== identityOidcAuth.boundSubject) {
|
||||
throw new UnauthorizedError();
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: OIDC subject not allowed."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (identityOidcAuth.boundAudiences) {
|
||||
if (!identityOidcAuth.boundAudiences.split(", ").includes(tokenData.aud)) {
|
||||
throw new UnauthorizedError();
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: OIDC audience not allowed."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +143,9 @@ export const identityOidcAuthServiceFactory = ({
|
||||
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
|
||||
// handle both single and multi-valued claims
|
||||
if (!claimValue.split(", ").some((claimEntry) => tokenData[claimKey] === claimEntry)) {
|
||||
throw new UnauthorizedError();
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: OIDC claim not allowed."
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -574,14 +574,14 @@ export const integrationAuthServiceFactory = ({
|
||||
const botKey = await projectBotService.getBotKey(integrationAuth.projectId);
|
||||
const { accessId, accessToken } = await getIntegrationAccessToken(integrationAuth, botKey);
|
||||
|
||||
AWS.config.update({
|
||||
const kms = new AWS.KMS({
|
||||
region,
|
||||
credentials: {
|
||||
accessKeyId: String(accessId),
|
||||
secretAccessKey: accessToken
|
||||
}
|
||||
});
|
||||
const kms = new AWS.KMS();
|
||||
|
||||
const aliases = await kms.listAliases({}).promise();
|
||||
|
||||
const keyAliases = aliases.Aliases!.filter((alias) => {
|
||||
|
10
backend/src/services/kms/internal-kms-dal.ts
Normal file
10
backend/src/services/kms/internal-kms-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TInternalKmsDALFactory = ReturnType<typeof internalKmsDALFactory>;
|
||||
|
||||
export const internalKmsDALFactory = (db: TDbClient) => {
|
||||
const internalKmsOrm = ormify(db, TableName.InternalKms);
|
||||
return internalKmsOrm;
|
||||
};
|
@@ -1,10 +0,0 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TKmsDALFactory = ReturnType<typeof kmsDALFactory>;
|
||||
|
||||
export const kmsDALFactory = (db: TDbClient) => {
|
||||
const kmsOrm = ormify(db, TableName.KmsKey);
|
||||
return kmsOrm;
|
||||
};
|
73
backend/src/services/kms/kms-key-dal.ts
Normal file
73
backend/src/services/kms/kms-key-dal.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { KmsKeysSchema, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TKmsKeyDALFactory = ReturnType<typeof kmskeyDALFactory>;
|
||||
|
||||
export const kmskeyDALFactory = (db: TDbClient) => {
|
||||
const kmsOrm = ormify(db, TableName.KmsKey);
|
||||
|
||||
const findByIdWithAssociatedKms = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const result = await (tx || db.replicaNode())(TableName.KmsKey)
|
||||
.where({ [`${TableName.KmsKey}.id` as "id"]: id })
|
||||
.join(TableName.Organization, `${TableName.KmsKey}.orgId`, `${TableName.Organization}.id`)
|
||||
.leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`)
|
||||
.leftJoin(TableName.ExternalKms, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`)
|
||||
.first()
|
||||
.select(selectAllTableCols(TableName.KmsKey))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.InternalKms).as("internalKmsId"),
|
||||
db.ref("encryptedKey").withSchema(TableName.InternalKms).as("internalKmsEncryptedKey"),
|
||||
db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms).as("internalKmsEncryptionAlgorithm"),
|
||||
db.ref("version").withSchema(TableName.InternalKms).as("internalKmsVersion"),
|
||||
db.ref("id").withSchema(TableName.InternalKms).as("internalKmsId")
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ExternalKms).as("externalKmsId"),
|
||||
db.ref("provider").withSchema(TableName.ExternalKms).as("externalKmsProvider"),
|
||||
db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"),
|
||||
db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"),
|
||||
db.ref("statusDetails").withSchema(TableName.ExternalKms).as("externalKmsStatusDetails")
|
||||
)
|
||||
.select(
|
||||
db.ref("kmsDefaultKeyId").withSchema(TableName.Organization).as("orgKmsDefaultKeyId"),
|
||||
db.ref("kmsEncryptedDataKey").withSchema(TableName.Organization).as("orgKmsEncryptedDataKey")
|
||||
);
|
||||
|
||||
const data = {
|
||||
...KmsKeysSchema.parse(result),
|
||||
isExternal: Boolean(result?.externalKmsId),
|
||||
orgKms: {
|
||||
id: result?.orgKmsDefaultKeyId,
|
||||
encryptedDataKey: result?.orgKmsEncryptedDataKey
|
||||
},
|
||||
externalKms: result?.externalKmsId
|
||||
? {
|
||||
id: result.externalKmsId,
|
||||
provider: result.externalKmsProvider,
|
||||
encryptedProviderInput: result.externalKmsEncryptedProviderInput,
|
||||
status: result.externalKmsStatus,
|
||||
statusDetails: result.externalKmsStatusDetails
|
||||
}
|
||||
: undefined,
|
||||
internalKms: result?.internalKmsId
|
||||
? {
|
||||
id: result.internalKmsId,
|
||||
encryptedKey: result.internalKmsEncryptedKey,
|
||||
encryptionAlgorithm: result.internalKmsEncryptionAlgorithm,
|
||||
version: result.internalKmsVersion
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by id" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...kmsOrm, findByIdWithAssociatedKms };
|
||||
};
|
@@ -1,22 +1,46 @@
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms";
|
||||
import {
|
||||
ExternalKmsAwsSchema,
|
||||
KmsProviders,
|
||||
TExternalKmsProviderFns
|
||||
} from "@app/ee/services/external-kms/providers/model";
|
||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { randomSecureBytes } from "@app/lib/crypto";
|
||||
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { generateHash } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TKmsDALFactory } from "./kms-dal";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TInternalKmsDALFactory } from "./internal-kms-dal";
|
||||
import { TKmsKeyDALFactory } from "./kms-key-dal";
|
||||
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
|
||||
import { TDecryptWithKmsDTO, TEncryptWithKmsDTO, TGenerateKMSDTO } from "./kms-types";
|
||||
import {
|
||||
TDecryptWithKeyDTO,
|
||||
TDecryptWithKmsDTO,
|
||||
TEncryptionWithKeyDTO,
|
||||
TEncryptWithKmsDTO,
|
||||
TGenerateKMSDTO
|
||||
} from "./kms-types";
|
||||
|
||||
type TKmsServiceFactoryDep = {
|
||||
kmsDAL: TKmsDALFactory;
|
||||
kmsDAL: TKmsKeyDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "updateById" | "transaction">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findById" | "updateById" | "transaction">;
|
||||
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
|
||||
internalKmsDAL: Pick<TInternalKmsDALFactory, "create">;
|
||||
};
|
||||
|
||||
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
|
||||
|
||||
export const INTERNAL_KMS_KEY_ID = "internal";
|
||||
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
|
||||
@@ -25,54 +49,633 @@ const KMS_ROOT_CREATION_WAIT_TIME = 10;
|
||||
// akhilmhdh: Don't edit this value. This is measured for blob concatination in kms
|
||||
const KMS_VERSION = "v01";
|
||||
const KMS_VERSION_BLOB_LENGTH = 3;
|
||||
export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsServiceFactoryDep) => {
|
||||
export const kmsServiceFactory = ({
|
||||
kmsDAL,
|
||||
kmsRootConfigDAL,
|
||||
keyStore,
|
||||
internalKmsDAL,
|
||||
orgDAL,
|
||||
projectDAL
|
||||
}: TKmsServiceFactoryDep) => {
|
||||
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
|
||||
|
||||
// this is used symmetric encryption
|
||||
const generateKmsKey = async ({ scopeId, scopeType, isReserved = true, tx }: TGenerateKMSDTO) => {
|
||||
const generateKmsKey = async ({ orgId, isReserved = true, tx, slug }: TGenerateKMSDTO) => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const kmsKeyMaterial = randomSecureBytes(32);
|
||||
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
||||
const sanitizedSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||
const dbQuery = async (db: Knex) => {
|
||||
const kmsDoc = await kmsDAL.create(
|
||||
{
|
||||
slug: sanitizedSlug,
|
||||
orgId,
|
||||
isReserved
|
||||
},
|
||||
db
|
||||
);
|
||||
|
||||
const { encryptedKey, ...doc } = await kmsDAL.create(
|
||||
{
|
||||
version: 1,
|
||||
encryptedKey: encryptedKeyMaterial,
|
||||
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
|
||||
isReserved,
|
||||
orgId: scopeType === "org" ? scopeId : undefined,
|
||||
projectId: scopeType === "project" ? scopeId : undefined
|
||||
},
|
||||
tx
|
||||
);
|
||||
await internalKmsDAL.create(
|
||||
{
|
||||
version: 1,
|
||||
encryptedKey: encryptedKeyMaterial,
|
||||
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
|
||||
kmsKeyId: kmsDoc.id
|
||||
},
|
||||
db
|
||||
);
|
||||
return kmsDoc;
|
||||
};
|
||||
if (tx) return dbQuery(tx);
|
||||
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
|
||||
return doc;
|
||||
};
|
||||
|
||||
const encrypt = async ({ kmsId, plainText }: TEncryptWithKmsDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" });
|
||||
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
|
||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
|
||||
const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY);
|
||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey);
|
||||
|
||||
// Buffer#1 encrypted text + Buffer#2 version number
|
||||
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
|
||||
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
|
||||
return { cipherTextBlob };
|
||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, key);
|
||||
// Buffer#1 encrypted text + Buffer#2 version number
|
||||
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
|
||||
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
|
||||
return { cipherTextBlob };
|
||||
};
|
||||
};
|
||||
|
||||
const decrypt = async ({ cipherTextBlob: versionedCipherTextBlob, kmsId }: TDecryptWithKmsDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" });
|
||||
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
|
||||
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, key);
|
||||
return decryptedBlob;
|
||||
};
|
||||
};
|
||||
|
||||
const getOrgKmsKeyId = async (orgId: string) => {
|
||||
let org = await orgDAL.findById(orgId);
|
||||
|
||||
if (!org) {
|
||||
throw new NotFoundError({ message: "Org not found" });
|
||||
}
|
||||
|
||||
if (!org.kmsDefaultKeyId) {
|
||||
const lock = await keyStore
|
||||
.acquireLock([KeyStorePrefixes.KmsOrgKeyCreation, orgId], 3000, { retryCount: 3 })
|
||||
.catch(() => null);
|
||||
|
||||
try {
|
||||
if (!lock) {
|
||||
await keyStore.waitTillReady({
|
||||
key: `${KeyStorePrefixes.WaitUntilReadyKmsOrgKeyCreation}${orgId}`,
|
||||
keyCheckCb: (val) => val === "true",
|
||||
waitingCb: () => logger.info("KMS. Waiting for org key to be created")
|
||||
});
|
||||
|
||||
org = await orgDAL.findById(orgId);
|
||||
} else {
|
||||
const keyId = await orgDAL.transaction(async (tx) => {
|
||||
org = await orgDAL.findById(orgId, tx);
|
||||
if (org.kmsDefaultKeyId) {
|
||||
return org.kmsDefaultKeyId;
|
||||
}
|
||||
|
||||
const key = await generateKmsKey({
|
||||
isReserved: true,
|
||||
orgId: org.id,
|
||||
tx
|
||||
});
|
||||
|
||||
await orgDAL.updateById(
|
||||
org.id,
|
||||
{
|
||||
kmsDefaultKeyId: key.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await keyStore.setItemWithExpiry(`${KeyStorePrefixes.WaitUntilReadyKmsOrgKeyCreation}${orgId}`, 10, "true");
|
||||
|
||||
return key.id;
|
||||
});
|
||||
|
||||
return keyId;
|
||||
}
|
||||
} finally {
|
||||
await lock?.release();
|
||||
}
|
||||
}
|
||||
|
||||
if (!org.kmsDefaultKeyId) {
|
||||
throw new Error("Invalid organization KMS");
|
||||
}
|
||||
|
||||
return org.kmsDefaultKeyId;
|
||||
};
|
||||
|
||||
const decryptWithKmsKey = async ({ kmsId }: Omit<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: "KMS ID not found" });
|
||||
}
|
||||
|
||||
if (kmsDoc.externalKms) {
|
||||
let externalKms: TExternalKmsProviderFns;
|
||||
|
||||
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
|
||||
throw new Error("Invalid organization KMS");
|
||||
}
|
||||
|
||||
const orgKmsDecryptor = await decryptWithKmsKey({
|
||||
kmsId: kmsDoc.orgKms.id
|
||||
});
|
||||
|
||||
const orgKmsDataKey = await orgKmsDecryptor({
|
||||
cipherTextBlob: kmsDoc.orgKms.encryptedDataKey
|
||||
});
|
||||
|
||||
const kmsDecryptor = await decryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
});
|
||||
|
||||
const decryptedProviderInputBlob = kmsDecryptor({
|
||||
cipherTextBlob: kmsDoc.externalKms.encryptedProviderInput
|
||||
});
|
||||
|
||||
switch (kmsDoc.externalKms.provider) {
|
||||
case KmsProviders.Aws: {
|
||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
||||
);
|
||||
|
||||
externalKms = await AwsKmsProviderFactory({
|
||||
inputs: decryptedProviderInput
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error("Invalid KMS provider.");
|
||||
}
|
||||
|
||||
return async ({ cipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||
const { data } = await externalKms.decrypt(cipherTextBlob);
|
||||
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
// internal KMS
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey);
|
||||
return Promise.resolve(decryptedBlob);
|
||||
};
|
||||
};
|
||||
|
||||
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: "KMS ID not found" });
|
||||
}
|
||||
|
||||
if (kmsDoc.externalKms) {
|
||||
let externalKms: TExternalKmsProviderFns;
|
||||
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
|
||||
throw new Error("Invalid organization KMS");
|
||||
}
|
||||
|
||||
const orgKmsDecryptor = await decryptWithKmsKey({
|
||||
kmsId: kmsDoc.orgKms.id
|
||||
});
|
||||
|
||||
const orgKmsDataKey = await orgKmsDecryptor({
|
||||
cipherTextBlob: kmsDoc.orgKms.encryptedDataKey
|
||||
});
|
||||
|
||||
const kmsDecryptor = await decryptWithInputKey({
|
||||
key: orgKmsDataKey
|
||||
});
|
||||
|
||||
const decryptedProviderInputBlob = kmsDecryptor({
|
||||
cipherTextBlob: kmsDoc.externalKms.encryptedProviderInput
|
||||
});
|
||||
|
||||
switch (kmsDoc.externalKms.provider) {
|
||||
case KmsProviders.Aws: {
|
||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
||||
);
|
||||
|
||||
externalKms = await AwsKmsProviderFactory({
|
||||
inputs: decryptedProviderInput
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error("Invalid KMS provider.");
|
||||
}
|
||||
|
||||
return async ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const { encryptedBlob } = await externalKms.encrypt(plainText);
|
||||
|
||||
return { cipherTextBlob: encryptedBlob };
|
||||
};
|
||||
}
|
||||
|
||||
// internal KMS
|
||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY);
|
||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey);
|
||||
|
||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey);
|
||||
return decryptedBlob;
|
||||
// Buffer#1 encrypted text + Buffer#2 version number
|
||||
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
|
||||
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
|
||||
|
||||
return Promise.resolve({ cipherTextBlob });
|
||||
};
|
||||
};
|
||||
|
||||
const getOrgKmsDataKey = async (orgId: string) => {
|
||||
const kmsKeyId = await getOrgKmsKeyId(orgId);
|
||||
let org = await orgDAL.findById(orgId);
|
||||
|
||||
if (!org) {
|
||||
throw new NotFoundError({ message: "Org not found" });
|
||||
}
|
||||
|
||||
if (!org.kmsEncryptedDataKey) {
|
||||
const lock = await keyStore
|
||||
.acquireLock([KeyStorePrefixes.KmsOrgDataKeyCreation, orgId], 3000, { retryCount: 3 })
|
||||
.catch(() => null);
|
||||
|
||||
try {
|
||||
if (!lock) {
|
||||
await keyStore.waitTillReady({
|
||||
key: `${KeyStorePrefixes.WaitUntilReadyKmsOrgDataKeyCreation}${orgId}`,
|
||||
keyCheckCb: (val) => val === "true",
|
||||
waitingCb: () => logger.info("KMS. Waiting for org data key to be created")
|
||||
});
|
||||
|
||||
org = await orgDAL.findById(orgId);
|
||||
} else {
|
||||
const orgDataKey = await orgDAL.transaction(async (tx) => {
|
||||
org = await orgDAL.findById(orgId, tx);
|
||||
if (org.kmsEncryptedDataKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataKey = randomSecureBytes();
|
||||
const kmsEncryptor = await encryptWithKmsKey(
|
||||
{
|
||||
kmsId: kmsKeyId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const { cipherTextBlob } = await kmsEncryptor({
|
||||
plainText: dataKey
|
||||
});
|
||||
|
||||
await orgDAL.updateById(
|
||||
org.id,
|
||||
{
|
||||
kmsEncryptedDataKey: cipherTextBlob
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await keyStore.setItemWithExpiry(
|
||||
`${KeyStorePrefixes.WaitUntilReadyKmsOrgDataKeyCreation}${orgId}`,
|
||||
10,
|
||||
"true"
|
||||
);
|
||||
|
||||
return dataKey;
|
||||
});
|
||||
|
||||
if (orgDataKey) {
|
||||
return orgDataKey;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await lock?.release();
|
||||
}
|
||||
}
|
||||
|
||||
if (!org.kmsEncryptedDataKey) {
|
||||
throw new Error("Invalid organization KMS");
|
||||
}
|
||||
|
||||
const kmsDecryptor = await decryptWithKmsKey({
|
||||
kmsId: kmsKeyId
|
||||
});
|
||||
|
||||
return kmsDecryptor({
|
||||
cipherTextBlob: org.kmsEncryptedDataKey
|
||||
});
|
||||
};
|
||||
|
||||
const getProjectSecretManagerKmsKeyId = async (projectId: string) => {
|
||||
let project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({ message: "Project not found" });
|
||||
}
|
||||
|
||||
if (!project.kmsSecretManagerKeyId) {
|
||||
const lock = await keyStore
|
||||
.acquireLock([KeyStorePrefixes.KmsProjectKeyCreation, projectId], 3000, { retryCount: 3 })
|
||||
.catch(() => null);
|
||||
|
||||
try {
|
||||
if (!lock) {
|
||||
await keyStore.waitTillReady({
|
||||
key: `${KeyStorePrefixes.WaitUntilReadyKmsProjectKeyCreation}${projectId}`,
|
||||
keyCheckCb: (val) => val === "true",
|
||||
waitingCb: () => logger.info("KMS. Waiting for project key to be created")
|
||||
});
|
||||
|
||||
project = await projectDAL.findById(projectId);
|
||||
} else {
|
||||
const kmsKeyId = await projectDAL.transaction(async (tx) => {
|
||||
project = await projectDAL.findById(projectId, tx);
|
||||
if (project.kmsSecretManagerKeyId) {
|
||||
return project.kmsSecretManagerKeyId;
|
||||
}
|
||||
|
||||
const key = await generateKmsKey({
|
||||
isReserved: true,
|
||||
orgId: project.orgId,
|
||||
tx
|
||||
});
|
||||
|
||||
await projectDAL.updateById(
|
||||
projectId,
|
||||
{
|
||||
kmsSecretManagerKeyId: key.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return key.id;
|
||||
});
|
||||
|
||||
await keyStore.setItemWithExpiry(
|
||||
`${KeyStorePrefixes.WaitUntilReadyKmsProjectKeyCreation}${projectId}`,
|
||||
10,
|
||||
"true"
|
||||
);
|
||||
|
||||
return kmsKeyId;
|
||||
}
|
||||
} finally {
|
||||
await lock?.release();
|
||||
}
|
||||
}
|
||||
|
||||
if (!project.kmsSecretManagerKeyId) {
|
||||
throw new Error("Missing project KMS key ID");
|
||||
}
|
||||
|
||||
return project.kmsSecretManagerKeyId;
|
||||
};
|
||||
|
||||
const getProjectSecretManagerKmsKey = async (projectId: string) => {
|
||||
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
|
||||
const kmsKey = await kmsDAL.findByIdWithAssociatedKms(kmsKeyId);
|
||||
|
||||
return kmsKey;
|
||||
};
|
||||
|
||||
const getProjectSecretManagerKmsDataKey = async (projectId: string) => {
|
||||
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
|
||||
let project = await projectDAL.findById(projectId);
|
||||
|
||||
if (!project.kmsSecretManagerEncryptedDataKey) {
|
||||
const lock = await keyStore
|
||||
.acquireLock([KeyStorePrefixes.KmsProjectDataKeyCreation, projectId], 3000, { retryCount: 3 })
|
||||
.catch(() => null);
|
||||
|
||||
try {
|
||||
if (!lock) {
|
||||
await keyStore.waitTillReady({
|
||||
key: `${KeyStorePrefixes.WaitUntilReadyKmsProjectDataKeyCreation}${projectId}`,
|
||||
keyCheckCb: (val) => val === "true",
|
||||
waitingCb: () => logger.info("KMS. Waiting for project data key to be created")
|
||||
});
|
||||
|
||||
project = await projectDAL.findById(projectId);
|
||||
} else {
|
||||
const projectDataKey = await projectDAL.transaction(async (tx) => {
|
||||
project = await projectDAL.findById(projectId, tx);
|
||||
if (project.kmsSecretManagerEncryptedDataKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataKey = randomSecureBytes();
|
||||
const kmsEncryptor = await encryptWithKmsKey({
|
||||
kmsId: kmsKeyId
|
||||
});
|
||||
|
||||
const { cipherTextBlob } = await kmsEncryptor({
|
||||
plainText: dataKey
|
||||
});
|
||||
|
||||
await projectDAL.updateById(
|
||||
projectId,
|
||||
{
|
||||
kmsSecretManagerEncryptedDataKey: cipherTextBlob
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await keyStore.setItemWithExpiry(
|
||||
`${KeyStorePrefixes.WaitUntilReadyKmsProjectDataKeyCreation}${projectId}`,
|
||||
10,
|
||||
"true"
|
||||
);
|
||||
return dataKey;
|
||||
});
|
||||
|
||||
if (projectDataKey) {
|
||||
return projectDataKey;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await lock?.release();
|
||||
}
|
||||
}
|
||||
|
||||
if (!project.kmsSecretManagerEncryptedDataKey) {
|
||||
throw new Error("Missing project data key");
|
||||
}
|
||||
|
||||
const kmsDecryptor = await decryptWithKmsKey({
|
||||
kmsId: kmsKeyId
|
||||
});
|
||||
|
||||
return kmsDecryptor({
|
||||
cipherTextBlob: project.kmsSecretManagerEncryptedDataKey
|
||||
});
|
||||
};
|
||||
|
||||
const updateProjectSecretManagerKmsKey = async (projectId: string, kmsId: string) => {
|
||||
const currentKms = await getProjectSecretManagerKmsKey(projectId);
|
||||
|
||||
if ((currentKms.isReserved && kmsId === INTERNAL_KMS_KEY_ID) || currentKms.id === kmsId) {
|
||||
return currentKms;
|
||||
}
|
||||
|
||||
if (kmsId !== INTERNAL_KMS_KEY_ID) {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: "Project not found."
|
||||
});
|
||||
}
|
||||
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: "KMS ID not found." });
|
||||
}
|
||||
|
||||
if (kmsDoc.orgId !== project.orgId) {
|
||||
throw new BadRequestError({
|
||||
message: "KMS ID does not belong in the organization."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const dataKey = await getProjectSecretManagerKmsDataKey(projectId);
|
||||
|
||||
return kmsDAL.transaction(async (tx) => {
|
||||
const project = await projectDAL.findById(projectId, tx);
|
||||
let newKmsId = kmsId;
|
||||
|
||||
if (newKmsId === INTERNAL_KMS_KEY_ID) {
|
||||
const key = await generateKmsKey({
|
||||
isReserved: true,
|
||||
orgId: project.orgId,
|
||||
tx
|
||||
});
|
||||
|
||||
newKmsId = key.id;
|
||||
}
|
||||
|
||||
const kmsEncryptor = await encryptWithKmsKey({ kmsId: newKmsId }, tx);
|
||||
const { cipherTextBlob } = await kmsEncryptor({ plainText: dataKey });
|
||||
await projectDAL.updateById(
|
||||
projectId,
|
||||
{
|
||||
kmsSecretManagerKeyId: newKmsId,
|
||||
kmsSecretManagerEncryptedDataKey: cipherTextBlob
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (currentKms.isReserved) {
|
||||
await kmsDAL.deleteById(currentKms.id, tx);
|
||||
}
|
||||
|
||||
return kmsDAL.findByIdWithAssociatedKms(newKmsId, tx);
|
||||
});
|
||||
};
|
||||
|
||||
const getProjectKeyBackup = async (projectId: string) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: "Project not found"
|
||||
});
|
||||
}
|
||||
|
||||
const secretManagerDataKey = await getProjectSecretManagerKmsDataKey(projectId);
|
||||
const kmsKeyIdForEncrypt = await getOrgKmsKeyId(project.orgId);
|
||||
const kmsEncryptor = await encryptWithKmsKey({ kmsId: kmsKeyIdForEncrypt });
|
||||
const { cipherTextBlob: encryptedSecretManagerDataKey } = await kmsEncryptor({ plainText: secretManagerDataKey });
|
||||
|
||||
// backup format: version.projectId.kmsFunction.kmsId.Base64(encryptedDataKey).verificationHash
|
||||
let secretManagerBackup = `v1.${projectId}.secretManager.${kmsKeyIdForEncrypt}.${encryptedSecretManagerDataKey.toString(
|
||||
"base64"
|
||||
)}`;
|
||||
|
||||
const verificationHash = generateHash(secretManagerBackup);
|
||||
secretManagerBackup = `${secretManagerBackup}.${verificationHash}`;
|
||||
|
||||
return {
|
||||
secretManager: secretManagerBackup
|
||||
};
|
||||
};
|
||||
|
||||
const loadProjectKeyBackup = async (projectId: string, backup: string) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: "Project not found"
|
||||
});
|
||||
}
|
||||
|
||||
const [, backupProjectId, , backupKmsKeyId, backupBase64EncryptedDataKey, backupHash] = backup.split(".");
|
||||
const computedHash = generateHash(backup.substring(0, backup.lastIndexOf(".")));
|
||||
if (computedHash !== backupHash) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid backup"
|
||||
});
|
||||
}
|
||||
|
||||
if (backupProjectId !== projectId) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid backup for project"
|
||||
});
|
||||
}
|
||||
|
||||
const kmsDecryptor = await decryptWithKmsKey({ kmsId: backupKmsKeyId });
|
||||
const dataKey = await kmsDecryptor({
|
||||
cipherTextBlob: Buffer.from(backupBase64EncryptedDataKey, "base64")
|
||||
});
|
||||
|
||||
const newKms = await kmsDAL.transaction(async (tx) => {
|
||||
const key = await generateKmsKey({
|
||||
isReserved: true,
|
||||
orgId: project.orgId,
|
||||
tx
|
||||
});
|
||||
|
||||
const kmsEncryptor = await encryptWithKmsKey({ kmsId: key.id }, tx);
|
||||
const { cipherTextBlob } = await kmsEncryptor({ plainText: dataKey });
|
||||
|
||||
await projectDAL.updateById(
|
||||
projectId,
|
||||
{
|
||||
kmsSecretManagerKeyId: key.id,
|
||||
kmsSecretManagerEncryptedDataKey: cipherTextBlob
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return kmsDAL.findByIdWithAssociatedKms(key.id, tx);
|
||||
});
|
||||
|
||||
return {
|
||||
secretManagerKmsKey: newKms
|
||||
};
|
||||
};
|
||||
|
||||
const getKmsById = async (kmsKeyId: string, tx?: Knex) => {
|
||||
const kms = await kmsDAL.findByIdWithAssociatedKms(kmsKeyId, tx);
|
||||
|
||||
if (!kms.id) {
|
||||
throw new NotFoundError({
|
||||
message: "KMS not found"
|
||||
});
|
||||
}
|
||||
|
||||
return kms;
|
||||
};
|
||||
|
||||
const startService = async () => {
|
||||
@@ -123,7 +726,18 @@ export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsSe
|
||||
return {
|
||||
startService,
|
||||
generateKmsKey,
|
||||
encrypt,
|
||||
decrypt
|
||||
encryptWithKmsKey,
|
||||
encryptWithInputKey,
|
||||
decryptWithKmsKey,
|
||||
decryptWithInputKey,
|
||||
getOrgKmsKeyId,
|
||||
getProjectSecretManagerKmsKeyId,
|
||||
getOrgKmsDataKey,
|
||||
getProjectSecretManagerKmsDataKey,
|
||||
getProjectSecretManagerKmsKey,
|
||||
updateProjectSecretManagerKmsKey,
|
||||
getProjectKeyBackup,
|
||||
loadProjectKeyBackup,
|
||||
getKmsById
|
||||
};
|
||||
};
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
export type TGenerateKMSDTO = {
|
||||
scopeType: "project" | "org";
|
||||
scopeId: string;
|
||||
orgId: string;
|
||||
isReserved?: boolean;
|
||||
slug?: string;
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,17 @@ export type TEncryptWithKmsDTO = {
|
||||
plainText: Buffer;
|
||||
};
|
||||
|
||||
export type TEncryptionWithKeyDTO = {
|
||||
key: Buffer;
|
||||
plainText: Buffer;
|
||||
};
|
||||
|
||||
export type TDecryptWithKmsDTO = {
|
||||
kmsId: string;
|
||||
cipherTextBlob: Buffer;
|
||||
};
|
||||
|
||||
export type TDecryptWithKeyDTO = {
|
||||
key: Buffer;
|
||||
cipherTextBlob: Buffer;
|
||||
};
|
||||
|
@@ -74,6 +74,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
db.ref("role").withSchema(TableName.OrgMembership),
|
||||
db.ref("roleId").withSchema(TableName.OrgMembership),
|
||||
db.ref("status").withSchema(TableName.OrgMembership),
|
||||
db.ref("isActive").withSchema(TableName.OrgMembership),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
@@ -207,9 +208,9 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateById = async (orgId: string, data: Partial<TOrganizations>) => {
|
||||
const updateById = async (orgId: string, data: Partial<TOrganizations>, tx?: Knex) => {
|
||||
try {
|
||||
const [org] = await db(TableName.Organization)
|
||||
const [org] = await (tx || db)(TableName.Organization)
|
||||
.where({ id: orgId })
|
||||
.update({ ...data })
|
||||
.returning("*");
|
||||
|
@@ -144,10 +144,7 @@ export const orgServiceFactory = ({
|
||||
return members;
|
||||
};
|
||||
|
||||
const findAllWorkspaces = async ({ actor, actorId, actorOrgId, actorAuthMethod, orgId }: TFindAllWorkspacesDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||
|
||||
const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => {
|
||||
const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id));
|
||||
|
||||
let workspaces: (TProjects & { organization: string } & {
|
||||
@@ -207,7 +204,8 @@ export const orgServiceFactory = ({
|
||||
orgId,
|
||||
userId: user.id,
|
||||
role: OrgMembershipRole.Admin,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
status: OrgMembershipStatus.Accepted,
|
||||
isActive: true
|
||||
};
|
||||
|
||||
await orgDAL.createMembership(createMembershipData, tx);
|
||||
@@ -311,7 +309,8 @@ export const orgServiceFactory = ({
|
||||
userId,
|
||||
orgId: org.id,
|
||||
role: OrgMembershipRole.Admin,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
status: OrgMembershipStatus.Accepted,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -460,7 +459,8 @@ export const orgServiceFactory = ({
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -491,7 +491,8 @@ export const orgServiceFactory = ({
|
||||
orgId,
|
||||
userId: user.id,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@@ -71,9 +71,8 @@ export const getProjectKmsCertificateKeyId = async ({
|
||||
if (!project.kmsCertificateKeyId) {
|
||||
// create default kms key for certificate service
|
||||
const key = await kmsService.generateKmsKey({
|
||||
scopeId: projectId,
|
||||
scopeType: "project",
|
||||
isReserved: true,
|
||||
orgId: project.orgId,
|
||||
tx
|
||||
});
|
||||
|
||||
|
@@ -21,6 +21,7 @@ import { TCertificateAuthorityDALFactory } from "../certificate-authority/certif
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
@@ -38,11 +39,14 @@ import {
|
||||
TCreateProjectDTO,
|
||||
TDeleteProjectDTO,
|
||||
TGetProjectDTO,
|
||||
TGetProjectKmsKey,
|
||||
TListProjectCasDTO,
|
||||
TListProjectCertsDTO,
|
||||
TLoadProjectKmsBackupDTO,
|
||||
TToggleProjectAutoCapitalizationDTO,
|
||||
TUpdateAuditLogsRetentionDTO,
|
||||
TUpdateProjectDTO,
|
||||
TUpdateProjectKmsDTO,
|
||||
TUpdateProjectNameDTO,
|
||||
TUpdateProjectVersionLimitDTO,
|
||||
TUpgradeProjectDTO
|
||||
@@ -76,6 +80,14 @@ type TProjectServiceFactoryDep = {
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||
kmsService: Pick<
|
||||
TKmsServiceFactory,
|
||||
| "updateProjectSecretManagerKmsKey"
|
||||
| "getProjectKeyBackup"
|
||||
| "loadProjectKeyBackup"
|
||||
| "getKmsById"
|
||||
| "getProjectSecretManagerKmsKeyId"
|
||||
>;
|
||||
};
|
||||
|
||||
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
||||
@@ -100,7 +112,8 @@ export const projectServiceFactory = ({
|
||||
identityProjectMembershipRoleDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
keyStore
|
||||
keyStore,
|
||||
kmsService
|
||||
}: TProjectServiceFactoryDep) => {
|
||||
/*
|
||||
* Create workspace. Make user the admin
|
||||
@@ -111,7 +124,8 @@ export const projectServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
workspaceName,
|
||||
slug: projectSlug
|
||||
slug: projectSlug,
|
||||
kmsKeyId
|
||||
}: TCreateProjectDTO) => {
|
||||
const organization = await orgDAL.findOne({ id: actorOrgId });
|
||||
|
||||
@@ -139,16 +153,28 @@ export const projectServiceFactory = ({
|
||||
const results = await projectDAL.transaction(async (tx) => {
|
||||
const ghostUser = await orgService.addGhostUser(organization.id, tx);
|
||||
|
||||
if (kmsKeyId) {
|
||||
const kms = await kmsService.getKmsById(kmsKeyId, tx);
|
||||
|
||||
if (kms.orgId !== organization.id) {
|
||||
throw new BadRequestError({
|
||||
message: "KMS does not belong in the organization"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const project = await projectDAL.create(
|
||||
{
|
||||
name: workspaceName,
|
||||
orgId: organization.id,
|
||||
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
||||
version: ProjectVersion.V2,
|
||||
pitVersionLimit: 10
|
||||
pitVersionLimit: 10,
|
||||
kmsSecretManagerKeyId: kmsKeyId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
// set ghost user as admin of project
|
||||
const projectMembership = await projectMembershipDAL.create(
|
||||
{
|
||||
@@ -647,6 +673,109 @@ export const projectServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const updateProjectKmsKey = async ({
|
||||
projectId,
|
||||
secretManagerKmsKeyId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TUpdateProjectKmsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
|
||||
|
||||
const secretManagerKmsKey = await kmsService.updateProjectSecretManagerKmsKey(projectId, secretManagerKmsKeyId);
|
||||
|
||||
return {
|
||||
secretManagerKmsKey
|
||||
};
|
||||
};
|
||||
|
||||
const getProjectKmsBackup = async ({
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.externalKms) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to get KMS backup due to plan restriction. Upgrade to the enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
const kmsBackup = await kmsService.getProjectKeyBackup(projectId);
|
||||
return kmsBackup;
|
||||
};
|
||||
|
||||
const loadProjectKmsBackup = async ({
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
backup
|
||||
}: TLoadProjectKmsBackupDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.externalKms) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to load KMS backup due to plan restriction. Upgrade to the enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
const kmsBackup = await kmsService.loadProjectKeyBackup(projectId, backup);
|
||||
return kmsBackup;
|
||||
};
|
||||
|
||||
const getProjectKmsKeys = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TGetProjectKmsKey) => {
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "User is not a member of the project"
|
||||
});
|
||||
}
|
||||
|
||||
const kmsKeyId = await kmsService.getProjectSecretManagerKmsKeyId(projectId);
|
||||
const kmsKey = await kmsService.getKmsById(kmsKeyId);
|
||||
|
||||
return { secretManagerKmsKey: kmsKey };
|
||||
};
|
||||
|
||||
return {
|
||||
createProject,
|
||||
deleteProject,
|
||||
@@ -660,6 +789,10 @@ export const projectServiceFactory = ({
|
||||
listProjectCas,
|
||||
listProjectCertificates,
|
||||
updateVersionLimit,
|
||||
updateAuditLogsRetention
|
||||
updateAuditLogsRetention,
|
||||
updateProjectKmsKey,
|
||||
getProjectKmsBackup,
|
||||
loadProjectKmsBackup,
|
||||
getProjectKmsKeys
|
||||
};
|
||||
};
|
||||
|
@@ -27,6 +27,7 @@ export type TCreateProjectDTO = {
|
||||
actorOrgId?: string;
|
||||
workspaceName: string;
|
||||
slug?: string;
|
||||
kmsKeyId?: string;
|
||||
};
|
||||
|
||||
export type TDeleteProjectBySlugDTO = {
|
||||
@@ -97,3 +98,13 @@ export type TListProjectCertsDTO = {
|
||||
offset: number;
|
||||
limit: number;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateProjectKmsDTO = {
|
||||
secretManagerKmsKeyId: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TLoadProjectKmsBackupDTO = {
|
||||
backup: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetProjectKmsKey = TProjectPermission;
|
||||
|
@@ -322,7 +322,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
||||
.first();
|
||||
if (folder) {
|
||||
const { envId, envName, envSlug, ...el } = folder;
|
||||
return { ...el, environment: { envId, envName, envSlug } };
|
||||
return { ...el, environment: { envId, envName, envSlug }, envId };
|
||||
}
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by id" });
|
||||
|
@@ -6,7 +6,7 @@ import { TSecretFoldersInsert } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
@@ -14,6 +14,7 @@ import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
||||
import {
|
||||
TCreateFolderDTO,
|
||||
TDeleteFolderDTO,
|
||||
TGetFolderByIdDTO,
|
||||
TGetFolderDTO,
|
||||
TUpdateFolderDTO,
|
||||
TUpdateManyFoldersDTO
|
||||
@@ -368,11 +369,22 @@ export const secretFolderServiceFactory = ({
|
||||
return folders;
|
||||
};
|
||||
|
||||
const getFolderById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetFolderByIdDTO) => {
|
||||
const folder = await folderDAL.findById(id);
|
||||
if (!folder) throw new NotFoundError({ message: "folder not found" });
|
||||
// folder list is allowed to be read by anyone
|
||||
// permission to check does user has access
|
||||
await permissionService.getProjectPermission(actor, actorId, folder.projectId, actorAuthMethod, actorOrgId);
|
||||
|
||||
return folder;
|
||||
};
|
||||
|
||||
return {
|
||||
createFolder,
|
||||
updateFolder,
|
||||
updateManyFolders,
|
||||
deleteFolder,
|
||||
getFolders
|
||||
getFolders,
|
||||
getFolderById
|
||||
};
|
||||
};
|
||||
|
@@ -37,3 +37,7 @@ export type TGetFolderDTO = {
|
||||
environment: string;
|
||||
path: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetFolderByIdDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TableName, TSecretSharing } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>;
|
||||
|
||||
@@ -13,15 +13,58 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
||||
const pruneExpiredSharedSecrets = async (tx?: Knex) => {
|
||||
try {
|
||||
const today = new Date();
|
||||
const docs = await (tx || db)(TableName.SecretSharing).where("expiresAt", "<", today).del();
|
||||
const docs = await (tx || db)(TableName.SecretSharing)
|
||||
.where("expiresAt", "<", today)
|
||||
.andWhere("encryptedValue", "<>", "")
|
||||
.update({
|
||||
encryptedValue: "",
|
||||
tag: "",
|
||||
iv: "",
|
||||
hashedHex: ""
|
||||
});
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "pruneExpiredSharedSecrets" });
|
||||
}
|
||||
};
|
||||
|
||||
const findActiveSharedSecrets = async (filters: Partial<TSecretSharing>, tx?: Knex) => {
|
||||
try {
|
||||
const now = new Date();
|
||||
return await (tx || db)(TableName.SecretSharing)
|
||||
.where(filters)
|
||||
.andWhere("expiresAt", ">", now)
|
||||
.andWhere("encryptedValue", "<>", "")
|
||||
.select(selectAllTableCols(TableName.SecretSharing))
|
||||
.orderBy("expiresAt", "asc");
|
||||
} catch (error) {
|
||||
throw new DatabaseError({
|
||||
error,
|
||||
name: "Find Active Shared Secrets"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const softDeleteById = async (id: string) => {
|
||||
try {
|
||||
await sharedSecretOrm.updateById(id, {
|
||||
encryptedValue: "",
|
||||
iv: "",
|
||||
tag: "",
|
||||
hashedHex: ""
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({
|
||||
error,
|
||||
name: "Soft Delete Shared Secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...sharedSecretOrm,
|
||||
pruneExpiredSharedSecrets
|
||||
pruneExpiredSharedSecrets,
|
||||
softDeleteById,
|
||||
findActiveSharedSecrets
|
||||
};
|
||||
};
|
||||
|
@@ -101,7 +101,7 @@ export const secretSharingServiceFactory = ({
|
||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
||||
const userSharedSecrets = await secretSharingDAL.find({ userId: actorId, orgId }, { sort: [["expiresAt", "asc"]] });
|
||||
const userSharedSecrets = await secretSharingDAL.findActiveSharedSecrets({ userId: actorId, orgId });
|
||||
return userSharedSecrets;
|
||||
};
|
||||
|
||||
@@ -113,7 +113,7 @@ export const secretSharingServiceFactory = ({
|
||||
}
|
||||
if (sharedSecret.expiresAfterViews != null && sharedSecret.expiresAfterViews >= 0) {
|
||||
if (sharedSecret.expiresAfterViews === 0) {
|
||||
await secretSharingDAL.deleteById(sharedSecretId);
|
||||
await secretSharingDAL.softDeleteById(sharedSecretId);
|
||||
return;
|
||||
}
|
||||
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
|
||||
|
@@ -642,7 +642,7 @@ export const secretQueueFactory = ({
|
||||
});
|
||||
|
||||
queueService.start(QueueName.SecretWebhook, async (job) => {
|
||||
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL });
|
||||
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL });
|
||||
});
|
||||
|
||||
return {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
@@ -20,6 +21,7 @@ type TSuperAdminServiceFactoryDep = {
|
||||
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
||||
};
|
||||
|
||||
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
|
||||
@@ -36,7 +38,8 @@ export const superAdminServiceFactory = ({
|
||||
userDAL,
|
||||
authService,
|
||||
orgService,
|
||||
keyStore
|
||||
keyStore,
|
||||
licenseService
|
||||
}: TSuperAdminServiceFactoryDep) => {
|
||||
const initServerCfg = async () => {
|
||||
// TODO(akhilmhdh): bad pattern time less change this later to me itself
|
||||
@@ -219,6 +222,12 @@ export const superAdminServiceFactory = ({
|
||||
};
|
||||
|
||||
const deleteUser = async (userId: string) => {
|
||||
if (!licenseService.onPremFeatures?.instanceUserManagement) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to delete user due to plan restriction. Upgrade to Infisical's Pro plan."
|
||||
});
|
||||
}
|
||||
|
||||
const user = await userDAL.deleteById(userId);
|
||||
return user;
|
||||
};
|
||||
|
@@ -9,6 +9,7 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TWebhookDALFactory } from "./webhook-dal";
|
||||
import { WebhookType } from "./webhook-types";
|
||||
@@ -66,11 +67,16 @@ export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record<str
|
||||
|
||||
export const getWebhookPayload = (
|
||||
eventName: string,
|
||||
workspaceId: string,
|
||||
environment: string,
|
||||
secretPath?: string,
|
||||
type?: string | null
|
||||
details: {
|
||||
workspaceName: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
type?: string | null;
|
||||
}
|
||||
) => {
|
||||
const { workspaceName, workspaceId, environment, secretPath, type } = details;
|
||||
|
||||
switch (type) {
|
||||
case WebhookType.SLACK:
|
||||
return {
|
||||
@@ -80,8 +86,8 @@ export const getWebhookPayload = (
|
||||
color: "#E7F256",
|
||||
fields: [
|
||||
{
|
||||
title: "Workspace ID",
|
||||
value: workspaceId,
|
||||
title: "Project",
|
||||
value: workspaceName,
|
||||
short: false
|
||||
},
|
||||
{
|
||||
@@ -117,7 +123,9 @@ export type TFnTriggerWebhookDTO = {
|
||||
environment: string;
|
||||
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
};
|
||||
|
||||
// this is reusable function
|
||||
// used in secret queue to trigger webhook and update status when secrets changes
|
||||
export const fnTriggerWebhook = async ({
|
||||
@@ -125,7 +133,8 @@ export const fnTriggerWebhook = async ({
|
||||
secretPath,
|
||||
projectId,
|
||||
webhookDAL,
|
||||
projectEnvDAL
|
||||
projectEnvDAL,
|
||||
projectDAL
|
||||
}: TFnTriggerWebhookDTO) => {
|
||||
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
|
||||
const toBeTriggeredHooks = webhooks.filter(
|
||||
@@ -134,9 +143,19 @@ export const fnTriggerWebhook = async ({
|
||||
);
|
||||
if (!toBeTriggeredHooks.length) return;
|
||||
logger.info("Secret webhook job started", { environment, secretPath, projectId });
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const webhooksTriggered = await Promise.allSettled(
|
||||
toBeTriggeredHooks.map((hook) =>
|
||||
triggerWebhookRequest(hook, getWebhookPayload("secrets.modified", projectId, environment, secretPath, hook.type))
|
||||
triggerWebhookRequest(
|
||||
hook,
|
||||
getWebhookPayload("secrets.modified", {
|
||||
workspaceName: project.name,
|
||||
workspaceId: projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
type: hook.type
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TWebhookDALFactory } from "./webhook-dal";
|
||||
import { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
|
||||
@@ -20,12 +21,18 @@ import {
|
||||
type TWebhookServiceFactoryDep = {
|
||||
webhookDAL: TWebhookDALFactory;
|
||||
projectEnvDAL: TProjectEnvDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
|
||||
|
||||
export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionService }: TWebhookServiceFactoryDep) => {
|
||||
export const webhookServiceFactory = ({
|
||||
webhookDAL,
|
||||
projectEnvDAL,
|
||||
permissionService,
|
||||
projectDAL
|
||||
}: TWebhookServiceFactoryDep) => {
|
||||
const createWebhook = async ({
|
||||
actor,
|
||||
actorId,
|
||||
@@ -124,13 +131,21 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
|
||||
const project = await projectDAL.findById(webhook.projectId);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
let webhookError: string | undefined;
|
||||
try {
|
||||
await triggerWebhookRequest(
|
||||
webhook,
|
||||
getWebhookPayload("test", webhook.projectId, webhook.environment.slug, webhook.secretPath, webhook.type)
|
||||
getWebhookPayload("test", {
|
||||
workspaceName: project.name,
|
||||
workspaceId: webhook.projectId,
|
||||
environment: webhook.environment.slug,
|
||||
secretPath: webhook.secretPath,
|
||||
type: webhook.type
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
webhookError = (err as Error).message;
|
||||
|
@@ -10,7 +10,7 @@ require (
|
||||
github.com/fatih/semgroup v1.2.0
|
||||
github.com/gitleaks/go-gitdiff v0.8.0
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/infisical/go-sdk v0.2.0
|
||||
github.com/infisical/go-sdk v0.3.0
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
||||
github.com/muesli/mango-cobra v1.2.0
|
||||
@@ -19,7 +19,7 @@ require (
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
|
||||
github.com/rs/cors v1.9.0
|
||||
github.com/rs/cors v1.11.0
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.8.1
|
||||
@@ -52,7 +52,7 @@ require (
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/danieljoos/wincred v1.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
|
||||
github.com/dvsekhvalnov/jose2go v1.6.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
|
12
cli/go.sum
12
cli/go.sum
@@ -117,8 +117,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||
github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM=
|
||||
github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
|
||||
github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY=
|
||||
github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -263,8 +263,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/infisical/go-sdk v0.2.0 h1:n1/KNdYpeQavSqVwC9BfeV8VRzf3N2X9zO1tzQOSj5Q=
|
||||
github.com/infisical/go-sdk v0.2.0/go.mod h1:vHTDVw3k+wfStXab513TGk1n53kaKF2xgLqpw/xvtl4=
|
||||
github.com/infisical/go-sdk v0.3.0 h1:Ls71t227F4CWVQWdStcwv8WDyfHe8eRlyAuMRNHsmlQ=
|
||||
github.com/infisical/go-sdk v0.3.0/go.mod h1:vHTDVw3k+wfStXab513TGk1n53kaKF2xgLqpw/xvtl4=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -356,8 +356,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=
|
||||
github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
|
||||
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
|
||||
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
|
||||
|
@@ -122,6 +122,21 @@ func handleAwsIamAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.Infi
|
||||
return infisicalClient.Auth().AwsIamAuthLogin(identityId)
|
||||
}
|
||||
|
||||
func handleOidcAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME)
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
jwt, err := util.GetCmdFlagOrEnv(cmd, "oidc-jwt", util.INFISICAL_OIDC_AUTH_JWT_NAME)
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return infisicalClient.Auth().OidcAuthLogin(identityId, jwt)
|
||||
}
|
||||
|
||||
func formatAuthMethod(authMethod string) string {
|
||||
return strings.ReplaceAll(authMethod, "-", " ")
|
||||
}
|
||||
@@ -257,6 +272,7 @@ var loginCmd = &cobra.Command{
|
||||
util.AuthStrategy.GCP_ID_TOKEN_AUTH: handleGcpIdTokenAuthLogin,
|
||||
util.AuthStrategy.GCP_IAM_AUTH: handleGcpIamAuthLogin,
|
||||
util.AuthStrategy.AWS_IAM_AUTH: handleAwsIamAuthLogin,
|
||||
util.AuthStrategy.OIDC_AUTH: handleOidcAuthLogin,
|
||||
}
|
||||
|
||||
credential, err := authStrategies[strategy](cmd, infisicalClient)
|
||||
@@ -456,6 +472,7 @@ func init() {
|
||||
loginCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods")
|
||||
loginCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth")
|
||||
loginCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth")
|
||||
loginCmd.Flags().String("oidc-jwt", "", "JWT for OIDC authentication")
|
||||
}
|
||||
|
||||
func DomainOverridePrompt() (bool, error) {
|
||||
@@ -616,7 +633,7 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R
|
||||
loginTwoResponseResult, err := api.CallLogin2V2(httpClient, api.GetLoginTwoV2Request{
|
||||
Email: email,
|
||||
ClientProof: hex.EncodeToString(srpM1),
|
||||
Password: password,
|
||||
Password: password,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
@@ -9,6 +9,7 @@ var AuthStrategy = struct {
|
||||
GCP_ID_TOKEN_AUTH AuthStrategyType
|
||||
GCP_IAM_AUTH AuthStrategyType
|
||||
AWS_IAM_AUTH AuthStrategyType
|
||||
OIDC_AUTH AuthStrategyType
|
||||
}{
|
||||
UNIVERSAL_AUTH: "universal-auth",
|
||||
KUBERNETES_AUTH: "kubernetes",
|
||||
@@ -16,6 +17,7 @@ var AuthStrategy = struct {
|
||||
GCP_ID_TOKEN_AUTH: "gcp-id-token",
|
||||
GCP_IAM_AUTH: "gcp-iam",
|
||||
AWS_IAM_AUTH: "aws-iam",
|
||||
OIDC_AUTH: "oidc-auth",
|
||||
}
|
||||
|
||||
var AVAILABLE_AUTH_STRATEGIES = []AuthStrategyType{
|
||||
@@ -25,6 +27,7 @@ var AVAILABLE_AUTH_STRATEGIES = []AuthStrategyType{
|
||||
AuthStrategy.GCP_ID_TOKEN_AUTH,
|
||||
AuthStrategy.GCP_IAM_AUTH,
|
||||
AuthStrategy.AWS_IAM_AUTH,
|
||||
AuthStrategy.OIDC_AUTH,
|
||||
}
|
||||
|
||||
func IsAuthMethodValid(authMethod string, allowUserAuth bool) (isValid bool, strategy AuthStrategyType) {
|
||||
|
@@ -19,6 +19,9 @@ const (
|
||||
// GCP Auth
|
||||
INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME = "INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH"
|
||||
|
||||
// OIDC Auth
|
||||
INFISICAL_OIDC_AUTH_JWT_NAME = "INFISICAL_OIDC_AUTH_JWT"
|
||||
|
||||
// Generic env variable used for auth methods that require a machine identity ID
|
||||
INFISICAL_MACHINE_IDENTITY_ID_NAME = "INFISICAL_MACHINE_IDENTITY_ID"
|
||||
|
||||
|
50
company/handbook/hiring.mdx
Normal file
50
company/handbook/hiring.mdx
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: "Hiring"
|
||||
sidebarTitle: "Hiring"
|
||||
description: "The guide to hiring at Infisical."
|
||||
---
|
||||
|
||||
Infisical is actively growing and we are hiring for many positions at any given time. This page describes some details of the hiring process we have.
|
||||
|
||||
## Strategy
|
||||
|
||||
Infisical recruitment strategy relies on 100% inbound interest by default. Many of our team members have previously used Infisical or contributed to our [open source project](https://github.com/Infisical/infisical). This allows us to hire the best candidates who are most interested in working at Infisical.
|
||||
|
||||
## Geography
|
||||
|
||||
Infisical is a remote-first company, and we have team members across the whole globe. That being said, there are some legal and accounting limitations that we need to abide by. As a result, we are currently only open to hiring from the following countries:
|
||||
|
||||
- Australia
|
||||
- Austria
|
||||
- Belgium
|
||||
- Brazil
|
||||
- Canada
|
||||
- Chile
|
||||
- Costa Rica
|
||||
- Denmark
|
||||
- Finland
|
||||
- France
|
||||
- Germany
|
||||
- India
|
||||
- Ireland
|
||||
- Israel
|
||||
- Italy
|
||||
- Japan
|
||||
- Kenya
|
||||
- Latvia
|
||||
- Luxembourg
|
||||
- Mexico
|
||||
- Netherlands
|
||||
- New Zealand
|
||||
- Philippines
|
||||
- Poland
|
||||
- Portugal
|
||||
- Singapore
|
||||
- South Africa
|
||||
- South Korea
|
||||
- Spain
|
||||
- Switzerland
|
||||
- Sweden
|
||||
- UAE
|
||||
- United Kingdom
|
||||
- United States
|
@@ -58,7 +58,8 @@
|
||||
"pages": [
|
||||
"handbook/onboarding",
|
||||
"handbook/spending-money",
|
||||
"handbook/time-off"
|
||||
"handbook/time-off",
|
||||
"handbook/hiring"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
4
docs/api-reference/endpoints/folders/get-by-id.mdx
Normal file
4
docs/api-reference/endpoints/folders/get-by-id.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/folders/{id}"
|
||||
---
|
@@ -8,7 +8,8 @@ infisical login
|
||||
```
|
||||
|
||||
### Description
|
||||
The CLI uses authentication to verify your identity. When you enter the correct email and password for your account, a token is generated and saved in your system Keyring to allow you to make future interactions with the CLI.
|
||||
|
||||
The CLI uses authentication to verify your identity. When you enter the correct email and password for your account, a token is generated and saved in your system Keyring to allow you to make future interactions with the CLI.
|
||||
|
||||
To change where the login credentials are stored, visit the [vaults command](./vault).
|
||||
|
||||
@@ -17,12 +18,12 @@ If you have added multiple users, you can switch between the users by using the
|
||||
<Info>
|
||||
When you authenticate with **any other method than `user`**, an access token will be printed to the console upon successful login. This token can be used to authenticate with the Infisical API and the CLI by passing it in the `--token` flag when applicable.
|
||||
|
||||
Use flag `--plain` along with `--silent` to print only the token in plain text when using a machine identity auth method.
|
||||
|
||||
Use flag `--plain` along with `--silent` to print only the token in plain text when using a machine identity auth method.
|
||||
|
||||
</Info>
|
||||
|
||||
|
||||
### Flags
|
||||
|
||||
The login command supports a number of flags that you can use for different authentication methods. Below is a list of all the flags that can be used with the login command.
|
||||
|
||||
<AccordionGroup>
|
||||
@@ -52,6 +53,7 @@ The login command supports a number of flags that you can use for different auth
|
||||
<Tip>
|
||||
The `client-id` flag can be substituted with the `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` environment variable.
|
||||
</Tip>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="--client-secret">
|
||||
```bash
|
||||
@@ -63,6 +65,7 @@ The login command supports a number of flags that you can use for different auth
|
||||
<Tip>
|
||||
The `client-secret` flag can be substituted with the `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` environment variable.
|
||||
</Tip>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="--machine-identity-id">
|
||||
```bash
|
||||
@@ -75,6 +78,7 @@ The login command supports a number of flags that you can use for different auth
|
||||
<Tip>
|
||||
The `machine-identity-id` flag can be substituted with the `INFISICAL_MACHINE_IDENTITY_ID` environment variable.
|
||||
</Tip>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="--service-account-token-path">
|
||||
```bash
|
||||
@@ -88,6 +92,7 @@ The login command supports a number of flags that you can use for different auth
|
||||
<Tip>
|
||||
The `service-account-token-path` flag can be substituted with the `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH` environment variable.
|
||||
</Tip>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="--service-account-key-file-path">
|
||||
```bash
|
||||
@@ -100,9 +105,23 @@ The login command supports a number of flags that you can use for different auth
|
||||
<Tip>
|
||||
The `service-account-key-path` flag can be substituted with the `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` environment variable.
|
||||
</Tip>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Accordion title="--oidc-jwt">
|
||||
```bash
|
||||
infisical login --oidc-jwt=<oidc-jwt-token>
|
||||
```
|
||||
|
||||
#### Description
|
||||
The JWT provided by an identity provider for OIDC authentication.
|
||||
|
||||
<Tip>
|
||||
The `oidc-jwt` flag can be substituted with the `INFISICAL_OIDC_AUTH_JWT` environment variable.
|
||||
</Tip>
|
||||
|
||||
</Accordion>
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
@@ -121,6 +140,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa
|
||||
Your machine identity client secret.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
||||
<Steps>
|
||||
@@ -134,6 +154,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa
|
||||
infisical login --method=universal-auth --client-id=<client-id> --client-secret=<client-secret>
|
||||
```
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
</Accordion>
|
||||
<Accordion title="Native Kubernetes">
|
||||
@@ -148,6 +169,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa
|
||||
Path to the Kubernetes service account token to use. Default: `/var/run/secrets/kubernetes.io/serviceaccount/token`.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
||||
<Steps>
|
||||
@@ -162,6 +184,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa
|
||||
infisical login --method=kubernetes --machine-identity-id=<machine-identity-id> --service-account-token-path=<service-account-token-path>
|
||||
```
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
</Accordion>
|
||||
@@ -213,6 +236,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="GCP IAM">
|
||||
The GCP IAM method is used to authenticate with Infisical with a GCP service account key.
|
||||
@@ -235,11 +259,12 @@ The Infisical CLI supports multiple authentication methods. Below are the availa
|
||||
<Step title="Obtain an access token">
|
||||
Run the `login` command with the following flags to obtain an access token:
|
||||
|
||||
```bash
|
||||
```bash
|
||||
infisical login --method=gcp-iam --machine-identity-id=<machine-identity-id> --service-account-key-file-path=<service-account-key-file-path>
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Native AWS IAM">
|
||||
The AWS IAM method is used to authenticate with Infisical with an AWS IAM role while running in an AWS environment like EC2, Lambda, etc.
|
||||
@@ -264,10 +289,40 @@ The Infisical CLI supports multiple authentication methods. Below are the availa
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="OIDC Auth">
|
||||
The OIDC Auth method is used to authenticate with Infisical via identity tokens with OIDC.
|
||||
|
||||
<ParamField query="Flags">
|
||||
<Expandable title="properties">
|
||||
<ParamField query="machine-identity-id" type="string" required>
|
||||
Your machine identity ID.
|
||||
</ParamField>
|
||||
<ParamField query="oidc-jwt" type="string" required>
|
||||
The OIDC JWT from the identity provider.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
<Steps>
|
||||
<Step title="Create an OIDC machine identity">
|
||||
To create an OIDC machine identity, follow the step by step guide outlined [here](/documentation/platform/identities/oidc-auth/general).
|
||||
</Step>
|
||||
<Step title="Obtain an access token">
|
||||
Run the `login` command with the following flags to obtain an access token:
|
||||
|
||||
```bash
|
||||
infisical login --method=oidc-auth --machine-identity-id=<machine-identity-id> --oidc-jwt=<oidc-jwt>
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### Machine Identity Authentication Quick Start
|
||||
|
||||
In this example we'll be using the `universal-auth` method to login to obtain an Infisical access token, which we will then use to fetch secrets with.
|
||||
|
||||
<Steps>
|
||||
@@ -277,8 +332,8 @@ In this example we'll be using the `universal-auth` method to login to obtain an
|
||||
```
|
||||
|
||||
Now that we've set the `INFISICAL_TOKEN` environment variable, we can use the CLI to interact with Infisical. The CLI will automatically check for the presence of the `INFISICAL_TOKEN` environment variable and use it for authentication.
|
||||
|
||||
|
||||
|
||||
|
||||
Alternatively, if you would rather use the `--token` flag to pass the token directly, you can do so by running the following command:
|
||||
|
||||
```bash
|
||||
@@ -297,6 +352,7 @@ In this example we'll be using the `universal-auth` method to login to obtain an
|
||||
The `--recursive`, and `--env` flag is optional and will fetch all secrets in subfolders. The default environment is `dev` if no `--env` flag is provided.
|
||||
</Info>
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
And that's it! Now you're ready to start using the Infisical CLI to interact with your secrets, with the use of Machine Identities.
|
||||
|
87
docs/documentation/platform/kms/aws-kms.mdx
Normal file
87
docs/documentation/platform/kms/aws-kms.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: "AWS Key Management Service (KMS)"
|
||||
description: "Learn how to manage encryption using AWS KMS"
|
||||
---
|
||||
|
||||
You can configure your projects to use AWS KMS keys for encryption, enhancing the security and management of your secrets.
|
||||
|
||||
## Setup AWS KMS in the Organization Settings
|
||||
|
||||
Follow these steps to set up AWS KMS for your organization:
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to the organization settings and select the Encryption tab.">
|
||||

|
||||
</Step>
|
||||
<Step title="Click on the 'Add' button">
|
||||

|
||||
Click the 'Add' button to begin adding a new external KMS.
|
||||
</Step>
|
||||
<Step title="Select 'AWS KMS'">
|
||||

|
||||
Choose 'AWS KMS' from the list of encryption providers.
|
||||
</Step>
|
||||
<Step title="Provide the inputs for AWS KMS">
|
||||
Fill in the required details for AWS KMS:
|
||||
<ParamField path="Alias" type="string" required>
|
||||
Name for referencing the AWS KMS key within the organization.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Description" type="string">
|
||||
Short description of the AWS KMS key.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Authentication Mode" type="string" required>
|
||||
Authentication mode for AWS, either "AWS Assume Role" or "Access Key".
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="IAM Role ARN For Role Assumption" type="string" required>
|
||||
ARN of the AWS role to assume for providing Infisical access to the AWS KMS Key (required if Authentication Mode is "AWS Assume Role")
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Assume Role External ID" type="string">
|
||||
Custom identifier for additional validation during role assumption.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Access Key ID" type="string" required>
|
||||
AWS IAM Access Key ID for authentication (required if Authentication Mode is "Access Key").
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Secret Access Key" type="string" required>
|
||||
AWS IAM Secret Access Key for authentication (required if Authentication Mode is "Access Key").
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="AWS Region" type="string" required>
|
||||
AWS region where the AWS KMS Key is located.
|
||||
</ParamField>
|
||||
<ParamField path="AWS KMS Key ID" type="string">
|
||||
Key ID of the AWS KMS Key. If left blank, Infisical will generate and use a new AWS KMS Key in the specified region.
|
||||

|
||||
</ParamField>
|
||||
|
||||
</Step>
|
||||
<Step title="Click Save">
|
||||
Save your configuration to apply the settings.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
You now have an AWS KMS Key configured at the organization level. You can assign these keys to existing projects via the Project Settings page.
|
||||
|
||||
## Assign AWS KMS Key to an Existing Project
|
||||
|
||||
Follow these steps to assign an AWS KMS key to a project:
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Project Settings and proceed to the Encryption Tab">
|
||||

|
||||
</Step>
|
||||
<Step title="Under the Key Management section, select your newly added AWS KMS key from the dropdown">
|
||||

|
||||
Choose the AWS KMS key you configured earlier.
|
||||
</Step>
|
||||
<Step title="Click Save">
|
||||
Save the changes to apply the new encryption settings to your project.
|
||||
</Step>
|
||||
</Steps>
|
28
docs/documentation/platform/kms/overview.mdx
Normal file
28
docs/documentation/platform/kms/overview.mdx
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: "Key Management Service (KMS)"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Learn how to configure your project's encryption"
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Infisical leverages a Key Management Service (KMS) to securely encrypt and decrypt secrets in your projects.
|
||||
|
||||
## Overview
|
||||
|
||||
Infisical's KMS ensures the security of your project's secrets through the following mechanisms:
|
||||
|
||||
- Each project is assigned a unique workspace key, which is responsible for encrypting and decrypting secret values.
|
||||
- The workspace key itself is encrypted using the project's configured KMS.
|
||||
- When secrets are requested, the workspace key is derived from the configured KMS. This key is then used to decrypt the secret values on-demand before sending them to the requesting client.
|
||||
|
||||
## Configuration
|
||||
|
||||
You can set the KMS for new projects during project creation.
|
||||

|
||||
For existing projects, you can configure the KMS from the Project Settings page.
|
||||

|
||||
|
||||
## External KMS
|
||||
|
||||
Infisical supports the use of external KMS solutions to enhance security and compliance. You can configure your project to use services like [AWS Key Management Service](./aws-kms) for managing encryption.
|
BIN
docs/images/platform/kms/aws/aws-kms-key-id.png
Normal file
BIN
docs/images/platform/kms/aws/aws-kms-key-id.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 151 KiB |
Binary file not shown.
After Width: | Height: | Size: 348 KiB |
BIN
docs/images/platform/kms/aws/encryption-org-settings-add.png
Normal file
BIN
docs/images/platform/kms/aws/encryption-org-settings-add.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 694 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user