Compare commits
34 Commits
misc/add-u
...
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 | |||
eb03fa4d4e | |||
0a7a9b6c37 | |||
a1bfbdf32e | |||
a07983ddc8 | |||
b9d5330db6 | |||
538ca972e6 | |||
9cce604ca8 |
@ -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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -13,9 +13,9 @@ export const KmsKeysSchema = z.object({
|
|||||||
isDisabled: z.boolean().default(false).nullable().optional(),
|
isDisabled: z.boolean().default(false).nullable().optional(),
|
||||||
isReserved: z.boolean().default(true).nullable().optional(),
|
isReserved: z.boolean().default(true).nullable().optional(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
|
slug: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date()
|
||||||
slug: z.string()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const OrganizationsSchema = z.object({
|
export const OrganizationsSchema = z.object({
|
||||||
@ -16,7 +18,8 @@ export const OrganizationsSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
authEnforced: z.boolean().default(false).nullable().optional(),
|
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()
|
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
|
||||||
|
kmsEncryptedDataKey: zodBuffer.nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const ProjectsSchema = z.object({
|
export const ProjectsSchema = z.object({
|
||||||
@ -20,7 +22,8 @@ export const ProjectsSchema = z.object({
|
|||||||
pitVersionLimit: z.number().default(10),
|
pitVersionLimit: z.number().default(10),
|
||||||
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
||||||
auditLogsRetentionDays: z.number().nullable().optional(),
|
auditLogsRetentionDays: z.number().nullable().optional(),
|
||||||
kmsSecretManagerKeyId: z.string().uuid().nullable().optional()
|
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
|
||||||
|
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import {
|
import {
|
||||||
ExternalKmsAwsSchema,
|
ExternalKmsAwsSchema,
|
||||||
ExternalKmsInputSchema,
|
ExternalKmsInputSchema,
|
||||||
@ -19,6 +20,23 @@ const sanitizedExternalSchema = KmsKeysSchema.extend({
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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({
|
const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({
|
||||||
external: ExternalKmsSchema.pick({
|
external: ExternalKmsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
@ -39,7 +57,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z.string().min(1).trim().toLowerCase().optional(),
|
slug: z.string().min(1).trim().toLowerCase(),
|
||||||
description: z.string().min(1).trim().optional(),
|
description: z.string().min(1).trim().optional(),
|
||||||
provider: ExternalKmsInputSchema
|
provider: ExternalKmsInputSchema
|
||||||
}),
|
}),
|
||||||
@ -60,6 +78,21 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
provider: req.body.provider,
|
provider: req.body.provider,
|
||||||
description: req.body.description
|
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 };
|
return { externalKms };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -97,6 +130,21 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: req.body.description,
|
description: req.body.description,
|
||||||
id: req.params.id
|
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 };
|
return { externalKms };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -126,6 +174,19 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
id: req.params.id
|
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 };
|
return { externalKms };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -155,10 +216,48 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
id: req.params.id
|
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 };
|
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({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/slug/:slug",
|
url: "/slug/:slug",
|
||||||
|
@ -4,6 +4,7 @@ import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
|||||||
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
||||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||||
|
import { registerExternalKmsRouter } from "./external-kms-router";
|
||||||
import { registerGroupRouter } from "./group-router";
|
import { registerGroupRouter } from "./group-router";
|
||||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||||
import { registerLdapRouter } from "./ldap-router";
|
import { registerLdapRouter } from "./ldap-router";
|
||||||
@ -87,4 +88,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
{ prefix: "/additional-privilege" }
|
{ 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 { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
|
import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
|
||||||
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@ -171,4 +171,178 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async () => ({ actors: [] })
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -138,7 +138,14 @@ export enum EventType {
|
|||||||
GET_CERT = "get-cert",
|
GET_CERT = "get-cert",
|
||||||
DELETE_CERT = "delete-cert",
|
DELETE_CERT = "delete-cert",
|
||||||
REVOKE_CERT = "revoke-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 {
|
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 =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@ -1264,4 +1327,11 @@ export type Event =
|
|||||||
| GetCert
|
| GetCert
|
||||||
| DeleteCert
|
| DeleteCert
|
||||||
| RevokeCert
|
| RevokeCert
|
||||||
| GetCertBody;
|
| GetCertBody
|
||||||
|
| CreateKmsEvent
|
||||||
|
| UpdateKmsEvent
|
||||||
|
| DeleteKmsEvent
|
||||||
|
| GetKmsEvent
|
||||||
|
| UpdateProjectKmsEvent
|
||||||
|
| GetProjectKmsBackupEvent
|
||||||
|
| LoadProjectKmsBackupEvent;
|
||||||
|
@ -72,7 +72,7 @@ export const certificateAuthorityCrlServiceFactory = ({
|
|||||||
kmsId: keyId
|
kmsId: keyId
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedCrl = kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
|
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
|
||||||
const crl = new x509.X509Crl(decryptedCrl);
|
const crl = new x509.X509Crl(decryptedCrl);
|
||||||
|
|
||||||
const base64crl = crl.toString("base64");
|
const base64crl = crl.toString("base64");
|
||||||
|
@ -31,6 +31,8 @@ export const externalKmsDALFactory = (db: TDbClient) => {
|
|||||||
isReserved: el.isReserved,
|
isReserved: el.isReserved,
|
||||||
orgId: el.orgId,
|
orgId: el.orgId,
|
||||||
slug: el.slug,
|
slug: el.slug,
|
||||||
|
createdAt: el.createdAt,
|
||||||
|
updatedAt: el.updatedAt,
|
||||||
externalKms: {
|
externalKms: {
|
||||||
id: el.externalKmsId,
|
id: el.externalKmsId,
|
||||||
provider: el.externalKmsProvider,
|
provider: el.externalKmsProvider,
|
||||||
|
@ -6,6 +6,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
|||||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { TExternalKmsDALFactory } from "./external-kms-dal";
|
import { TExternalKmsDALFactory } from "./external-kms-dal";
|
||||||
@ -22,9 +23,13 @@ import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model";
|
|||||||
|
|
||||||
type TExternalKmsServiceFactoryDep = {
|
type TExternalKmsServiceFactoryDep = {
|
||||||
externalKmsDAL: TExternalKmsDALFactory;
|
externalKmsDAL: TExternalKmsDALFactory;
|
||||||
kmsService: Pick<TKmsServiceFactory, "getOrgKmsKeyId" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
kmsService: Pick<
|
||||||
|
TKmsServiceFactory,
|
||||||
|
"getOrgKmsKeyId" | "decryptWithInputKey" | "encryptWithInputKey" | "getOrgKmsDataKey"
|
||||||
|
>;
|
||||||
kmsDAL: Pick<TKmsKeyDALFactory, "create" | "updateById" | "findById" | "deleteById" | "findOne">;
|
kmsDAL: Pick<TKmsKeyDALFactory, "create" | "updateById" | "findById" | "deleteById" | "findOne">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TExternalKmsServiceFactory = ReturnType<typeof externalKmsServiceFactory>;
|
export type TExternalKmsServiceFactory = ReturnType<typeof externalKmsServiceFactory>;
|
||||||
@ -32,6 +37,7 @@ export type TExternalKmsServiceFactory = ReturnType<typeof externalKmsServiceFac
|
|||||||
export const externalKmsServiceFactory = ({
|
export const externalKmsServiceFactory = ({
|
||||||
externalKmsDAL,
|
externalKmsDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
licenseService,
|
||||||
kmsService,
|
kmsService,
|
||||||
kmsDAL
|
kmsDAL
|
||||||
}: TExternalKmsServiceFactoryDep) => {
|
}: TExternalKmsServiceFactoryDep) => {
|
||||||
@ -51,7 +57,15 @@ export const externalKmsServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
|
||||||
|
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());
|
const kmsSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||||
|
|
||||||
let sanitizedProviderInput = "";
|
let sanitizedProviderInput = "";
|
||||||
@ -59,20 +73,22 @@ export const externalKmsServiceFactory = ({
|
|||||||
case KmsProviders.Aws:
|
case KmsProviders.Aws:
|
||||||
{
|
{
|
||||||
const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs });
|
const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs });
|
||||||
await externalKms.validateConnection();
|
|
||||||
// if missing kms key this generate a new kms key id and returns new provider input
|
// if missing kms key this generate a new kms key id and returns new provider input
|
||||||
const newProviderInput = await externalKms.generateInputKmsKey();
|
const newProviderInput = await externalKms.generateInputKmsKey();
|
||||||
sanitizedProviderInput = JSON.stringify(newProviderInput);
|
sanitizedProviderInput = JSON.stringify(newProviderInput);
|
||||||
|
|
||||||
|
await externalKms.validateConnection();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
throw new BadRequestError({ message: "external kms provided is invalid" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId);
|
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(actorOrgId);
|
||||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
const kmsEncryptor = await kmsService.encryptWithInputKey({
|
||||||
kmsId: orgKmsKeyId
|
key: orgKmsDataKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const { cipherTextBlob: encryptedProviderInputs } = kmsEncryptor({
|
const { cipherTextBlob: encryptedProviderInputs } = kmsEncryptor({
|
||||||
plainText: Buffer.from(sanitizedProviderInput, "utf8")
|
plainText: Buffer.from(sanitizedProviderInput, "utf8")
|
||||||
});
|
});
|
||||||
@ -119,18 +135,27 @@ export const externalKmsServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
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 kmsSlug = slug ? slugify(slug) : undefined;
|
||||||
|
|
||||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||||
|
|
||||||
const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId);
|
|
||||||
let sanitizedProviderInput = "";
|
let sanitizedProviderInput = "";
|
||||||
if (provider) {
|
if (provider) {
|
||||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId);
|
||||||
kmsId: orgDefaultKmsId
|
const kmsDecryptor = await kmsService.decryptWithInputKey({
|
||||||
|
key: orgKmsDataKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedProviderInputBlob = kmsDecryptor({
|
const decryptedProviderInputBlob = kmsDecryptor({
|
||||||
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
||||||
});
|
});
|
||||||
@ -154,8 +179,9 @@ export const externalKmsServiceFactory = ({
|
|||||||
|
|
||||||
let encryptedProviderInputs: Buffer | undefined;
|
let encryptedProviderInputs: Buffer | undefined;
|
||||||
if (sanitizedProviderInput) {
|
if (sanitizedProviderInput) {
|
||||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(actorOrgId);
|
||||||
kmsId: orgDefaultKmsId
|
const kmsEncryptor = await kmsService.encryptWithInputKey({
|
||||||
|
key: orgKmsDataKey
|
||||||
});
|
});
|
||||||
const { cipherTextBlob } = kmsEncryptor({
|
const { cipherTextBlob } = kmsEncryptor({
|
||||||
plainText: Buffer.from(sanitizedProviderInput, "utf8")
|
plainText: Buffer.from(sanitizedProviderInput, "utf8")
|
||||||
@ -197,7 +223,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||||
|
|
||||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||||
@ -218,7 +244,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||||
|
|
||||||
const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId });
|
const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId });
|
||||||
|
|
||||||
@ -234,15 +260,17 @@ export const externalKmsServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||||
|
|
||||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||||
|
|
||||||
const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId);
|
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId);
|
||||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
const kmsDecryptor = await kmsService.decryptWithInputKey({
|
||||||
kmsId: orgDefaultKmsId
|
key: orgKmsDataKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedProviderInputBlob = kmsDecryptor({
|
const decryptedProviderInputBlob = kmsDecryptor({
|
||||||
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
||||||
});
|
});
|
||||||
@ -273,15 +301,16 @@ export const externalKmsServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||||
|
|
||||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||||
|
|
||||||
const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId);
|
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId);
|
||||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
const kmsDecryptor = await kmsService.decryptWithInputKey({
|
||||||
kmsId: orgDefaultKmsId
|
key: orgKmsDataKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedProviderInputBlob = kmsDecryptor({
|
const decryptedProviderInputBlob = kmsDecryptor({
|
||||||
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
||||||
});
|
});
|
||||||
|
@ -50,17 +50,26 @@ type TAwsKmsProviderFactoryReturn = TExternalKmsProviderFns & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Promise<TAwsKmsProviderFactoryReturn> => {
|
export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Promise<TAwsKmsProviderFactoryReturn> => {
|
||||||
const providerInputs = await ExternalKmsAwsSchema.parseAsync(inputs);
|
let providerInputs = await ExternalKmsAwsSchema.parseAsync(inputs);
|
||||||
const awsClient = await getAwsKmsClient(providerInputs);
|
let awsClient = await getAwsKmsClient(providerInputs);
|
||||||
|
|
||||||
const generateInputKmsKey = async () => {
|
const generateInputKmsKey = async () => {
|
||||||
if (providerInputs.kmsKeyId) return providerInputs;
|
if (providerInputs.kmsKeyId) return providerInputs;
|
||||||
|
|
||||||
const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] });
|
const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] });
|
||||||
const kmsKey = await awsClient.send(command);
|
const kmsKey = await awsClient.send(command);
|
||||||
|
|
||||||
if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key");
|
if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key");
|
||||||
|
|
||||||
return { ...providerInputs, kmsKeyId: kmsKey.KeyMetadata?.KeyId };
|
const updatedProviderInputs = await ExternalKmsAwsSchema.parseAsync({
|
||||||
|
...providerInputs,
|
||||||
|
kmsKeyId: kmsKey.KeyMetadata?.KeyId
|
||||||
|
});
|
||||||
|
|
||||||
|
providerInputs = updatedProviderInputs;
|
||||||
|
awsClient = await getAwsKmsClient(providerInputs);
|
||||||
|
|
||||||
|
return updatedProviderInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateConnection = async () => {
|
const validateConnection = async () => {
|
||||||
|
@ -39,7 +39,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
secretApproval: false,
|
secretApproval: false,
|
||||||
secretRotation: true,
|
secretRotation: true,
|
||||||
caCrl: false,
|
caCrl: false,
|
||||||
instanceUserManagement: false
|
instanceUserManagement: false,
|
||||||
|
externalKms: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
@ -57,6 +57,7 @@ export type TFeatureSet = {
|
|||||||
secretRotation: true;
|
secretRotation: true;
|
||||||
caCrl: false;
|
caCrl: false;
|
||||||
instanceUserManagement: false;
|
instanceUserManagement: false;
|
||||||
|
externalKms: false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgPlansTableDTO = {
|
export type TOrgPlansTableDTO = {
|
||||||
|
@ -21,7 +21,8 @@ export enum OrgPermissionSubjects {
|
|||||||
Groups = "groups",
|
Groups = "groups",
|
||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
Identity = "identity"
|
Identity = "identity",
|
||||||
|
Kms = "kms"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@ -37,7 +38,8 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
|
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
@ -100,6 +102,11 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||||
can(OrgPermissionActions.Delete, 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 });
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -28,7 +28,8 @@ export enum ProjectPermissionSub {
|
|||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
CertificateAuthorities = "certificate-authorities",
|
CertificateAuthorities = "certificate-authorities",
|
||||||
Certificates = "certificates"
|
Certificates = "certificates",
|
||||||
|
Kms = "kms"
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubjectFields = {
|
type SubjectFields = {
|
||||||
@ -60,7 +61,8 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
|
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||||
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||||
|
|
||||||
const buildAdminPermissionRules = () => {
|
const buildAdminPermissionRules = () => {
|
||||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
@ -157,6 +159,8 @@ const buildAdminPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||||
|
|
||||||
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,7 +6,15 @@ export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
|||||||
|
|
||||||
// all the key prefixes used must be set here to avoid conflict
|
// all the key prefixes used must be set here to avoid conflict
|
||||||
export enum KeyStorePrefixes {
|
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 = {
|
type TWaitTillReady = {
|
||||||
|
@ -116,6 +116,8 @@ export const decryptAsymmetric = ({ ciphertext, nonce, publicKey, privateKey }:
|
|||||||
|
|
||||||
export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64");
|
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 = () => {
|
export const generateAsymmetricKeyPair = () => {
|
||||||
const pair = nacl.box.keyPair();
|
const pair = nacl.box.keyPair();
|
||||||
|
|
||||||
|
@ -316,7 +316,8 @@ export const registerRoutes = async (
|
|||||||
kmsDAL,
|
kmsDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
permissionService,
|
permissionService,
|
||||||
externalKmsDAL
|
externalKmsDAL,
|
||||||
|
licenseService
|
||||||
});
|
});
|
||||||
|
|
||||||
const trustedIpService = trustedIpServiceFactory({
|
const trustedIpService = trustedIpServiceFactory({
|
||||||
@ -624,7 +625,8 @@ export const registerRoutes = async (
|
|||||||
certificateDAL,
|
certificateDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
keyStore
|
keyStore,
|
||||||
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectEnvService = projectEnvServiceFactory({
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
|
@ -161,7 +161,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
message: "Slug must be a valid slug"
|
message: "Slug must be a valid slug"
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECTS.CREATE.slug)
|
.describe(PROJECTS.CREATE.slug),
|
||||||
|
kmsKeyId: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -177,7 +178,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
workspaceName: req.body.projectName,
|
workspaceName: req.body.projectName,
|
||||||
slug: req.body.slug
|
slug: req.body.slug,
|
||||||
|
kmsKeyId: req.body.kmsKeyId
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.telemetry.sendPostHogEvents({
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
|
@ -78,7 +78,7 @@ export const getCaCredentials = async ({
|
|||||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
kmsId: keyId
|
kmsId: keyId
|
||||||
});
|
});
|
||||||
const decryptedPrivateKey = kmsDecryptor({
|
const decryptedPrivateKey = await kmsDecryptor({
|
||||||
cipherTextBlob: caSecret.encryptedPrivateKey
|
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -129,13 +129,13 @@ export const getCaCertChain = async ({
|
|||||||
kmsId: keyId
|
kmsId: keyId
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedCaCert = kmsDecryptor({
|
const decryptedCaCert = await kmsDecryptor({
|
||||||
cipherTextBlob: caCert.encryptedCertificate
|
cipherTextBlob: caCert.encryptedCertificate
|
||||||
});
|
});
|
||||||
|
|
||||||
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||||
|
|
||||||
const decryptedChain = kmsDecryptor({
|
const decryptedChain = await kmsDecryptor({
|
||||||
cipherTextBlob: caCert.encryptedCertificateChain
|
cipherTextBlob: caCert.encryptedCertificateChain
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ export const rebuildCaCrl = async ({
|
|||||||
kmsId: keyId
|
kmsId: keyId
|
||||||
});
|
});
|
||||||
|
|
||||||
const privateKey = kmsDecryptor({
|
const privateKey = await kmsDecryptor({
|
||||||
cipherTextBlob: caSecret.encryptedPrivateKey
|
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -210,7 +210,7 @@ export const rebuildCaCrl = async ({
|
|||||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||||
kmsId: keyId
|
kmsId: keyId
|
||||||
});
|
});
|
||||||
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({
|
const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
|
||||||
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ export const certificateAuthorityQueueFactory = ({
|
|||||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
kmsId: keyId
|
kmsId: keyId
|
||||||
});
|
});
|
||||||
const privateKey = kmsDecryptor({
|
const privateKey = await kmsDecryptor({
|
||||||
cipherTextBlob: caSecret.encryptedPrivateKey
|
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ export const certificateAuthorityQueueFactory = ({
|
|||||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||||
kmsId: keyId
|
kmsId: keyId
|
||||||
});
|
});
|
||||||
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({
|
const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
|
||||||
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -181,11 +181,11 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({
|
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||||
plainText: Buffer.from(new Uint8Array(cert.rawData))
|
plainText: Buffer.from(new Uint8Array(cert.rawData))
|
||||||
});
|
});
|
||||||
|
|
||||||
const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({
|
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||||
plainText: Buffer.alloc(0)
|
plainText: Buffer.alloc(0)
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -209,7 +209,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
signingKey: keys.privateKey
|
signingKey: keys.privateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({
|
const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
|
||||||
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -224,7 +224,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
|
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
|
||||||
const skObj = KeyObject.from(keys.privateKey);
|
const skObj = KeyObject.from(keys.privateKey);
|
||||||
|
|
||||||
const { cipherTextBlob: encryptedPrivateKey } = kmsEncryptor({
|
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||||
plainText: skObj.export({
|
plainText: skObj.export({
|
||||||
type: "pkcs8",
|
type: "pkcs8",
|
||||||
format: "der"
|
format: "der"
|
||||||
@ -458,7 +458,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||||
const decryptedCaCert = kmsDecryptor({
|
const decryptedCaCert = await kmsDecryptor({
|
||||||
cipherTextBlob: caCert.encryptedCertificate
|
cipherTextBlob: caCert.encryptedCertificate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -615,11 +615,11 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
kmsId: certificateManagerKmsId
|
kmsId: certificateManagerKmsId
|
||||||
});
|
});
|
||||||
|
|
||||||
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({
|
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||||
plainText: Buffer.from(new Uint8Array(certObj.rawData))
|
plainText: Buffer.from(new Uint8Array(certObj.rawData))
|
||||||
});
|
});
|
||||||
|
|
||||||
const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({
|
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||||
plainText: Buffer.from(certificateChain)
|
plainText: Buffer.from(certificateChain)
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -693,7 +693,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
kmsId: certificateManagerKmsId
|
kmsId: certificateManagerKmsId
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedCaCert = kmsDecryptor({
|
const decryptedCaCert = await kmsDecryptor({
|
||||||
cipherTextBlob: caCert.encryptedCertificate
|
cipherTextBlob: caCert.encryptedCertificate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -803,7 +803,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||||
kmsId: certificateManagerKmsId
|
kmsId: certificateManagerKmsId
|
||||||
});
|
});
|
||||||
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({
|
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||||
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -173,7 +173,7 @@ export const certificateServiceFactory = ({
|
|||||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
kmsId: certificateManagerKeyId
|
kmsId: certificateManagerKeyId
|
||||||
});
|
});
|
||||||
const decryptedCert = kmsDecryptor({
|
const decryptedCert = await kmsDecryptor({
|
||||||
cipherTextBlob: certBody.encryptedCertificate
|
cipherTextBlob: certBody.encryptedCertificate
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
|||||||
try {
|
try {
|
||||||
const result = await (tx || db.replicaNode())(TableName.KmsKey)
|
const result = await (tx || db.replicaNode())(TableName.KmsKey)
|
||||||
.where({ [`${TableName.KmsKey}.id` as "id"]: id })
|
.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.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`)
|
||||||
.leftJoin(TableName.ExternalKms, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`)
|
.leftJoin(TableName.ExternalKms, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`)
|
||||||
.first()
|
.first()
|
||||||
@ -31,11 +32,19 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"),
|
db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"),
|
||||||
db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"),
|
db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"),
|
||||||
db.ref("statusDetails").withSchema(TableName.ExternalKms).as("externalKmsStatusDetails")
|
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 = {
|
const data = {
|
||||||
...KmsKeysSchema.parse(result),
|
...KmsKeysSchema.parse(result),
|
||||||
isExternal: Boolean(result?.externalKmsId),
|
isExternal: Boolean(result?.externalKmsId),
|
||||||
|
orgKms: {
|
||||||
|
id: result?.orgKmsDefaultKeyId,
|
||||||
|
encryptedDataKey: result?.orgKmsEncryptedDataKey
|
||||||
|
},
|
||||||
externalKms: result?.externalKmsId
|
externalKms: result?.externalKmsId
|
||||||
? {
|
? {
|
||||||
id: result.externalKmsId,
|
id: result.externalKmsId,
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
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 { getConfig } from "@app/lib/config/env";
|
||||||
import { randomSecureBytes } from "@app/lib/crypto";
|
import { randomSecureBytes } from "@app/lib/crypto";
|
||||||
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
|
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 { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
@ -33,6 +40,7 @@ type TKmsServiceFactoryDep = {
|
|||||||
|
|
||||||
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
|
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_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
|
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
|
||||||
@ -83,22 +91,6 @@ export const kmsServiceFactory = ({
|
|||||||
return doc;
|
return doc;
|
||||||
};
|
};
|
||||||
|
|
||||||
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">) => {
|
|
||||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
|
||||||
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" });
|
|
||||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
|
||||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
|
||||||
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, 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 };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
|
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
|
||||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
@ -111,19 +103,6 @@ export const kmsServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const decryptWithKmsKey = async ({ kmsId }: Omit<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
|
||||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
|
||||||
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" });
|
|
||||||
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 decryptedBlob;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
|
|
||||||
@ -135,14 +114,33 @@ export const kmsServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getOrgKmsKeyId = async (orgId: string) => {
|
const getOrgKmsKeyId = async (orgId: string) => {
|
||||||
const keyId = await orgDAL.transaction(async (tx) => {
|
let org = await orgDAL.findById(orgId);
|
||||||
const org = await orgDAL.findById(orgId, tx);
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
throw new BadRequestError({ message: "Org not found" });
|
throw new NotFoundError({ message: "Org not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!org.kmsDefaultKeyId) {
|
if (!org.kmsDefaultKeyId) {
|
||||||
// create default kms key for certificate service
|
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({
|
const key = await generateKmsKey({
|
||||||
isReserved: true,
|
isReserved: true,
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
@ -157,24 +155,259 @@ export const kmsServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
return key.id;
|
await keyStore.setItemWithExpiry(`${KeyStorePrefixes.WaitUntilReadyKmsOrgKeyCreation}${orgId}`, 10, "true");
|
||||||
}
|
|
||||||
|
|
||||||
return org.kmsDefaultKeyId;
|
return key.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
return keyId;
|
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);
|
||||||
|
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||||
|
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, 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 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) => {
|
const getProjectSecretManagerKmsKeyId = async (projectId: string) => {
|
||||||
const keyId = await projectDAL.transaction(async (tx) => {
|
let project = await projectDAL.findById(projectId);
|
||||||
const project = await projectDAL.findById(projectId, tx);
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new BadRequestError({ message: "Project not found" });
|
throw new NotFoundError({ message: "Project not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!project.kmsSecretManagerKeyId) {
|
if (!project.kmsSecretManagerKeyId) {
|
||||||
// create default kms key for certificate service
|
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({
|
const key = await generateKmsKey({
|
||||||
isReserved: true,
|
isReserved: true,
|
||||||
orgId: project.orgId,
|
orgId: project.orgId,
|
||||||
@ -190,12 +423,259 @@ export const kmsServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return key.id;
|
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;
|
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")
|
||||||
});
|
});
|
||||||
|
|
||||||
return keyId;
|
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 () => {
|
const startService = async () => {
|
||||||
@ -251,6 +731,13 @@ export const kmsServiceFactory = ({
|
|||||||
decryptWithKmsKey,
|
decryptWithKmsKey,
|
||||||
decryptWithInputKey,
|
decryptWithInputKey,
|
||||||
getOrgKmsKeyId,
|
getOrgKmsKeyId,
|
||||||
getProjectSecretManagerKmsKeyId
|
getProjectSecretManagerKmsKeyId,
|
||||||
|
getOrgKmsDataKey,
|
||||||
|
getProjectSecretManagerKmsDataKey,
|
||||||
|
getProjectSecretManagerKmsKey,
|
||||||
|
updateProjectSecretManagerKmsKey,
|
||||||
|
getProjectKeyBackup,
|
||||||
|
loadProjectKeyBackup,
|
||||||
|
getKmsById
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -21,6 +21,7 @@ import { TCertificateAuthorityDALFactory } from "../certificate-authority/certif
|
|||||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
@ -38,11 +39,14 @@ import {
|
|||||||
TCreateProjectDTO,
|
TCreateProjectDTO,
|
||||||
TDeleteProjectDTO,
|
TDeleteProjectDTO,
|
||||||
TGetProjectDTO,
|
TGetProjectDTO,
|
||||||
|
TGetProjectKmsKey,
|
||||||
TListProjectCasDTO,
|
TListProjectCasDTO,
|
||||||
TListProjectCertsDTO,
|
TListProjectCertsDTO,
|
||||||
|
TLoadProjectKmsBackupDTO,
|
||||||
TToggleProjectAutoCapitalizationDTO,
|
TToggleProjectAutoCapitalizationDTO,
|
||||||
TUpdateAuditLogsRetentionDTO,
|
TUpdateAuditLogsRetentionDTO,
|
||||||
TUpdateProjectDTO,
|
TUpdateProjectDTO,
|
||||||
|
TUpdateProjectKmsDTO,
|
||||||
TUpdateProjectNameDTO,
|
TUpdateProjectNameDTO,
|
||||||
TUpdateProjectVersionLimitDTO,
|
TUpdateProjectVersionLimitDTO,
|
||||||
TUpgradeProjectDTO
|
TUpgradeProjectDTO
|
||||||
@ -76,6 +80,14 @@ type TProjectServiceFactoryDep = {
|
|||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||||
|
kmsService: Pick<
|
||||||
|
TKmsServiceFactory,
|
||||||
|
| "updateProjectSecretManagerKmsKey"
|
||||||
|
| "getProjectKeyBackup"
|
||||||
|
| "loadProjectKeyBackup"
|
||||||
|
| "getKmsById"
|
||||||
|
| "getProjectSecretManagerKmsKeyId"
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
||||||
@ -100,7 +112,8 @@ export const projectServiceFactory = ({
|
|||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
keyStore
|
keyStore,
|
||||||
|
kmsService
|
||||||
}: TProjectServiceFactoryDep) => {
|
}: TProjectServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Create workspace. Make user the admin
|
* Create workspace. Make user the admin
|
||||||
@ -111,7 +124,8 @@ export const projectServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
slug: projectSlug
|
slug: projectSlug,
|
||||||
|
kmsKeyId
|
||||||
}: TCreateProjectDTO) => {
|
}: TCreateProjectDTO) => {
|
||||||
const organization = await orgDAL.findOne({ id: actorOrgId });
|
const organization = await orgDAL.findOne({ id: actorOrgId });
|
||||||
|
|
||||||
@ -139,16 +153,28 @@ export const projectServiceFactory = ({
|
|||||||
const results = await projectDAL.transaction(async (tx) => {
|
const results = await projectDAL.transaction(async (tx) => {
|
||||||
const ghostUser = await orgService.addGhostUser(organization.id, 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(
|
const project = await projectDAL.create(
|
||||||
{
|
{
|
||||||
name: workspaceName,
|
name: workspaceName,
|
||||||
orgId: organization.id,
|
orgId: organization.id,
|
||||||
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
||||||
version: ProjectVersion.V2,
|
version: ProjectVersion.V2,
|
||||||
pitVersionLimit: 10
|
pitVersionLimit: 10,
|
||||||
|
kmsSecretManagerKeyId: kmsKeyId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
// set ghost user as admin of project
|
// set ghost user as admin of project
|
||||||
const projectMembership = await projectMembershipDAL.create(
|
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 {
|
return {
|
||||||
createProject,
|
createProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
@ -660,6 +789,10 @@ export const projectServiceFactory = ({
|
|||||||
listProjectCas,
|
listProjectCas,
|
||||||
listProjectCertificates,
|
listProjectCertificates,
|
||||||
updateVersionLimit,
|
updateVersionLimit,
|
||||||
updateAuditLogsRetention
|
updateAuditLogsRetention,
|
||||||
|
updateProjectKmsKey,
|
||||||
|
getProjectKmsBackup,
|
||||||
|
loadProjectKmsBackup,
|
||||||
|
getProjectKmsKeys
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -27,6 +27,7 @@ export type TCreateProjectDTO = {
|
|||||||
actorOrgId?: string;
|
actorOrgId?: string;
|
||||||
workspaceName: string;
|
workspaceName: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
|
kmsKeyId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteProjectBySlugDTO = {
|
export type TDeleteProjectBySlugDTO = {
|
||||||
@ -97,3 +98,13 @@ export type TListProjectCertsDTO = {
|
|||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateProjectKmsDTO = {
|
||||||
|
secretManagerKmsKeyId: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TLoadProjectKmsBackupDTO = {
|
||||||
|
backup: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetProjectKmsKey = TProjectPermission;
|
||||||
|
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
@ -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
After Width: | Height: | Size: 151 KiB |
After Width: | Height: | Size: 348 KiB |
BIN
docs/images/platform/kms/aws/encryption-org-settings-add.png
Normal file
After Width: | Height: | Size: 694 KiB |
BIN
docs/images/platform/kms/aws/encryption-org-settings.png
Normal file
After Width: | Height: | Size: 482 KiB |
After Width: | Height: | Size: 476 KiB |
BIN
docs/images/platform/kms/aws/encryption-project-settings.png
Normal file
After Width: | Height: | Size: 479 KiB |
BIN
docs/images/platform/kms/configure-kms-existing.png
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
docs/images/platform/kms/configure-kms-new.png
Normal file
After Width: | Height: | Size: 104 KiB |
@ -154,6 +154,13 @@
|
|||||||
"documentation/platform/dynamic-secrets/aws-iam"
|
"documentation/platform/dynamic-secrets/aws-iam"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Key Management",
|
||||||
|
"pages": [
|
||||||
|
"documentation/platform/kms/overview",
|
||||||
|
"documentation/platform/kms/aws-kms"
|
||||||
|
]
|
||||||
|
},
|
||||||
"documentation/platform/secret-sharing"
|
"documentation/platform/secret-sharing"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -19,7 +19,8 @@ export enum OrgPermissionSubjects {
|
|||||||
Groups = "groups",
|
Groups = "groups",
|
||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
Identity = "identity"
|
Identity = "identity",
|
||||||
|
Kms = "kms"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@ -35,6 +36,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
|
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
||||||
|
|
||||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||||
|
@ -26,7 +26,8 @@ export enum ProjectPermissionSub {
|
|||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
CertificateAuthorities = "certificate-authorities",
|
CertificateAuthorities = "certificate-authorities",
|
||||||
Certificates = "certificates"
|
Certificates = "certificates",
|
||||||
|
Kms = "kms"
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubjectFields = {
|
type SubjectFields = {
|
||||||
|
@ -16,6 +16,7 @@ export * from "./incidentContacts";
|
|||||||
export * from "./integrationAuth";
|
export * from "./integrationAuth";
|
||||||
export * from "./integrations";
|
export * from "./integrations";
|
||||||
export * from "./keys";
|
export * from "./keys";
|
||||||
|
export * from "./kms";
|
||||||
export * from "./ldapConfig";
|
export * from "./ldapConfig";
|
||||||
export * from "./oidcConfig";
|
export * from "./oidcConfig";
|
||||||
export * from "./organization";
|
export * from "./organization";
|
||||||
|
8
frontend/src/hooks/api/kms/index.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export {
|
||||||
|
useAddExternalKms,
|
||||||
|
useLoadProjectKmsBackup,
|
||||||
|
useRemoveExternalKms,
|
||||||
|
useUpdateExternalKms,
|
||||||
|
useUpdateProjectKms
|
||||||
|
} from "./mutations";
|
||||||
|
export { useGetActiveProjectKms, useGetExternalKmsById, useGetExternalKmsList } from "./queries";
|
94
frontend/src/hooks/api/kms/mutations.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { kmsKeys } from "./queries";
|
||||||
|
import { AddExternalKmsType } from "./types";
|
||||||
|
|
||||||
|
export const useAddExternalKms = (orgId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ slug, description, provider }: AddExternalKmsType) => {
|
||||||
|
const { data } = await apiRequest.post("/api/v1/external-kms", {
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
provider
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(kmsKeys.getExternalKmsList(orgId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateExternalKms = (orgId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
kmsId,
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
provider
|
||||||
|
}: {
|
||||||
|
kmsId: string;
|
||||||
|
} & AddExternalKmsType) => {
|
||||||
|
const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, {
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
provider
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { kmsId }) => {
|
||||||
|
queryClient.invalidateQueries(kmsKeys.getExternalKmsList(orgId));
|
||||||
|
queryClient.invalidateQueries(kmsKeys.getExternalKmsById(kmsId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRemoveExternalKms = (orgId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (kmsId: string) => {
|
||||||
|
const { data } = await apiRequest.delete(`/api/v1/external-kms/${kmsId}`);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(kmsKeys.getExternalKmsList(orgId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateProjectKms = (projectId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (updatedData: { secretManagerKmsKeyId: string }) => {
|
||||||
|
const { data } = await apiRequest.patch(`/api/v1/workspace/${projectId}/kms`, updatedData);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(kmsKeys.getActiveProjectKms(projectId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLoadProjectKmsBackup = (projectId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (backup: string) => {
|
||||||
|
const { data } = await apiRequest.post(`/api/v1/workspace/${projectId}/kms/backup`, {
|
||||||
|
backup
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(kmsKeys.getActiveProjectKms(projectId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
63
frontend/src/hooks/api/kms/queries.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { Kms, KmsListEntry } from "./types";
|
||||||
|
|
||||||
|
export const kmsKeys = {
|
||||||
|
getExternalKmsList: (orgId: string) => ["get-all-external-kms", { orgId }],
|
||||||
|
getExternalKmsById: (id: string) => ["get-external-kms", { id }],
|
||||||
|
getActiveProjectKms: (projectId: string) => ["get-active-project-kms", { projectId }]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetExternalKmsList = (orgId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: kmsKeys.getExternalKmsList(orgId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const {
|
||||||
|
data: { externalKmsList }
|
||||||
|
} = await apiRequest.get<{ externalKmsList: KmsListEntry[] }>("/api/v1/external-kms");
|
||||||
|
return externalKmsList;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetExternalKmsById = (kmsId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: kmsKeys.getExternalKmsById(kmsId),
|
||||||
|
enabled: Boolean(kmsId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const {
|
||||||
|
data: { externalKms }
|
||||||
|
} = await apiRequest.get<{ externalKms: Kms }>(`/api/v1/external-kms/${kmsId}`);
|
||||||
|
return externalKms;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetActiveProjectKms = (projectId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: kmsKeys.getActiveProjectKms(projectId),
|
||||||
|
enabled: Boolean(projectId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const {
|
||||||
|
data: { secretManagerKmsKey }
|
||||||
|
} = await apiRequest.get<{
|
||||||
|
secretManagerKmsKey: {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
isExternal: string;
|
||||||
|
};
|
||||||
|
}>(`/api/v1/workspace/${projectId}/kms`);
|
||||||
|
return secretManagerKmsKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchProjectKmsBackup = async (projectId: string) => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
secretManager: string;
|
||||||
|
}>(`/api/v1/workspace/${projectId}/kms/backup`);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
97
frontend/src/hooks/api/kms/types.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export type Kms = {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
orgId: string;
|
||||||
|
slug: string;
|
||||||
|
external: {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
statusDetails: string;
|
||||||
|
provider: string;
|
||||||
|
providerInput: Record<string, any>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KmsListEntry = {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
isDisabled: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
slug: string;
|
||||||
|
externalKms: {
|
||||||
|
provider: string;
|
||||||
|
status: string;
|
||||||
|
statusDetails: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ExternalKmsProvider {
|
||||||
|
AWS = "aws"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const INTERNAL_KMS_KEY_ID = "internal";
|
||||||
|
|
||||||
|
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 const ExternalKmsInputSchema = z.discriminatedUnion("type", [
|
||||||
|
z.object({ type: z.literal(ExternalKmsProvider.AWS), inputs: ExternalKmsAwsSchema })
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const AddExternalKmsSchema = z.object({
|
||||||
|
slug: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.refine((v) => slugify(v) === v, {
|
||||||
|
message: "Alias must be a valid slug"
|
||||||
|
}),
|
||||||
|
description: z.string().trim().min(1).default(""),
|
||||||
|
provider: ExternalKmsInputSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AddExternalKmsType = z.infer<typeof AddExternalKmsSchema>;
|
@ -40,4 +40,5 @@ export type SubscriptionPlan = {
|
|||||||
has_used_trial: boolean;
|
has_used_trial: boolean;
|
||||||
caCrl: boolean;
|
caCrl: boolean;
|
||||||
instanceUserManagement: boolean;
|
instanceUserManagement: boolean;
|
||||||
|
externalKms: boolean;
|
||||||
};
|
};
|
||||||
|
@ -225,18 +225,20 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const createWorkspace = ({
|
export const createWorkspace = ({
|
||||||
projectName
|
projectName,
|
||||||
|
kmsKeyId
|
||||||
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
|
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
|
||||||
return apiRequest.post("/api/v2/workspace", { projectName });
|
return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateWorkspace = () => {
|
export const useCreateWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
|
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
|
||||||
mutationFn: async ({ projectName }) =>
|
mutationFn: async ({ projectName, kmsKeyId }) =>
|
||||||
createWorkspace({
|
createWorkspace({
|
||||||
projectName
|
projectName,
|
||||||
|
kmsKeyId
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||||
|
@ -48,6 +48,7 @@ export type TGetUpgradeProjectStatusDTO = {
|
|||||||
// mutation dto
|
// mutation dto
|
||||||
export type CreateWorkspaceDTO = {
|
export type CreateWorkspaceDTO = {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
|
kmsKeyId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
||||||
|
@ -36,6 +36,10 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
||||||
import {
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -66,11 +70,13 @@ import {
|
|||||||
useAddUserToWsNonE2EE,
|
useAddUserToWsNonE2EE,
|
||||||
useCreateWorkspace,
|
useCreateWorkspace,
|
||||||
useGetAccessRequestsCount,
|
useGetAccessRequestsCount,
|
||||||
|
useGetExternalKmsList,
|
||||||
useGetOrgTrialUrl,
|
useGetOrgTrialUrl,
|
||||||
useGetSecretApprovalRequestCount,
|
useGetSecretApprovalRequestCount,
|
||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useSelectOrganization
|
useSelectOrganization
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||||
import { Workspace } from "@app/hooks/api/types";
|
import { Workspace } from "@app/hooks/api/types";
|
||||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||||
@ -113,7 +119,8 @@ const formSchema = yup.object({
|
|||||||
.label("Project Name")
|
.label("Project Name")
|
||||||
.trim()
|
.trim()
|
||||||
.max(64, "Too long, maximum length is 64 characters"),
|
.max(64, "Too long, maximum length is 64 characters"),
|
||||||
addMembers: yup.bool().required().label("Add Members")
|
addMembers: yup.bool().required().label("Add Members"),
|
||||||
|
kmsKeyId: yup.string().label("KMS Key ID")
|
||||||
});
|
});
|
||||||
|
|
||||||
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
||||||
@ -147,6 +154,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
|
|
||||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
||||||
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
|
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
|
||||||
|
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
|
||||||
|
|
||||||
const pendingRequestsCount = useMemo(() => {
|
const pendingRequestsCount = useMemo(() => {
|
||||||
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||||
@ -172,7 +180,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
reset,
|
reset,
|
||||||
handleSubmit
|
handleSubmit
|
||||||
} = useForm<TAddProjectFormData>({
|
} = useForm<TAddProjectFormData>({
|
||||||
resolver: yupResolver(formSchema)
|
resolver: yupResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
kmsKeyId: INTERNAL_KMS_KEY_ID
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -245,7 +256,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
putUserInOrg();
|
putUserInOrg();
|
||||||
}, [router.query.id]);
|
}, [router.query.id]);
|
||||||
|
|
||||||
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
const onCreateProject = async ({ name, addMembers, kmsKeyId }: TAddProjectFormData) => {
|
||||||
// type check
|
// type check
|
||||||
if (!currentOrg) return;
|
if (!currentOrg) return;
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@ -255,7 +266,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
project: { id: newProjectId }
|
project: { id: newProjectId }
|
||||||
}
|
}
|
||||||
} = await createWs.mutateAsync({
|
} = await createWs.mutateAsync({
|
||||||
projectName: name
|
projectName: name,
|
||||||
|
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
if (addMembers) {
|
if (addMembers) {
|
||||||
@ -888,24 +900,67 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-7 flex items-center">
|
<div className="mt-14 flex">
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem
|
||||||
|
value="advance-settings"
|
||||||
|
className="data-[state=open]:border-none"
|
||||||
|
>
|
||||||
|
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||||
|
<div className="order-1 ml-3">Advanced Settings</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<Controller
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="KMS"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
className="mb-12 w-full bg-mineshaft-600"
|
||||||
|
>
|
||||||
|
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
|
||||||
|
Default Infisical KMS
|
||||||
|
</SelectItem>
|
||||||
|
{externalKmsList?.map((kms) => (
|
||||||
|
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
||||||
|
{kms.slug}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="kmsKeyId"
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
<div className="absolute right-0 bottom-0 mr-6 mb-6 flex items-start justify-end">
|
||||||
|
<Button
|
||||||
|
key="layout-cancel-create-project"
|
||||||
|
onClick={() => handlePopUpClose("addNewWs")}
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
className="py-2"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
key="layout-create-project-submit"
|
key="layout-create-project-submit"
|
||||||
className="mr-4"
|
className="ml-4"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create Project
|
Create Project
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
</div>
|
||||||
key="layout-cancel-create-project"
|
|
||||||
onClick={() => handlePopUpClose("addNewWs")}
|
|
||||||
variant="plain"
|
|
||||||
colorSchema="secondary"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
@ -36,6 +36,10 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||||
import {
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -43,6 +47,8 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
UpgradePlanModal
|
UpgradePlanModal
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
@ -59,8 +65,10 @@ import {
|
|||||||
fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
useAddUserToWsNonE2EE,
|
useAddUserToWsNonE2EE,
|
||||||
useCreateWorkspace,
|
useCreateWorkspace,
|
||||||
|
useGetExternalKmsList,
|
||||||
useRegisterUserAction
|
useRegisterUserAction
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||||
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
import { Workspace } from "@app/hooks/api/types";
|
import { Workspace } from "@app/hooks/api/types";
|
||||||
@ -473,7 +481,8 @@ const formSchema = yup.object({
|
|||||||
.label("Project Name")
|
.label("Project Name")
|
||||||
.trim()
|
.trim()
|
||||||
.max(64, "Too long, maximum length is 64 characters"),
|
.max(64, "Too long, maximum length is 64 characters"),
|
||||||
addMembers: yup.bool().required().label("Add Members")
|
addMembers: yup.bool().required().label("Add Members"),
|
||||||
|
kmsKeyId: yup.string().label("KMS Key ID")
|
||||||
});
|
});
|
||||||
|
|
||||||
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
||||||
@ -506,7 +515,10 @@ const OrganizationPage = withPermission(
|
|||||||
reset,
|
reset,
|
||||||
handleSubmit
|
handleSubmit
|
||||||
} = useForm<TAddProjectFormData>({
|
} = useForm<TAddProjectFormData>({
|
||||||
resolver: yupResolver(formSchema)
|
resolver: yupResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
kmsKeyId: INTERNAL_KMS_KEY_ID
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
|
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
|
||||||
@ -521,7 +533,9 @@ const OrganizationPage = withPermission(
|
|||||||
(localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID
|
(localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID
|
||||||
);
|
);
|
||||||
|
|
||||||
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
|
||||||
|
|
||||||
|
const onCreateProject = async ({ name, addMembers, kmsKeyId }: TAddProjectFormData) => {
|
||||||
// type check
|
// type check
|
||||||
if (!currentOrg) return;
|
if (!currentOrg) return;
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@ -531,7 +545,8 @@ const OrganizationPage = withPermission(
|
|||||||
project: { id: newProjectId }
|
project: { id: newProjectId }
|
||||||
}
|
}
|
||||||
} = await createWs.mutateAsync({
|
} = await createWs.mutateAsync({
|
||||||
projectName: name
|
projectName: name,
|
||||||
|
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
if (addMembers) {
|
if (addMembers) {
|
||||||
@ -1063,24 +1078,64 @@ const OrganizationPage = withPermission(
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-7 flex items-center">
|
<div className="mt-14 flex">
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="advance-settings" className="data-[state=open]:border-none">
|
||||||
|
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||||
|
<div className="order-1 ml-3">Advanced Settings</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<Controller
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="KMS"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
className="mb-12 w-full bg-mineshaft-600"
|
||||||
|
>
|
||||||
|
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
|
||||||
|
Default Infisical KMS
|
||||||
|
</SelectItem>
|
||||||
|
{externalKmsList?.map((kms) => (
|
||||||
|
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
||||||
|
{kms.slug}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="kmsKeyId"
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
<div className="absolute right-0 bottom-0 mr-6 mb-6 flex items-start justify-end">
|
||||||
|
<Button
|
||||||
|
key="layout-cancel-create-project"
|
||||||
|
onClick={() => handlePopUpClose("addNewWs")}
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
className="py-2"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
key="layout-create-project-submit"
|
key="layout-create-project-submit"
|
||||||
className="mr-4"
|
className="ml-4"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create Project
|
Create Project
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
</div>
|
||||||
key="layout-cancel-create-project"
|
|
||||||
onClick={() => handlePopUpClose("addNewWs")}
|
|
||||||
variant="plain"
|
|
||||||
colorSchema="secondary"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faAws } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
|
||||||
|
|
||||||
|
import { AwsKmsForm } from "./AwsKmsForm";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
onToggle: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum WizardSteps {
|
||||||
|
SelectProvider = "select-provider",
|
||||||
|
ProviderInputs = "provider-inputs"
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXTERNAL_KMS_LIST = [
|
||||||
|
{
|
||||||
|
icon: faAws,
|
||||||
|
provider: ExternalKmsProvider.AWS,
|
||||||
|
title: "AWS KMS"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AddExternalKmsForm = ({ isOpen, onToggle }: Props) => {
|
||||||
|
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectProvider);
|
||||||
|
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleFormReset = (state: boolean = false) => {
|
||||||
|
onToggle(state);
|
||||||
|
setWizardStep(WizardSteps.SelectProvider);
|
||||||
|
setSelectedProvider(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
|
||||||
|
<ModalContent
|
||||||
|
title="Add a Key Management System"
|
||||||
|
subTitle="Configure an external key management system (KMS)"
|
||||||
|
className="my-4"
|
||||||
|
>
|
||||||
|
<AnimatePresence exitBeforeEnter>
|
||||||
|
{wizardStep === WizardSteps.SelectProvider && (
|
||||||
|
<motion.div
|
||||||
|
key="select-type-step"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<div className="mb-4 text-mineshaft-300">Select a KMS Provider</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{EXTERNAL_KMS_LIST.map(({ icon, provider, title }) => (
|
||||||
|
<div
|
||||||
|
key={`kms-${provider}`}
|
||||||
|
className="flex h-28 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedProvider(provider);
|
||||||
|
setWizardStep(WizardSteps.ProviderInputs);
|
||||||
|
}}
|
||||||
|
onKeyDown={(evt) => {
|
||||||
|
if (evt.key === "Enter") {
|
||||||
|
setSelectedProvider(provider);
|
||||||
|
setWizardStep(WizardSteps.ProviderInputs);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={icon} size="lg" />
|
||||||
|
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
{wizardStep === WizardSteps.ProviderInputs &&
|
||||||
|
selectedProvider === ExternalKmsProvider.AWS && (
|
||||||
|
<motion.div
|
||||||
|
key="kms-aws"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<AwsKmsForm onCancel={() => onToggle(false)} onCompleted={() => onToggle(false)} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,273 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import { useAddExternalKms, useUpdateExternalKms } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
AddExternalKmsSchema,
|
||||||
|
AddExternalKmsType,
|
||||||
|
ExternalKmsProvider,
|
||||||
|
Kms,
|
||||||
|
KmsAwsCredentialType
|
||||||
|
} from "@app/hooks/api/kms/types";
|
||||||
|
|
||||||
|
const AWS_REGIONS = [
|
||||||
|
{ name: "US East (Ohio)", slug: "us-east-2" },
|
||||||
|
{ name: "US East (N. Virginia)", slug: "us-east-1" },
|
||||||
|
{ name: "US West (N. California)", slug: "us-west-1" },
|
||||||
|
{ name: "US West (Oregon)", slug: "us-west-2" },
|
||||||
|
{ name: "Africa (Cape Town)", slug: "af-south-1" },
|
||||||
|
{ name: "Asia Pacific (Hong Kong)", slug: "ap-east-1" },
|
||||||
|
{ name: "Asia Pacific (Hyderabad)", slug: "ap-south-2" },
|
||||||
|
{ name: "Asia Pacific (Jakarta)", slug: "ap-southeast-3" },
|
||||||
|
{ name: "Asia Pacific (Melbourne)", slug: "ap-southeast-4" },
|
||||||
|
{ name: "Asia Pacific (Mumbai)", slug: "ap-south-1" },
|
||||||
|
{ name: "Asia Pacific (Osaka)", slug: "ap-northeast-3" },
|
||||||
|
{ name: "Asia Pacific (Seoul)", slug: "ap-northeast-2" },
|
||||||
|
{ name: "Asia Pacific (Singapore)", slug: "ap-southeast-1" },
|
||||||
|
{ name: "Asia Pacific (Sydney)", slug: "ap-southeast-2" },
|
||||||
|
{ name: "Asia Pacific (Tokyo)", slug: "ap-northeast-1" },
|
||||||
|
{ name: "Canada (Central)", slug: "ca-central-1" },
|
||||||
|
{ name: "Europe (Frankfurt)", slug: "eu-central-1" },
|
||||||
|
{ name: "Europe (Ireland)", slug: "eu-west-1" },
|
||||||
|
{ name: "Europe (London)", slug: "eu-west-2" },
|
||||||
|
{ name: "Europe (Milan)", slug: "eu-south-1" },
|
||||||
|
{ name: "Europe (Paris)", slug: "eu-west-3" },
|
||||||
|
{ name: "Europe (Spain)", slug: "eu-south-2" },
|
||||||
|
{ name: "Europe (Stockholm)", slug: "eu-north-1" },
|
||||||
|
{ name: "Europe (Zurich)", slug: "eu-central-2" },
|
||||||
|
{ name: "Middle East (Bahrain)", slug: "me-south-1" },
|
||||||
|
{ name: "Middle East (UAE)", slug: "me-central-1" },
|
||||||
|
{ name: "South America (Sao Paulo)", slug: "sa-east-1" },
|
||||||
|
{ name: "AWS GovCloud (US-East)", slug: "us-gov-east-1" },
|
||||||
|
{ name: "AWS GovCloud (US-West)", slug: "us-gov-west-1" }
|
||||||
|
];
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onCompleted: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
kms?: Kms;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<AddExternalKmsType>({
|
||||||
|
resolver: zodResolver(AddExternalKmsSchema),
|
||||||
|
defaultValues: {
|
||||||
|
slug: kms?.slug,
|
||||||
|
description: kms?.description,
|
||||||
|
provider: {
|
||||||
|
type: ExternalKmsProvider.AWS,
|
||||||
|
inputs: {
|
||||||
|
credential: {
|
||||||
|
type: kms?.external?.providerInput?.credential?.type,
|
||||||
|
data: {
|
||||||
|
accessKey: kms?.external?.providerInput?.credential?.data?.accessKey,
|
||||||
|
secretKey: kms?.external?.providerInput?.credential?.data?.secretKey,
|
||||||
|
assumeRoleArn: kms?.external?.providerInput?.credential?.data?.assumeRoleArn,
|
||||||
|
externalId: kms?.external?.providerInput?.credential?.data?.externalId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
awsRegion: kms?.external?.providerInput?.awsRegion,
|
||||||
|
kmsKeyId: kms?.external?.providerInput?.kmsKeyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { mutateAsync: addAwsExternalKms } = useAddExternalKms(currentOrg?.id!);
|
||||||
|
const { mutateAsync: updateAwsExternalKms } = useUpdateExternalKms(currentOrg?.id!);
|
||||||
|
|
||||||
|
const selectedAwsAuthType = watch("provider.inputs.credential.type");
|
||||||
|
|
||||||
|
const handleAddAwsKms = async (data: AddExternalKmsType) => {
|
||||||
|
const { slug, description, provider } = data;
|
||||||
|
try {
|
||||||
|
if (kms) {
|
||||||
|
await updateAwsExternalKms({
|
||||||
|
kmsId: kms.id,
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
provider
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated AWS External KMS",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await addAwsExternalKms({
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
provider
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully added AWS External KMS",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onCompleted();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleAddAwsKms)} autoComplete="off">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="slug"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Alias" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Description" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.inputs.credential.type"
|
||||||
|
defaultValue={KmsAwsCredentialType.AssumeRole}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Authentication Mode"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setValue("provider.inputs.credential.data.accessKey", "");
|
||||||
|
setValue("provider.inputs.credential.data.secretKey", "");
|
||||||
|
setValue("provider.inputs.credential.data.assumeRoleArn", "");
|
||||||
|
setValue("provider.inputs.credential.data.externalId", "");
|
||||||
|
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value={KmsAwsCredentialType.AssumeRole}>AWS Assume Role</SelectItem>
|
||||||
|
<SelectItem value={KmsAwsCredentialType.AccessKey}>Access Key</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectedAwsAuthType === KmsAwsCredentialType.AccessKey ? (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.inputs.credential.data.accessKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Key ID"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.inputs.credential.data.secretKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Access Key"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input type="password" autoComplete="new-password" placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.inputs.credential.data.assumeRoleArn"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="IAM Role ARN For Role Assumption"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.inputs.credential.data.externalId"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Assume Role External ID"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.inputs.awsRegion"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="AWS Region" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{AWS_REGIONS.map((awsRegion) => (
|
||||||
|
<SelectItem value={awsRegion.slug} key={`kms-aws-region-${awsRegion.slug}`}>
|
||||||
|
{awsRegion.name} ({awsRegion.slug})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.inputs.kmsKeyId"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="AWS KMS Key ID" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-6 flex items-center space-x-4">
|
||||||
|
<Button type="submit" isLoading={isSubmitting}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,218 @@
|
|||||||
|
import { faAws } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { faEllipsis, faLock, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
THead,
|
||||||
|
Tr,
|
||||||
|
UpgradePlanModal
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects,
|
||||||
|
useOrganization,
|
||||||
|
useSubscription
|
||||||
|
} from "@app/context";
|
||||||
|
import { withPermission } from "@app/hoc";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useGetExternalKmsList, useRemoveExternalKms } from "@app/hooks/api";
|
||||||
|
import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
|
||||||
|
|
||||||
|
import { AddExternalKmsForm } from "./AddExternalKmsForm";
|
||||||
|
import { UpdateExternalKmsForm } from "./UpdateExternalKmsForm";
|
||||||
|
|
||||||
|
export const OrgEncryptionTab = withPermission(
|
||||||
|
() => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
const orgId = currentOrg?.id || "";
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
|
"upgradePlan",
|
||||||
|
"addExternalKms",
|
||||||
|
"editExternalKms",
|
||||||
|
"removeExternalKms"
|
||||||
|
] as const);
|
||||||
|
const { data: externalKmsList, isLoading: isExternalKmsListLoading } =
|
||||||
|
useGetExternalKmsList(orgId);
|
||||||
|
|
||||||
|
const { mutateAsync: removeExternalKms } = useRemoveExternalKms(currentOrg?.id!);
|
||||||
|
|
||||||
|
const handleRemoveExternalKms = async () => {
|
||||||
|
const { kmsId } = popUp?.removeExternalKms?.data as {
|
||||||
|
kmsId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeExternalKms(kmsId);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully deleted external KMS",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpToggle("removeExternalKms", false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Key Management System (KMS)</p>
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Kms}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription && !subscription?.externalKms) {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handlePopUpOpen("addExternalKms");
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
<p className="mb-4 text-gray-400">
|
||||||
|
Integrate with external KMS for encrypting your organization's data
|
||||||
|
</p>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Td>Provider</Td>
|
||||||
|
<Td>Alias</Td>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isExternalKmsListLoading && <TableSkeleton columns={2} innerKey="kms-loading" />}
|
||||||
|
{!isExternalKmsListLoading && externalKmsList && externalKmsList?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={5}>
|
||||||
|
<EmptyState title="No external KMS found" icon={faLock} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
{!isExternalKmsListLoading &&
|
||||||
|
externalKmsList?.map((kms) => (
|
||||||
|
<Tr key={kms.id}>
|
||||||
|
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
|
||||||
|
{kms.externalKms.provider === ExternalKmsProvider.AWS && (
|
||||||
|
<FontAwesomeIcon icon={faAws} />
|
||||||
|
)}
|
||||||
|
<div className="ml-2">{kms.externalKms.provider.toUpperCase()}</div>
|
||||||
|
</Td>
|
||||||
|
<Td>{kms.slug}</Td>
|
||||||
|
<Td>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
an={OrgPermissionSubjects.Kms}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={!isAllowed}
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (subscription && !subscription?.externalKms) {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("editExternalKms", {
|
||||||
|
kmsId: kms.id
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
an={OrgPermissionSubjects.Kms}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={!isAllowed}
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpOpen("removeExternalKms", {
|
||||||
|
slug: kms.slug,
|
||||||
|
kmsId: kms.id,
|
||||||
|
provider: kms.externalKms.provider
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text="You can configure external KMS if you switch to Infisical's Enterprise plan."
|
||||||
|
/>
|
||||||
|
<AddExternalKmsForm
|
||||||
|
isOpen={popUp.addExternalKms.isOpen}
|
||||||
|
onToggle={(state) => handlePopUpToggle("addExternalKms", state)}
|
||||||
|
/>
|
||||||
|
<UpdateExternalKmsForm
|
||||||
|
isOpen={popUp.editExternalKms.isOpen}
|
||||||
|
kmsId={(popUp.editExternalKms.data as { kmsId: string })?.kmsId}
|
||||||
|
onOpenChange={(state) => handlePopUpToggle("editExternalKms", state)}
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeExternalKms.isOpen}
|
||||||
|
title={`Are you sure want to remove ${
|
||||||
|
(popUp?.removeExternalKms?.data as { slug: string })?.slug || ""
|
||||||
|
} from ${(popUp?.removeExternalKms?.data as { provider: string })?.provider || ""}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeExternalKms", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={handleRemoveExternalKms}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Kms }
|
||||||
|
);
|
@ -0,0 +1,29 @@
|
|||||||
|
import { ContentLoader, Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { useGetExternalKmsById } from "@app/hooks/api";
|
||||||
|
import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
|
||||||
|
|
||||||
|
import { AwsKmsForm } from "./AwsKmsForm";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
kmsId: string;
|
||||||
|
onOpenChange: (state: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UpdateExternalKmsForm = ({ isOpen, kmsId, onOpenChange }: Props) => {
|
||||||
|
const { data: externalKms, isLoading } = useGetExternalKmsById(kmsId);
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent title="Edit configuration">
|
||||||
|
{isLoading && <ContentLoader />}
|
||||||
|
{externalKms?.external?.provider === ExternalKmsProvider.AWS && (
|
||||||
|
<AwsKmsForm
|
||||||
|
kms={externalKms}
|
||||||
|
onCancel={() => onOpenChange(false)}
|
||||||
|
onCompleted={() => onOpenChange(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export { OrgEncryptionTab } from "./OrgEncryptionTab";
|
@ -3,11 +3,13 @@ import { Tab } from "@headlessui/react";
|
|||||||
|
|
||||||
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
||||||
import { OrgAuthTab } from "../OrgAuthTab";
|
import { OrgAuthTab } from "../OrgAuthTab";
|
||||||
|
import { OrgEncryptionTab } from "../OrgEncryptionTab";
|
||||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: "General", key: "tab-org-general" },
|
{ name: "General", key: "tab-org-general" },
|
||||||
{ name: "Security", key: "tab-org-security" },
|
{ name: "Security", key: "tab-org-security" },
|
||||||
|
{ name: "Encryption", key: "tab-org-encryption" },
|
||||||
{ name: "Audit Log Streams", key: "tag-audit-log-streams" }
|
{ name: "Audit Log Streams", key: "tag-audit-log-streams" }
|
||||||
];
|
];
|
||||||
export const OrgTabGroup = () => {
|
export const OrgTabGroup = () => {
|
||||||
@ -19,7 +21,8 @@ export const OrgTabGroup = () => {
|
|||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
|
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
|
||||||
|
selected ? "border-b border-white text-white" : "text-mineshaft-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.name}
|
{tab.name}
|
||||||
@ -35,6 +38,9 @@ export const OrgTabGroup = () => {
|
|||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<OrgAuthTab />
|
<OrgAuthTab />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>
|
||||||
|
<OrgEncryptionTab />
|
||||||
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<AuditLogStreamsTab />
|
<AuditLogStreamsTab />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
@ -2,12 +2,14 @@ import { Fragment } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
|
|
||||||
|
import { EncryptionTab } from "./components/EncryptionTab";
|
||||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||||
import { WebhooksTab } from "./components/WebhooksTab";
|
import { WebhooksTab } from "./components/WebhooksTab";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: "General", key: "tab-project-general" },
|
{ name: "General", key: "tab-project-general" },
|
||||||
{ name: "Webhooks", key: "tab-project-webhooks" },
|
{ name: "Encryption", key: "tab-project-encryption" },
|
||||||
|
{ name: "Webhooks", key: "tab-project-webhooks" }
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ProjectSettingsPage = () => {
|
export const ProjectSettingsPage = () => {
|
||||||
@ -25,7 +27,8 @@ export const ProjectSettingsPage = () => {
|
|||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
|
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
|
||||||
|
selected ? "border-b border-white text-white" : "text-mineshaft-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.name}
|
{tab.name}
|
||||||
@ -38,6 +41,9 @@ export const ProjectSettingsPage = () => {
|
|||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<ProjectGeneralTab />
|
<ProjectGeneralTab />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>
|
||||||
|
<EncryptionTab />
|
||||||
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<WebhooksTab />
|
<WebhooksTab />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
@ -0,0 +1,339 @@
|
|||||||
|
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import FileSaver from "file-saver";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useOrganization,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useGetActiveProjectKms,
|
||||||
|
useGetExternalKmsList,
|
||||||
|
useLoadProjectKmsBackup,
|
||||||
|
useUpdateProjectKms
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { fetchProjectKmsBackup } from "@app/hooks/api/kms/queries";
|
||||||
|
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||||
|
import { Organization, Workspace } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
kmsKeyId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const BackupConfirmationModal = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
org,
|
||||||
|
workspace
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (state: boolean) => void;
|
||||||
|
org?: Organization;
|
||||||
|
workspace?: Workspace;
|
||||||
|
}) => {
|
||||||
|
const downloadKmsBackup = async () => {
|
||||||
|
if (!workspace || !org) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { secretManager } = await fetchProjectKmsBackup(workspace.id);
|
||||||
|
|
||||||
|
const [, , kmsFunction] = secretManager.split(".");
|
||||||
|
const file = secretManager;
|
||||||
|
|
||||||
|
const blob = new Blob([file], { type: "text/plain;charset=utf-8" });
|
||||||
|
FileSaver.saveAs(blob, `kms-backup-${org.slug}-${workspace.slug}-${kmsFunction}.infisical.txt`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent title="Create KMS backup">
|
||||||
|
<p className="mb-6 text-bunker-300">
|
||||||
|
In case of interruptions with your configured external KMS, you can load a backup to set
|
||||||
|
the project's KMS back to the default Infisical KMS.
|
||||||
|
</p>
|
||||||
|
<Button onClick={downloadKmsBackup}>Generate</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
className="ml-4"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LoadBackupModal = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
org,
|
||||||
|
workspace
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (state: boolean) => void;
|
||||||
|
org?: Organization;
|
||||||
|
workspace?: Workspace;
|
||||||
|
}) => {
|
||||||
|
const fileUploadRef = useRef<HTMLInputElement>(null);
|
||||||
|
const { mutateAsync: loadKmsBackup, isLoading } = useLoadProjectKmsBackup(workspace?.id!);
|
||||||
|
const [backupContent, setBackupContent] = useState("");
|
||||||
|
const [backupFileName, setBackupFileName] = useState("");
|
||||||
|
|
||||||
|
const uploadKmsBackup = async () => {
|
||||||
|
if (!workspace || !org) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadKmsBackup(backupContent);
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully loaded KMS backup",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseFile = (file?: File) => {
|
||||||
|
if (!file) {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to parse uploaded file.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
if (!event?.target?.result) return;
|
||||||
|
const data = event.target.result.toString();
|
||||||
|
setBackupContent(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
reader.readAsText(file);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
parseFile(e.target?.files?.[0]);
|
||||||
|
setBackupFileName(e.target?.files?.[0]?.name || "");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onOpenChange={(state: boolean) => {
|
||||||
|
setBackupContent("");
|
||||||
|
setBackupFileName("");
|
||||||
|
onOpenChange(state);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title="Load KMS backup">
|
||||||
|
<p className="mb-6 text-bunker-300">
|
||||||
|
By loading a backup, the project's KMS will be switched to the default Infisical KMS.
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<input
|
||||||
|
id="fileSelect"
|
||||||
|
className="hidden"
|
||||||
|
type="file"
|
||||||
|
accept=".txt"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
ref={fileUploadRef}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
className="p-10"
|
||||||
|
ariaLabel="upload-backup"
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
fileUploadRef?.current?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faUpload} size="3x" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
{backupFileName && (
|
||||||
|
<div className="mt-2 flex justify-center px-4 text-center">{backupFileName}</div>
|
||||||
|
)}
|
||||||
|
{backupContent && (
|
||||||
|
<Button
|
||||||
|
onClick={uploadKmsBackup}
|
||||||
|
className="mt-10 w-fit"
|
||||||
|
disabled={isLoading}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EncryptionTab = () => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
|
||||||
|
const { data: activeKms } = useGetActiveProjectKms(currentWorkspace?.id!);
|
||||||
|
|
||||||
|
const { mutateAsync: updateProjectKms } = useUpdateProjectKms(currentWorkspace?.id!);
|
||||||
|
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([
|
||||||
|
"createBackupConfirmation",
|
||||||
|
"loadBackup"
|
||||||
|
] as const);
|
||||||
|
const [kmsKeyId, setKmsKeyId] = useState("");
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeKms) {
|
||||||
|
setKmsKeyId(activeKms.isExternal ? activeKms.id : INTERNAL_KMS_KEY_ID);
|
||||||
|
} else {
|
||||||
|
setKmsKeyId(INTERNAL_KMS_KEY_ID);
|
||||||
|
}
|
||||||
|
}, [activeKms]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (kmsKeyId) {
|
||||||
|
setValue("kmsKeyId", kmsKeyId);
|
||||||
|
}
|
||||||
|
}, [kmsKeyId]);
|
||||||
|
|
||||||
|
const onUpdateProjectKms = async (data: TForm) => {
|
||||||
|
try {
|
||||||
|
await updateProjectKms({
|
||||||
|
secretManagerKmsKeyId: data.kmsKeyId
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated project KMS",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onUpdateProjectKms)}
|
||||||
|
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Key Management</h2>
|
||||||
|
{kmsKeyId !== INTERNAL_KMS_KEY_ID && (
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button colorSchema="secondary" onClick={() => handlePopUpOpen("loadBackup")}>
|
||||||
|
Load Backup
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("createBackupConfirmation");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create Backup
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mb-4 text-gray-400">
|
||||||
|
Select which Key Management System to use for encrypting your project data
|
||||||
|
</p>
|
||||||
|
<div className="mb-6 max-w-md">
|
||||||
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Kms}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
className="w-3/4 bg-mineshaft-600"
|
||||||
|
>
|
||||||
|
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
|
||||||
|
Default Infisical KMS
|
||||||
|
</SelectItem>
|
||||||
|
{externalKmsList?.map((kms) => (
|
||||||
|
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
||||||
|
{kms.slug}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="kmsKeyId"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
isDisabled={!isAllowed || isSubmitting || !isDirty}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<BackupConfirmationModal
|
||||||
|
isOpen={popUp.createBackupConfirmation.isOpen}
|
||||||
|
onOpenChange={(state: boolean) => handlePopUpToggle("createBackupConfirmation", state)}
|
||||||
|
org={currentOrg}
|
||||||
|
workspace={currentWorkspace}
|
||||||
|
/>
|
||||||
|
<LoadBackupModal
|
||||||
|
isOpen={popUp.loadBackup.isOpen}
|
||||||
|
onOpenChange={(state: boolean) => handlePopUpToggle("loadBackup", state)}
|
||||||
|
org={currentOrg}
|
||||||
|
workspace={currentWorkspace}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export { EncryptionTab } from "./EncryptionTab";
|