1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-23 03:03:05 +00:00

Compare commits

..

34 Commits

Author SHA1 Message Date
9f73c77624 doc: initial docs for kms 2024-07-22 17:25:50 +08:00
5b7afea3f5 misc: made kms hook generic 2024-07-22 14:17:07 +08:00
fe9318cf8d misc: renamed project method 2024-07-20 02:56:04 +08:00
c5a9f36a0c misc: removed kms from service 2024-07-20 02:52:34 +08:00
5bd6a193f4 misc: created abstraction for get kms by id 2024-07-20 02:33:16 +08:00
9ac17718b3 misc: modified design of advanced settings 2024-07-20 02:07:35 +08:00
b9f35d16a5 misc: finalized project backup prompts 2024-07-20 01:25:00 +08:00
5e9929a9d5 misc: added empty metadata 2024-07-20 01:23:54 +08:00
c2870dffcd misc: added ability for users to select KMS during project creation 2024-07-20 00:47:48 +08:00
e9ee38fb54 misc: modified modal text 2024-07-19 19:45:17 +08:00
9d88caf66b misc: addressed type issue with audit log 2024-07-19 19:43:07 +08:00
7ef4b68503 feat: load project kms backup 2024-07-19 19:37:25 +08:00
d2456b5bd8 misc: added UI for load backup 2024-07-19 17:09:14 +08:00
1b64cdf09c misc: added audit logs for kms backup and other minor edits 2024-07-19 02:05:53 +08:00
73a00df439 misc: developed create kms backup feature 2024-07-19 01:31:13 +08:00
9f87689a8f misc: made project key and data key creation concurrency safe 2024-07-18 22:44:32 +08:00
5d6bbdfd24 misc: made org key and data key concurrency safe 2024-07-18 22:06:07 +08:00
f1b5e6104c misc: finalized switching of project KMS 2024-07-18 20:50:31 +08:00
0f7e055981 misc: partial project kms switch 2024-07-18 03:00:43 +08:00
2045305127 Merge branch 'secret-engine-v2-bridge' into feat/integrate-external-kms 2024-07-18 00:22:18 +08:00
6b6f8f5523 Merge pull request from Infisical/feat/add-project-data-key
feat: added project data key
2024-07-18 00:20:36 +08:00
9860d15d33 Merge branch 'feat/add-project-data-key' into feat/integrate-external-kms 2024-07-17 23:42:06 +08:00
166de417f1 feat: added project data key 2024-07-17 23:19:32 +08:00
65f416378a misc: changed order of aws validate connection and creation 2024-07-17 15:11:13 +08:00
de0b179b0c misc: added audit logs for external kms 2024-07-17 13:39:47 +08:00
8b0c62fbdb misc: added license checks for external kms management 2024-07-17 13:04:26 +08:00
0d512f041f misc: migrated to dedicated org permissions for kms management 2024-07-17 12:43:55 +08:00
eb03fa4d4e misc: minor UI updates 2024-07-17 00:40:54 +08:00
0a7a9b6c37 feat: finalized kms settings in org-level 2024-07-16 21:29:20 +08:00
a1bfbdf32e misc: modified encryption/decryption of external kms config 2024-07-16 15:52:29 +08:00
a07983ddc8 Merge remote-tracking branch 'akhilmhdh/feat/aws-kms-sm' into feat/integrate-external-kms 2024-07-16 14:19:51 +08:00
b9d5330db6 Merge remote-tracking branch 'akhilmhdh/feat/aws-kms-sm' into feat/integrate-external-kms 2024-07-16 14:19:18 +08:00
538ca972e6 misc: connected aws add kms 2024-07-16 14:17:54 +08:00
9cce604ca8 feat: added initial aws form 2024-07-16 02:21:14 +08:00
94 changed files with 2897 additions and 1999 deletions
backend/src
docs
frontend/src
components/v2/DeleteActionModal
context
OrgPermissionContext
ProjectPermissionContext
hooks/api
layouts/AppLayout
pages/org/[id]
memberships/[membershipId]
overview
views
Org
Project/MembersPage/components/MembersTab/components
Settings

@ -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(),
isReserved: z.boolean().default(true).nullable().optional(),
orgId: z.string().uuid(),
slug: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
slug: z.string()
updatedAt: z.date()
});
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;

@ -5,6 +5,8 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const OrganizationsSchema = z.object({
@ -16,7 +18,8 @@ export const OrganizationsSchema = z.object({
updatedAt: z.date(),
authEnforced: 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>;

@ -5,6 +5,8 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const ProjectsSchema = z.object({
@ -20,7 +22,8 @@ export const ProjectsSchema = z.object({
pitVersionLimit: z.number().default(10),
kmsCertificateKeyId: z.string().uuid().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>;

@ -1,6 +1,7 @@
import { z } from "zod";
import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
ExternalKmsAwsSchema,
ExternalKmsInputSchema,
@ -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({
external: ExternalKmsSchema.pick({
id: true,
@ -39,7 +57,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
},
schema: {
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(),
provider: ExternalKmsInputSchema
}),
@ -60,6 +78,21 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
provider: req.body.provider,
description: req.body.description
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.CREATE_KMS,
metadata: {
kmsId: externalKms.id,
provider: req.body.provider.type,
slug: req.body.slug,
description: req.body.description
}
}
});
return { externalKms };
}
});
@ -97,6 +130,21 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
description: req.body.description,
id: req.params.id
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.UPDATE_KMS,
metadata: {
kmsId: externalKms.id,
provider: req.body.provider.type,
slug: req.body.slug,
description: req.body.description
}
}
});
return { externalKms };
}
});
@ -126,6 +174,19 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
id: req.params.id
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_KMS,
metadata: {
kmsId: externalKms.id,
slug: externalKms.slug
}
}
});
return { externalKms };
}
});
@ -155,10 +216,48 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
id: req.params.id
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_KMS,
metadata: {
kmsId: externalKms.id,
slug: externalKms.slug
}
}
});
return { externalKms };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
externalKmsList: sanitizedExternalSchemaForGetAll
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const externalKmsList = await server.services.externalKms.list({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return { externalKmsList };
}
});
server.route({
method: "GET",
url: "/slug/:slug",

@ -4,6 +4,7 @@ import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerExternalKmsRouter } from "./external-kms-router";
import { registerGroupRouter } from "./group-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerLdapRouter } from "./ldap-router";
@ -87,4 +88,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
},
{ prefix: "/additional-privilege" }
);
await server.register(registerExternalKmsRouter, {
prefix: "/external-kms"
});
};

@ -4,7 +4,7 @@ import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
import { readLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -171,4 +171,178 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT]),
handler: async () => ({ actors: [] })
});
server.route({
method: "GET",
url: "/:workspaceId/kms",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
secretManagerKmsKey: z.object({
id: z.string(),
slug: z.string(),
isExternal: z.boolean()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const kmsKeys = await server.services.project.getProjectKmsKeys({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
return kmsKeys;
}
});
server.route({
method: "PATCH",
url: "/:workspaceId/kms",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
secretManagerKmsKeyId: z.string()
}),
response: {
200: z.object({
secretManagerKmsKey: z.object({
id: z.string(),
slug: z.string(),
isExternal: z.boolean()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretManagerKmsKey } = await server.services.project.updateProjectKmsKey({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.UPDATE_PROJECT_KMS,
metadata: {
secretManagerKmsKey: {
id: secretManagerKmsKey.id,
slug: secretManagerKmsKey.slug
}
}
}
});
return {
secretManagerKmsKey
};
}
});
server.route({
method: "GET",
url: "/:workspaceId/kms/backup",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
secretManager: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const backup = await server.services.project.getProjectKmsBackup({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.GET_PROJECT_KMS_BACKUP,
metadata: {}
}
});
return backup;
}
});
server.route({
method: "POST",
url: "/:workspaceId/kms/backup",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
backup: z.string().min(1)
}),
response: {
200: z.object({
secretManagerKmsKey: z.object({
id: z.string(),
slug: z.string(),
isExternal: z.boolean()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const backup = await server.services.project.loadProjectKmsBackup({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
backup: req.body.backup
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.LOAD_PROJECT_KMS_BACKUP,
metadata: {}
}
});
return backup;
}
});
};

@ -350,12 +350,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
members: z.array(z.any()).length(0),
meta: z.object({
resourceType: z.string().trim()
})
@ -428,7 +423,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
value: z.string(), // infisical orgMembershipId
display: z.string()
})
)

@ -138,7 +138,14 @@ export enum EventType {
GET_CERT = "get-cert",
DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert",
GET_CERT_BODY = "get-cert-body"
GET_CERT_BODY = "get-cert-body",
CREATE_KMS = "create-kms",
UPDATE_KMS = "update-kms",
DELETE_KMS = "delete-kms",
GET_KMS = "get-kms",
UPDATE_PROJECT_KMS = "update-project-kms",
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup"
}
interface UserActorMetadata {
@ -1164,6 +1171,62 @@ interface GetCertBody {
};
}
interface CreateKmsEvent {
type: EventType.CREATE_KMS;
metadata: {
kmsId: string;
provider: string;
slug: string;
description?: string;
};
}
interface DeleteKmsEvent {
type: EventType.DELETE_KMS;
metadata: {
kmsId: string;
slug: string;
};
}
interface UpdateKmsEvent {
type: EventType.UPDATE_KMS;
metadata: {
kmsId: string;
provider: string;
slug?: string;
description?: string;
};
}
interface GetKmsEvent {
type: EventType.GET_KMS;
metadata: {
kmsId: string;
slug: string;
};
}
interface UpdateProjectKmsEvent {
type: EventType.UPDATE_PROJECT_KMS;
metadata: {
secretManagerKmsKey: {
id: string;
slug: string;
};
};
}
interface GetProjectKmsBackupEvent {
type: EventType.GET_PROJECT_KMS_BACKUP;
metadata: Record<string, string>; // no metadata yet
}
interface LoadProjectKmsBackupEvent {
type: EventType.LOAD_PROJECT_KMS_BACKUP;
metadata: Record<string, string>; // no metadata yet
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -1264,4 +1327,11 @@ export type Event =
| GetCert
| DeleteCert
| RevokeCert
| GetCertBody;
| GetCertBody
| CreateKmsEvent
| UpdateKmsEvent
| DeleteKmsEvent
| GetKmsEvent
| UpdateProjectKmsEvent
| GetProjectKmsBackupEvent
| LoadProjectKmsBackupEvent;

@ -72,7 +72,7 @@ export const certificateAuthorityCrlServiceFactory = ({
kmsId: keyId
});
const decryptedCrl = kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl);
const base64crl = crl.toString("base64");

@ -31,6 +31,8 @@ export const externalKmsDALFactory = (db: TDbClient) => {
isReserved: el.isReserved,
orgId: el.orgId,
slug: el.slug,
createdAt: el.createdAt,
updatedAt: el.updatedAt,
externalKms: {
id: el.externalKmsId,
provider: el.externalKmsProvider,

@ -6,6 +6,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TExternalKmsDALFactory } from "./external-kms-dal";
@ -22,9 +23,13 @@ import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model";
type TExternalKmsServiceFactoryDep = {
externalKmsDAL: TExternalKmsDALFactory;
kmsService: Pick<TKmsServiceFactory, "getOrgKmsKeyId" | "encryptWithKmsKey" | "decryptWithKmsKey">;
kmsService: Pick<
TKmsServiceFactory,
"getOrgKmsKeyId" | "decryptWithInputKey" | "encryptWithInputKey" | "getOrgKmsDataKey"
>;
kmsDAL: Pick<TKmsKeyDALFactory, "create" | "updateById" | "findById" | "deleteById" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TExternalKmsServiceFactory = ReturnType<typeof externalKmsServiceFactory>;
@ -32,6 +37,7 @@ export type TExternalKmsServiceFactory = ReturnType<typeof externalKmsServiceFac
export const externalKmsServiceFactory = ({
externalKmsDAL,
permissionService,
licenseService,
kmsService,
kmsDAL
}: TExternalKmsServiceFactoryDep) => {
@ -51,7 +57,15 @@ export const externalKmsServiceFactory = ({
actorAuthMethod,
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());
let sanitizedProviderInput = "";
@ -59,20 +73,22 @@ export const externalKmsServiceFactory = ({
case KmsProviders.Aws:
{
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
const newProviderInput = await externalKms.generateInputKmsKey();
sanitizedProviderInput = JSON.stringify(newProviderInput);
await externalKms.validateConnection();
}
break;
default:
throw new BadRequestError({ message: "external kms provided is invalid" });
}
const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId);
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: orgKmsKeyId
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(actorOrgId);
const kmsEncryptor = await kmsService.encryptWithInputKey({
key: orgKmsDataKey
});
const { cipherTextBlob: encryptedProviderInputs } = kmsEncryptor({
plainText: Buffer.from(sanitizedProviderInput, "utf8")
});
@ -119,18 +135,27 @@ export const externalKmsServiceFactory = ({
actorAuthMethod,
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 externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId);
let sanitizedProviderInput = "";
if (provider) {
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: orgDefaultKmsId
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId);
const kmsDecryptor = await kmsService.decryptWithInputKey({
key: orgKmsDataKey
});
const decryptedProviderInputBlob = kmsDecryptor({
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
});
@ -154,8 +179,9 @@ export const externalKmsServiceFactory = ({
let encryptedProviderInputs: Buffer | undefined;
if (sanitizedProviderInput) {
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: orgDefaultKmsId
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(actorOrgId);
const kmsEncryptor = await kmsService.encryptWithInputKey({
key: orgKmsDataKey
});
const { cipherTextBlob } = kmsEncryptor({
plainText: Buffer.from(sanitizedProviderInput, "utf8")
@ -197,7 +223,7 @@ export const externalKmsServiceFactory = ({
actorAuthMethod,
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 });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
@ -218,7 +244,7 @@ export const externalKmsServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId });
@ -234,15 +260,17 @@ export const externalKmsServiceFactory = ({
actorAuthMethod,
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 });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId);
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: orgDefaultKmsId
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId);
const kmsDecryptor = await kmsService.decryptWithInputKey({
key: orgKmsDataKey
});
const decryptedProviderInputBlob = kmsDecryptor({
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
});
@ -273,15 +301,16 @@ export const externalKmsServiceFactory = ({
actorAuthMethod,
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 });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId);
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: orgDefaultKmsId
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId);
const kmsDecryptor = await kmsService.decryptWithInputKey({
key: orgKmsDataKey
});
const decryptedProviderInputBlob = kmsDecryptor({
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
});

@ -50,17 +50,26 @@ type TAwsKmsProviderFactoryReturn = TExternalKmsProviderFns & {
};
export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Promise<TAwsKmsProviderFactoryReturn> => {
const providerInputs = await ExternalKmsAwsSchema.parseAsync(inputs);
const awsClient = await getAwsKmsClient(providerInputs);
let providerInputs = await ExternalKmsAwsSchema.parseAsync(inputs);
let awsClient = await getAwsKmsClient(providerInputs);
const generateInputKmsKey = async () => {
if (providerInputs.kmsKeyId) return providerInputs;
const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] });
const kmsKey = await awsClient.send(command);
if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key");
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 () => {

@ -162,50 +162,17 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
}
};
const findGroupMembershipsByUserIdInOrg = async (userId: string, orgId: string) => {
const findUserGroupMembershipsInOrg = async (userId: string, orgId: string) => {
try {
const docs = await db
.replicaNode()(TableName.UserGroupMembership)
.join(TableName.Groups, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`)
.join(TableName.OrgMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.OrgMembership}.userId`)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.where(`${TableName.UserGroupMembership}.userId`, userId)
.where(`${TableName.Groups}.orgId`, orgId)
.select(
db.ref("id").withSchema(TableName.UserGroupMembership),
db.ref("groupId").withSchema(TableName.UserGroupMembership),
db.ref("name").withSchema(TableName.Groups).as("groupName"),
db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"),
db.ref("firstName").withSchema(TableName.Users).as("firstName"),
db.ref("lastName").withSchema(TableName.Users).as("lastName")
);
.where(`${TableName.Groups}.orgId`, orgId);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "Find group memberships by user id in org" });
}
};
const findGroupMembershipsByGroupIdInOrg = async (groupId: string, orgId: string) => {
try {
const docs = await db
.replicaNode()(TableName.UserGroupMembership)
.join(TableName.Groups, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`)
.join(TableName.OrgMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.OrgMembership}.userId`)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.where(`${TableName.Groups}.id`, groupId)
.where(`${TableName.Groups}.orgId`, orgId)
.select(
db.ref("id").withSchema(TableName.UserGroupMembership),
db.ref("groupId").withSchema(TableName.UserGroupMembership),
db.ref("name").withSchema(TableName.Groups).as("groupName"),
db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"),
db.ref("firstName").withSchema(TableName.Users).as("firstName"),
db.ref("lastName").withSchema(TableName.Users).as("lastName")
);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "Find group memberships by group id in org" });
throw new DatabaseError({ error, name: "findTest" });
}
};
@ -215,7 +182,6 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
findUserGroupMembershipsInProject,
findGroupMembersNotInProject,
deletePendingUserGroupMembershipsByUserIds,
findGroupMembershipsByUserIdInOrg,
findGroupMembershipsByGroupIdInOrg
findUserGroupMembershipsInOrg
};
};

@ -39,7 +39,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretApproval: false,
secretRotation: true,
caCrl: false,
instanceUserManagement: false
instanceUserManagement: false,
externalKms: false
});
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

@ -57,6 +57,7 @@ export type TFeatureSet = {
secretRotation: true;
caCrl: false;
instanceUserManagement: false;
externalKms: false;
};
export type TOrgPlansTableDTO = {

@ -21,7 +21,8 @@ export enum OrgPermissionSubjects {
Groups = "groups",
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity"
Identity = "identity",
Kms = "kms"
}
export type OrgPermissionSet =
@ -37,7 +38,8 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
@ -100,6 +102,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
return build({ conditionsMatcher });
};

@ -28,7 +28,8 @@ export enum ProjectPermissionSub {
SecretRotation = "secret-rotation",
Identity = "identity",
CertificateAuthorities = "certificate-authorities",
Certificates = "certificates"
Certificates = "certificates",
Kms = "kms"
}
type SubjectFields = {
@ -60,7 +61,8 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
@ -157,6 +159,8 @@ const buildAdminPermissionRules = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
return rules;
};

@ -9,7 +9,6 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types";
import { AuthTokenType } from "@app/services/auth/auth-type";
@ -52,7 +51,6 @@ import {
TListScimUsers,
TListScimUsersDTO,
TReplaceScimUserDTO,
TScimGroup,
TScimTokenJwtPayload,
TUpdateScimGroupNamePatchDTO,
TUpdateScimGroupNamePutDTO,
@ -85,8 +83,7 @@ type TScimServiceFactoryDep = {
| "insertMany"
| "filterProjectsByUserMembership"
| "delete"
| "findGroupMembershipsByUserIdInOrg"
| "findGroupMembershipsByGroupIdInOrg"
| "findUserGroupMembershipsInOrg"
>;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
@ -255,10 +252,7 @@ export const scimServiceFactory = ({
status: 403
});
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
membership.userId,
orgId
);
const groupMembershipsInOrg = await userGroupMembershipDAL.findUserGroupMembershipsInOrg(membership.userId, orgId);
return buildScimUser({
orgMembershipId: membership.id,
@ -269,7 +263,7 @@ export const scimServiceFactory = ({
active: membership.isActive,
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
display: group.groupName
display: group.name
}))
});
};
@ -515,10 +509,7 @@ export const scimServiceFactory = ({
isActive: active
});
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
membership.userId,
orgId
);
const groupMembershipsInOrg = await userGroupMembershipDAL.findUserGroupMembershipsInOrg(membership.userId, orgId);
return buildScimUser({
orgMembershipId: membership.id,
@ -529,7 +520,7 @@ export const scimServiceFactory = ({
active,
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
display: group.groupName
display: group.name
}))
});
};
@ -598,20 +589,13 @@ export const scimServiceFactory = ({
}
);
const scimGroups: TScimGroup[] = [];
for await (const group of groups) {
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
const scimGroup = buildScimGroup({
const scimGroups = groups.map((group) =>
buildScimGroup({
groupId: group.id,
name: group.name,
members: members.map((member) => ({
value: member.orgMembershipId,
display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
}))
});
scimGroups.push(scimGroup);
}
members: [] // does this need to be populated?
})
);
return buildScimGroupList({
scimGroups,
@ -888,27 +872,23 @@ export const scimServiceFactory = ({
break;
}
case "add": {
try {
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: operation.value.map((member) => member.value)
}
});
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: operation.value.map((member) => member.value)
}
});
await addUsersToGroupByUserIds({
group,
userIds: orgMemberships.map((membership) => membership.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL
});
} catch {
logger.info("Repeat SCIM user-group add operation");
}
await addUsersToGroupByUserIds({
group,
userIds: orgMemberships.map((membership) => membership.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL
});
break;
}
@ -936,15 +916,10 @@ export const scimServiceFactory = ({
}
}
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
return buildScimGroup({
groupId: group.id,
name: group.name,
members: members.map((member) => ({
value: member.orgMembershipId,
display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
}))
members: []
});
};

@ -6,7 +6,15 @@ export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
// all the key prefixes used must be set here to avoid conflict
export enum KeyStorePrefixes {
SecretReplication = "secret-replication-import-lock"
SecretReplication = "secret-replication-import-lock",
KmsProjectDataKeyCreation = "kms-project-data-key-creation-lock",
KmsProjectKeyCreation = "kms-project-key-creation-lock",
WaitUntilReadyKmsProjectDataKeyCreation = "wait-until-ready-kms-project-data-key-creation-",
WaitUntilReadyKmsProjectKeyCreation = "wait-until-ready-kms-project-key-creation-",
KmsOrgKeyCreation = "kms-org-key-creation-lock",
KmsOrgDataKeyCreation = "kms-org-data-key-creation-lock",
WaitUntilReadyKmsOrgKeyCreation = "wait-until-ready-kms-org-key-creation-",
WaitUntilReadyKmsOrgDataKeyCreation = "wait-until-ready-kms-org-data-key-creation-"
}
type TWaitTillReady = {

@ -348,15 +348,10 @@ export const ORGANIZATIONS = {
LIST_USER_MEMBERSHIPS: {
organizationId: "The ID of the organization to get memberships from."
},
GET_USER_MEMBERSHIP: {
organizationId: "The ID of the organization to get the membership for.",
membershipId: "The ID of the membership to get."
},
UPDATE_USER_MEMBERSHIP: {
organizationId: "The ID of the organization to update the membership for.",
membershipId: "The ID of the membership to update.",
role: "The new role of the membership.",
isActive: "The active status of the membership"
role: "The new role of the membership."
},
DELETE_USER_MEMBERSHIP: {
organizationId: "The ID of the organization to delete the membership from.",

@ -116,6 +116,8 @@ export const decryptAsymmetric = ({ ciphertext, nonce, publicKey, privateKey }:
export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64");
export const generateHash = (value: string) => crypto.createHash("sha256").update(value).digest("hex");
export const generateAsymmetricKeyPair = () => {
const pair = nacl.box.keyPair();

@ -316,7 +316,8 @@ export const registerRoutes = async (
kmsDAL,
kmsService,
permissionService,
externalKmsDAL
externalKmsDAL,
licenseService
});
const trustedIpService = trustedIpServiceFactory({
@ -457,7 +458,6 @@ export const registerRoutes = async (
tokenService,
projectDAL,
projectMembershipDAL,
orgMembershipDAL,
projectKeyDAL,
smtpService,
userDAL,
@ -625,7 +625,8 @@ export const registerRoutes = async (
certificateDAL,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL,
keyStore
keyStore,
kmsService
});
const projectEnvService = projectEnvServiceFactory({

@ -78,7 +78,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
project: ProjectsSchema.pick({ name: true, id: true }),
roles: z.array(
z.object({
id: z.string(),

@ -1,13 +1,6 @@
import { z } from "zod";
import {
OrganizationsSchema,
OrgMembershipsSchema,
ProjectMembershipsSchema,
ProjectsSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { OrganizationsSchema, OrgMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { ORGANIZATIONS } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -37,7 +30,6 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
user: UsersSchema.pick({
username: true,
email: true,
isEmailVerified: true,
firstName: true,
lastName: true,
id: true
@ -111,54 +103,6 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/:organizationId/memberships/:membershipId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Get organization user membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
organizationId: z.string().trim().describe(ORGANIZATIONS.GET_USER_MEMBERSHIP.organizationId),
membershipId: z.string().trim().describe(ORGANIZATIONS.GET_USER_MEMBERSHIP.membershipId)
}),
response: {
200: z.object({
membership: OrgMembershipsSchema.merge(
z.object({
user: UsersSchema.pick({
username: true,
email: true,
isEmailVerified: true,
firstName: true,
lastName: true,
id: true
}).merge(z.object({ publicKey: z.string().nullable() }))
})
).omit({ createdAt: true, updatedAt: true })
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const membership = await server.services.org.getOrgMembership({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId,
membershipId: req.params.membershipId
});
return { membership };
}
});
server.route({
method: "PATCH",
url: "/:organizationId/memberships/:membershipId",
@ -177,8 +121,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
membershipId: z.string().trim().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.membershipId)
}),
body: z.object({
role: z.string().trim().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role),
isActive: z.boolean().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.isActive)
role: z.string().trim().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role)
}),
response: {
200: z.object({
@ -186,17 +129,17 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
if (req.auth.actor !== ActorType.USER) return;
const membership = await server.services.org.updateOrgMembership({
userId: req.permission.id,
role: req.body.role,
actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId,
membershipId: req.params.membershipId,
actorOrgId: req.permission.orgId,
...req.body
actorOrgId: req.permission.orgId
});
return { membership };
}
@ -240,69 +183,6 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
// TODO: re-think endpoint structure in future so users only need to pass in membershipId bc organizationId is redundant
method: "GET",
url: "/:organizationId/memberships/:membershipId/project-memberships",
config: {
rateLimit: writeLimit
},
schema: {
description: "Get project memberships given organization membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
organizationId: z.string().trim().describe(ORGANIZATIONS.DELETE_USER_MEMBERSHIP.organizationId),
membershipId: z.string().trim().describe(ORGANIZATIONS.DELETE_USER_MEMBERSHIP.membershipId)
}),
response: {
200: z.object({
memberships: ProjectMembershipsSchema.extend({
user: UsersSchema.pick({
email: true,
username: true,
firstName: true,
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
project: ProjectsSchema.pick({ name: true, id: true }),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
)
})
.omit({ createdAt: true, updatedAt: true })
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const memberships = await server.services.org.listProjectMembershipsByOrgMembershipId({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId,
orgMembershipId: req.params.membershipId
});
return { memberships };
}
});
server.route({
method: "POST",
url: "/",

@ -161,7 +161,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
message: "Slug must be a valid slug"
})
.optional()
.describe(PROJECTS.CREATE.slug)
.describe(PROJECTS.CREATE.slug),
kmsKeyId: z.string().optional()
}),
response: {
200: z.object({
@ -177,7 +178,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
workspaceName: req.body.projectName,
slug: req.body.slug
slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId
});
await server.services.telemetry.sendPostHogEvents({

@ -78,7 +78,7 @@ export const getCaCredentials = async ({
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId
});
const decryptedPrivateKey = kmsDecryptor({
const decryptedPrivateKey = await kmsDecryptor({
cipherTextBlob: caSecret.encryptedPrivateKey
});
@ -129,13 +129,13 @@ export const getCaCertChain = async ({
kmsId: keyId
});
const decryptedCaCert = kmsDecryptor({
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const decryptedChain = kmsDecryptor({
const decryptedChain = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificateChain
});
@ -176,7 +176,7 @@ export const rebuildCaCrl = async ({
kmsId: keyId
});
const privateKey = kmsDecryptor({
const privateKey = await kmsDecryptor({
cipherTextBlob: caSecret.encryptedPrivateKey
});
@ -210,7 +210,7 @@ export const rebuildCaCrl = async ({
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: keyId
});
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({
const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(crl.rawData))
});

@ -91,7 +91,7 @@ export const certificateAuthorityQueueFactory = ({
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId
});
const privateKey = kmsDecryptor({
const privateKey = await kmsDecryptor({
cipherTextBlob: caSecret.encryptedPrivateKey
});
@ -125,7 +125,7 @@ export const certificateAuthorityQueueFactory = ({
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: keyId
});
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({
const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
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))
});
const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.alloc(0)
});
@ -209,7 +209,7 @@ export const certificateAuthorityServiceFactory = ({
signingKey: keys.privateKey
});
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({
const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(crl.rawData))
});
@ -224,7 +224,7 @@ export const certificateAuthorityServiceFactory = ({
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
const skObj = KeyObject.from(keys.privateKey);
const { cipherTextBlob: encryptedPrivateKey } = kmsEncryptor({
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: skObj.export({
type: "pkcs8",
format: "der"
@ -458,7 +458,7 @@ export const certificateAuthorityServiceFactory = ({
});
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
const decryptedCaCert = kmsDecryptor({
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
@ -615,11 +615,11 @@ export const certificateAuthorityServiceFactory = ({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(certObj.rawData))
});
const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChain)
});
@ -693,7 +693,7 @@ export const certificateAuthorityServiceFactory = ({
kmsId: certificateManagerKmsId
});
const decryptedCaCert = kmsDecryptor({
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
@ -803,7 +803,7 @@ export const certificateAuthorityServiceFactory = ({
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
});

@ -173,7 +173,7 @@ export const certificateServiceFactory = ({
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKeyId
});
const decryptedCert = kmsDecryptor({
const decryptedCert = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificate
});

@ -14,6 +14,7 @@ export const kmskeyDALFactory = (db: TDbClient) => {
try {
const result = await (tx || db.replicaNode())(TableName.KmsKey)
.where({ [`${TableName.KmsKey}.id` as "id"]: id })
.join(TableName.Organization, `${TableName.KmsKey}.orgId`, `${TableName.Organization}.id`)
.leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`)
.leftJoin(TableName.ExternalKms, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`)
.first()
@ -31,11 +32,19 @@ export const kmskeyDALFactory = (db: TDbClient) => {
db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"),
db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"),
db.ref("statusDetails").withSchema(TableName.ExternalKms).as("externalKmsStatusDetails")
)
.select(
db.ref("kmsDefaultKeyId").withSchema(TableName.Organization).as("orgKmsDefaultKeyId"),
db.ref("kmsEncryptedDataKey").withSchema(TableName.Organization).as("orgKmsEncryptedDataKey")
);
const data = {
...KmsKeysSchema.parse(result),
isExternal: Boolean(result?.externalKmsId),
orgKms: {
id: result?.orgKmsDefaultKeyId,
encryptedDataKey: result?.orgKmsEncryptedDataKey
},
externalKms: result?.externalKmsId
? {
id: result.externalKmsId,

@ -1,11 +1,18 @@
import slugify from "@sindresorhus/slugify";
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 { randomSecureBytes } from "@app/lib/crypto";
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
import { BadRequestError } from "@app/lib/errors";
import { generateHash } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
@ -33,6 +40,7 @@ type TKmsServiceFactoryDep = {
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
export const INTERNAL_KMS_KEY_ID = "internal";
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
@ -83,22 +91,6 @@ export const kmsServiceFactory = ({
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">) => {
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
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 cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
@ -135,67 +114,568 @@ export const kmsServiceFactory = ({
};
const getOrgKmsKeyId = async (orgId: string) => {
const keyId = await orgDAL.transaction(async (tx) => {
const org = await orgDAL.findById(orgId, tx);
if (!org) {
throw new BadRequestError({ message: "Org not found" });
let org = await orgDAL.findById(orgId);
if (!org) {
throw new NotFoundError({ message: "Org not found" });
}
if (!org.kmsDefaultKeyId) {
const lock = await keyStore
.acquireLock([KeyStorePrefixes.KmsOrgKeyCreation, orgId], 3000, { retryCount: 3 })
.catch(() => null);
try {
if (!lock) {
await keyStore.waitTillReady({
key: `${KeyStorePrefixes.WaitUntilReadyKmsOrgKeyCreation}${orgId}`,
keyCheckCb: (val) => val === "true",
waitingCb: () => logger.info("KMS. Waiting for org key to be created")
});
org = await orgDAL.findById(orgId);
} else {
const keyId = await orgDAL.transaction(async (tx) => {
org = await orgDAL.findById(orgId, tx);
if (org.kmsDefaultKeyId) {
return org.kmsDefaultKeyId;
}
const key = await generateKmsKey({
isReserved: true,
orgId: org.id,
tx
});
await orgDAL.updateById(
org.id,
{
kmsDefaultKeyId: key.id
},
tx
);
await keyStore.setItemWithExpiry(`${KeyStorePrefixes.WaitUntilReadyKmsOrgKeyCreation}${orgId}`, 10, "true");
return key.id;
});
return keyId;
}
} finally {
await lock?.release();
}
}
if (!org.kmsDefaultKeyId) {
throw new Error("Invalid organization KMS");
}
return org.kmsDefaultKeyId;
};
const decryptWithKmsKey = async ({ kmsId }: Omit<TDecryptWithKmsDTO, "cipherTextBlob">) => {
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
if (!kmsDoc) {
throw new NotFoundError({ message: "KMS ID not found" });
}
if (kmsDoc.externalKms) {
let externalKms: TExternalKmsProviderFns;
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
throw new Error("Invalid organization KMS");
}
if (!org.kmsDefaultKeyId) {
// create default kms key for certificate service
const key = await generateKmsKey({
isReserved: true,
orgId: org.id,
tx
});
const orgKmsDecryptor = await decryptWithKmsKey({
kmsId: kmsDoc.orgKms.id
});
await orgDAL.updateById(
org.id,
{
kmsDefaultKeyId: key.id
},
tx
);
const orgKmsDataKey = await orgKmsDecryptor({
cipherTextBlob: kmsDoc.orgKms.encryptedDataKey
});
return key.id;
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 org.kmsDefaultKeyId;
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 keyId;
return kmsDecryptor({
cipherTextBlob: org.kmsEncryptedDataKey
});
};
const getProjectSecretManagerKmsKeyId = async (projectId: string) => {
const keyId = await projectDAL.transaction(async (tx) => {
const project = await projectDAL.findById(projectId, tx);
let project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({ message: "Project not found" });
}
if (!project.kmsSecretManagerKeyId) {
const lock = await keyStore
.acquireLock([KeyStorePrefixes.KmsProjectKeyCreation, projectId], 3000, { retryCount: 3 })
.catch(() => null);
try {
if (!lock) {
await keyStore.waitTillReady({
key: `${KeyStorePrefixes.WaitUntilReadyKmsProjectKeyCreation}${projectId}`,
keyCheckCb: (val) => val === "true",
waitingCb: () => logger.info("KMS. Waiting for project key to be created")
});
project = await projectDAL.findById(projectId);
} else {
const kmsKeyId = await projectDAL.transaction(async (tx) => {
project = await projectDAL.findById(projectId, tx);
if (project.kmsSecretManagerKeyId) {
return project.kmsSecretManagerKeyId;
}
const key = await generateKmsKey({
isReserved: true,
orgId: project.orgId,
tx
});
await projectDAL.updateById(
projectId,
{
kmsSecretManagerKeyId: key.id
},
tx
);
return key.id;
});
await keyStore.setItemWithExpiry(
`${KeyStorePrefixes.WaitUntilReadyKmsProjectKeyCreation}${projectId}`,
10,
"true"
);
return kmsKeyId;
}
} finally {
await lock?.release();
}
}
if (!project.kmsSecretManagerKeyId) {
throw new Error("Missing project KMS key ID");
}
return project.kmsSecretManagerKeyId;
};
const getProjectSecretManagerKmsKey = async (projectId: string) => {
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
const kmsKey = await kmsDAL.findByIdWithAssociatedKms(kmsKeyId);
return kmsKey;
};
const getProjectSecretManagerKmsDataKey = async (projectId: string) => {
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
let project = await projectDAL.findById(projectId);
if (!project.kmsSecretManagerEncryptedDataKey) {
const lock = await keyStore
.acquireLock([KeyStorePrefixes.KmsProjectDataKeyCreation, projectId], 3000, { retryCount: 3 })
.catch(() => null);
try {
if (!lock) {
await keyStore.waitTillReady({
key: `${KeyStorePrefixes.WaitUntilReadyKmsProjectDataKeyCreation}${projectId}`,
keyCheckCb: (val) => val === "true",
waitingCb: () => logger.info("KMS. Waiting for project data key to be created")
});
project = await projectDAL.findById(projectId);
} else {
const projectDataKey = await projectDAL.transaction(async (tx) => {
project = await projectDAL.findById(projectId, tx);
if (project.kmsSecretManagerEncryptedDataKey) {
return;
}
const dataKey = randomSecureBytes();
const kmsEncryptor = await encryptWithKmsKey({
kmsId: kmsKeyId
});
const { cipherTextBlob } = await kmsEncryptor({
plainText: dataKey
});
await projectDAL.updateById(
projectId,
{
kmsSecretManagerEncryptedDataKey: cipherTextBlob
},
tx
);
await keyStore.setItemWithExpiry(
`${KeyStorePrefixes.WaitUntilReadyKmsProjectDataKeyCreation}${projectId}`,
10,
"true"
);
return dataKey;
});
if (projectDataKey) {
return projectDataKey;
}
}
} finally {
await lock?.release();
}
}
if (!project.kmsSecretManagerEncryptedDataKey) {
throw new Error("Missing project data key");
}
const kmsDecryptor = await decryptWithKmsKey({
kmsId: kmsKeyId
});
return kmsDecryptor({
cipherTextBlob: project.kmsSecretManagerEncryptedDataKey
});
};
const updateProjectSecretManagerKmsKey = async (projectId: string, kmsId: string) => {
const currentKms = await getProjectSecretManagerKmsKey(projectId);
if ((currentKms.isReserved && kmsId === INTERNAL_KMS_KEY_ID) || currentKms.id === kmsId) {
return currentKms;
}
if (kmsId !== INTERNAL_KMS_KEY_ID) {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new BadRequestError({ message: "Project not found" });
throw new NotFoundError({
message: "Project not found."
});
}
if (!project.kmsSecretManagerKeyId) {
// create default kms key for certificate service
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
});
await projectDAL.updateById(
projectId,
{
kmsSecretManagerKeyId: key.id
},
tx
);
return key.id;
newKmsId = key.id;
}
return project.kmsSecretManagerKeyId;
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")
});
return keyId;
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 () => {
@ -251,6 +731,13 @@ export const kmsServiceFactory = ({
decryptWithKmsKey,
decryptWithInputKey,
getOrgKmsKeyId,
getProjectSecretManagerKmsKeyId
getProjectSecretManagerKmsKeyId,
getOrgKmsDataKey,
getProjectSecretManagerKmsDataKey,
getProjectSecretManagerKmsKey,
updateProjectSecretManagerKmsKey,
getProjectKeyBackup,
loadProjectKeyBackup,
getKmsById
};
};

@ -1,6 +1,5 @@
import { TDbClient } from "@app/db";
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TOrgMembershipDALFactory = ReturnType<typeof orgMembershipDALFactory>;
@ -8,51 +7,7 @@ export type TOrgMembershipDALFactory = ReturnType<typeof orgMembershipDALFactory
export const orgMembershipDALFactory = (db: TDbClient) => {
const orgMembershipOrm = ormify(db, TableName.OrgMembership);
const findOrgMembershipById = async (membershipId: string) => {
try {
const member = await db
.replicaNode()(TableName.OrgMembership)
.where(`${TableName.OrgMembership}.id`, membershipId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin<TUserEncryptionKeys>(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.select(
db.ref("id").withSchema(TableName.OrgMembership),
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
db.ref("orgId").withSchema(TableName.OrgMembership),
db.ref("role").withSchema(TableName.OrgMembership),
db.ref("roleId").withSchema(TableName.OrgMembership),
db.ref("status").withSchema(TableName.OrgMembership),
db.ref("isActive").withSchema(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("isEmailVerified").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
)
.where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER
.first();
if (!member) return undefined;
const { email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data } = member;
return {
...data,
user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey }
};
} catch (error) {
throw new DatabaseError({ error, name: "Find org membership by id" });
}
};
return {
...orgMembershipOrm,
findOrgMembershipById
...orgMembershipOrm
};
};

@ -76,7 +76,6 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("status").withSchema(TableName.OrgMembership),
db.ref("isActive").withSchema(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("isEmailVerified").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
@ -85,9 +84,9 @@ export const orgDALFactory = (db: TDbClient) => {
)
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER
return members.map(({ email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data }) => ({
return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({
...data,
user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey }
user: { email, username, firstName, lastName, id: userId, publicKey }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find all org members" });

@ -15,10 +15,9 @@ import { getConfig } from "@app/lib/config/env";
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
@ -39,9 +38,7 @@ import {
TFindAllWorkspacesDTO,
TFindOrgMembersByEmailDTO,
TGetOrgGroupsDTO,
TGetOrgMembershipDTO,
TInviteUserToOrgDTO,
TListProjectMembershipsByOrgMembershipIdDTO,
TUpdateOrgDTO,
TUpdateOrgMembershipDTO,
TVerifyUserToOrgDTO
@ -57,7 +54,6 @@ type TOrgServiceFactoryDep = {
projectDAL: TProjectDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">;
incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
smtpService: TSmtpService;
@ -83,7 +79,6 @@ export const orgServiceFactory = ({
projectDAL,
projectMembershipDAL,
projectKeyDAL,
orgMembershipDAL,
tokenService,
orgBotDAL,
licenseService,
@ -369,7 +364,6 @@ export const orgServiceFactory = ({
* */
const updateOrgMembership = async ({
role,
isActive,
orgId,
userId,
membershipId,
@ -379,16 +373,8 @@ export const orgServiceFactory = ({
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
const foundMembership = await orgMembershipDAL.findOne({
id: membershipId,
orgId
});
if (!foundMembership) throw new NotFoundError({ message: "Failed to find organization membership" });
if (foundMembership.userId === userId)
throw new BadRequestError({ message: "Cannot update own organization membership" });
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
if (role && isCustomRole) {
if (isCustomRole) {
const customRole = await orgRoleDAL.findOne({ slug: role, orgId });
if (!customRole) throw new BadRequestError({ name: "Update membership", message: "Role not found" });
@ -408,7 +394,7 @@ export const orgServiceFactory = ({
return membership;
}
const [membership] = await orgDAL.updateMembership({ id: membershipId, orgId }, { role, roleId: null, isActive });
const [membership] = await orgDAL.updateMembership({ id: membershipId, orgId }, { role, roleId: null });
return membership;
};
/*
@ -599,24 +585,6 @@ export const orgServiceFactory = ({
return { token, user };
};
const getOrgMembership = async ({
membershipId,
orgId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TGetOrgMembershipDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
const membership = await orgMembershipDAL.findOrgMembershipById(membershipId);
if (!membership) throw new NotFoundError({ message: "Failed to find organization membership" });
if (membership.orgId !== orgId) throw new NotFoundError({ message: "Failed to find organization membership" });
return membership;
};
const deleteOrgMembership = async ({
orgId,
userId,
@ -640,26 +608,6 @@ export const orgServiceFactory = ({
return deletedMembership;
};
const listProjectMembershipsByOrgMembershipId = async ({
orgMembershipId,
orgId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TListProjectMembershipsByOrgMembershipIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
const membership = await orgMembershipDAL.findOrgMembershipById(orgMembershipId);
if (!membership) throw new NotFoundError({ message: "Failed to find organization membership" });
if (membership.orgId !== orgId) throw new NotFoundError({ message: "Failed to find organization membership" });
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, membership.user.id);
return projectMemberships;
};
/*
* CRUD operations of incident contacts
* */
@ -720,7 +668,6 @@ export const orgServiceFactory = ({
findOrgMembersByUsername,
createOrganization,
deleteOrganizationById,
getOrgMembership,
deleteOrgMembership,
findAllWorkspaces,
addGhostUser,
@ -729,7 +676,6 @@ export const orgServiceFactory = ({
findIncidentContacts,
createIncidentContact,
deleteIncidentContact,
getOrgGroups,
listProjectMembershipsByOrgMembershipId
getOrgGroups
};
};

@ -6,16 +6,11 @@ export type TUpdateOrgMembershipDTO = {
userId: string;
orgId: string;
membershipId: string;
role?: string;
isActive?: boolean;
role: string;
actorOrgId: string | undefined;
actorAuthMethod: ActorAuthMethod;
};
export type TGetOrgMembershipDTO = {
membershipId: string;
} & TOrgPermission;
export type TDeleteOrgMembershipDTO = {
userId: string;
orgId: string;
@ -60,7 +55,3 @@ export type TUpdateOrgDTO = {
} & TOrgPermission;
export type TGetOrgGroupsDTO = TOrgPermission;
export type TListProjectMembershipsByOrgMembershipIdDTO = {
orgMembershipId: string;
} & TOrgPermission;

@ -16,7 +16,6 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
const docs = await db
.replicaNode()(TableName.ProjectMembership)
.where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId })
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.where((qb) => {
if (filter.usernames) {
@ -59,22 +58,17 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole),
db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole),
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
db.ref("name").as("projectName").withSchema(TableName.Project)
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole)
)
.where({ isGhost: false });
const members = sqlNestRelationships({
data: docs,
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId, projectName }) => ({
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId }) => ({
id,
userId,
projectId,
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
project: {
id: projectId,
name: projectName
}
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }
}),
key: "id",
childrenMapper: [
@ -157,95 +151,14 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
const findProjectMembershipsByUserId = async (orgId: string, userId: string) => {
try {
const docs = await db
const memberships = await db
.replicaNode()(TableName.ProjectMembership)
.where({ userId })
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.where(`${TableName.Users}.id`, userId)
.where(`${TableName.Project}.orgId`, orgId)
.join<TUserEncryptionKeys>(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.join(
TableName.ProjectUserMembershipRole,
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
`${TableName.ProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.ProjectUserMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.select(
db.ref("id").withSchema(TableName.ProjectMembership),
db.ref("isGhost").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("role").withSchema(TableName.ProjectUserMembershipRole),
db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"),
db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole),
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole),
db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole),
db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole),
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("id").as("projectId").withSchema(TableName.Project)
)
.where({ isGhost: false });
.where({ [`${TableName.Project}.orgId` as "orgId"]: orgId })
.select(selectAllTableCols(TableName.ProjectMembership));
const members = sqlNestRelationships({
data: docs,
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, projectId, projectName }) => ({
id,
userId,
projectId,
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
project: {
id: projectId,
name: projectName
}
}),
key: "id",
childrenMapper: [
{
label: "roles" as const,
key: "membershipRoleId",
mapper: ({
role,
customRoleId,
customRoleName,
customRoleSlug,
membershipRoleId,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
}) => ({
id: membershipRoleId,
role,
customRoleId,
customRoleName,
customRoleSlug,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
})
}
]
});
return members;
return memberships;
} catch (error) {
throw new DatabaseError({ error, name: "Find project memberships by user id" });
}

@ -21,6 +21,7 @@ import { TCertificateAuthorityDALFactory } from "../certificate-authority/certif
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
@ -38,11 +39,14 @@ import {
TCreateProjectDTO,
TDeleteProjectDTO,
TGetProjectDTO,
TGetProjectKmsKey,
TListProjectCasDTO,
TListProjectCertsDTO,
TLoadProjectKmsBackupDTO,
TToggleProjectAutoCapitalizationDTO,
TUpdateAuditLogsRetentionDTO,
TUpdateProjectDTO,
TUpdateProjectKmsDTO,
TUpdateProjectNameDTO,
TUpdateProjectVersionLimitDTO,
TUpgradeProjectDTO
@ -76,6 +80,14 @@ type TProjectServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
kmsService: Pick<
TKmsServiceFactory,
| "updateProjectSecretManagerKmsKey"
| "getProjectKeyBackup"
| "loadProjectKeyBackup"
| "getKmsById"
| "getProjectSecretManagerKmsKeyId"
>;
};
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@ -100,7 +112,8 @@ export const projectServiceFactory = ({
identityProjectMembershipRoleDAL,
certificateAuthorityDAL,
certificateDAL,
keyStore
keyStore,
kmsService
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
@ -111,7 +124,8 @@ export const projectServiceFactory = ({
actorOrgId,
actorAuthMethod,
workspaceName,
slug: projectSlug
slug: projectSlug,
kmsKeyId
}: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId });
@ -139,16 +153,28 @@ export const projectServiceFactory = ({
const results = await projectDAL.transaction(async (tx) => {
const ghostUser = await orgService.addGhostUser(organization.id, tx);
if (kmsKeyId) {
const kms = await kmsService.getKmsById(kmsKeyId, tx);
if (kms.orgId !== organization.id) {
throw new BadRequestError({
message: "KMS does not belong in the organization"
});
}
}
const project = await projectDAL.create(
{
name: workspaceName,
orgId: organization.id,
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
version: ProjectVersion.V2,
pitVersionLimit: 10
pitVersionLimit: 10,
kmsSecretManagerKeyId: kmsKeyId
},
tx
);
// set ghost user as admin of project
const projectMembership = await projectMembershipDAL.create(
{
@ -647,6 +673,109 @@ export const projectServiceFactory = ({
};
};
const updateProjectKmsKey = async ({
projectId,
secretManagerKmsKeyId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TUpdateProjectKmsDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
const secretManagerKmsKey = await kmsService.updateProjectSecretManagerKmsKey(projectId, secretManagerKmsKeyId);
return {
secretManagerKmsKey
};
};
const getProjectKmsBackup = async ({
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.externalKms) {
throw new BadRequestError({
message: "Failed to get KMS backup due to plan restriction. Upgrade to the enterprise plan."
});
}
const kmsBackup = await kmsService.getProjectKeyBackup(projectId);
return kmsBackup;
};
const loadProjectKmsBackup = async ({
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
backup
}: TLoadProjectKmsBackupDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.externalKms) {
throw new BadRequestError({
message: "Failed to load KMS backup due to plan restriction. Upgrade to the enterprise plan."
});
}
const kmsBackup = await kmsService.loadProjectKeyBackup(projectId, backup);
return kmsBackup;
};
const getProjectKmsKeys = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TGetProjectKmsKey) => {
const { membership } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
if (!membership) {
throw new ForbiddenRequestError({
message: "User is not a member of the project"
});
}
const kmsKeyId = await kmsService.getProjectSecretManagerKmsKeyId(projectId);
const kmsKey = await kmsService.getKmsById(kmsKeyId);
return { secretManagerKmsKey: kmsKey };
};
return {
createProject,
deleteProject,
@ -660,6 +789,10 @@ export const projectServiceFactory = ({
listProjectCas,
listProjectCertificates,
updateVersionLimit,
updateAuditLogsRetention
updateAuditLogsRetention,
updateProjectKmsKey,
getProjectKmsBackup,
loadProjectKmsBackup,
getProjectKmsKeys
};
};

@ -27,6 +27,7 @@ export type TCreateProjectDTO = {
actorOrgId?: string;
workspaceName: string;
slug?: string;
kmsKeyId?: string;
};
export type TDeleteProjectBySlugDTO = {
@ -97,3 +98,13 @@ export type TListProjectCertsDTO = {
offset: number;
limit: number;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateProjectKmsDTO = {
secretManagerKmsKeyId: string;
} & TProjectPermission;
export type TLoadProjectKmsBackupDTO = {
backup: string;
} & TProjectPermission;
export type TGetProjectKmsKey = TProjectPermission;

@ -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.">
![Open encryption org settings](../../../images/platform/kms/aws/encryption-org-settings.png)
</Step>
<Step title="Click on the 'Add' button">
![Add encryption org settings](../../../images/platform/kms/aws/encryption-org-settings-add.png)
Click the 'Add' button to begin adding a new external KMS.
</Step>
<Step title="Select 'AWS KMS'">
![Select Encryption Provider](../../../images/platform/kms/aws/encryption-modal-provider-select.png)
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.
![AWS KMS key ID](../../../images/platform/kms/aws/aws-kms-key-id.png)
</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">
![Open encryption project
settings](../../../images/platform/kms/aws/encryption-project-settings.png)
</Step>
<Step title="Under the Key Management section, select your newly added AWS KMS key from the dropdown">
![Select encryption project
settings](../../../images/platform/kms/aws/encryption-project-settings-select.png)
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>

@ -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.
![Configure KMS new](../../../images/platform/kms/configure-kms-new.png)
For existing projects, you can configure the KMS from the Project Settings page.
![Configure KMS existing](../../../images/platform/kms/configure-kms-existing.png)
## 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.

Binary file not shown.

After

(image error) Size: 151 KiB

Binary file not shown.

After

(image error) Size: 348 KiB

Binary file not shown.

After

(image error) Size: 694 KiB

Binary file not shown.

After

(image error) Size: 482 KiB

Binary file not shown.

After

(image error) Size: 476 KiB

Binary file not shown.

After

(image error) Size: 479 KiB

Binary file not shown.

After

(image error) Size: 97 KiB

Binary file not shown.

After

(image error) Size: 104 KiB

@ -154,6 +154,13 @@
"documentation/platform/dynamic-secrets/aws-iam"
]
},
{
"group": "Key Management",
"pages": [
"documentation/platform/kms/overview",
"documentation/platform/kms/aws-kms"
]
},
"documentation/platform/secret-sharing"
]
},

@ -15,30 +15,15 @@ This guide walks through how you can use these paid features on a self hosted in
</Step>
<Step title="Activate the license">
Depending on whether or not the environment where Infisical is deployed has internet access, you may be issued a regular license or an offline license.
<Tabs>
<Tab title="Regular License">
- Assign the issued license key to the `LICENSE_KEY` environment variable in your Infisical instance.
- If using a regular license, you should set the value of the environment variable `LICENSE_KEY` in Infisical to the issued license key.
- If using an offline license, you should set the value of the environment variable `LICENSE_KEY_OFFLINE` in Infisical to the issued license key.
- Your Infisical instance will need to communicate with the Infisical license server to validate the license key.
If you want to limit outgoing connections only to the Infisical license server, you can use the following IP addresses: `13.248.249.247` and `35.71.190.59`
<Note>
Ensure that your firewall or network settings allow outbound connections to these IP addresses to avoid any issues with license validation.
</Note>
</Tab>
<Tab title="Offline License">
- Assign the issued license key to the `LICENSE_KEY_OFFLINE` environment variable in your Infisical instance.
<Note>
How you set the environment variable will depend on the deployment method you used. Please refer to the documentation of your deployment method for specific instructions.
</Note>
</Tab>
</Tabs>
<Note>
How you set the environment variable will depend on the deployment method you used. Please refer to the documentation of your deployment method for specific instructions.
</Note>
Once your instance starts up, the license key will be validated and youll be able to use the paid features.
However, when the license expires, Infisical will continue to run, but EE features will be disabled until the license is renewed or a new one is purchased.
</Step>
</Steps>

@ -25,7 +25,7 @@ export const DeleteActionModal = ({
deleteKey,
onDeleteApproved,
title,
subTitle = "This action is irreversible.",
subTitle = "This action is irreversible!",
buttonText = "Delete"
}: Props): JSX.Element => {
const [inputData, setInputData] = useState("");
@ -86,7 +86,7 @@ export const DeleteActionModal = ({
<FormControl
label={
<div className="break-words pb-2 text-sm">
Type <span className="font-bold">{deleteKey}</span> to perform this action
Type <span className="font-bold">{deleteKey}</span> to delete the resource
</div>
}
className="mb-0"
@ -94,7 +94,7 @@ export const DeleteActionModal = ({
<Input
value={inputData}
onChange={(e) => setInputData(e.target.value)}
placeholder="Type confirm..."
placeholder="Type to delete..."
/>
</FormControl>
</form>

@ -19,7 +19,8 @@ export enum OrgPermissionSubjects {
Groups = "groups",
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity"
Identity = "identity",
Kms = "kms"
}
export type OrgPermissionSet =
@ -35,6 +36,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
export type TOrgPermission = MongoAbility<OrgPermissionSet>;

@ -26,7 +26,8 @@ export enum ProjectPermissionSub {
SecretRotation = "secret-rotation",
Identity = "identity",
CertificateAuthorities = "certificate-authorities",
Certificates = "certificates"
Certificates = "certificates",
Kms = "kms"
}
type SubjectFields = {

@ -16,6 +16,7 @@ export * from "./incidentContacts";
export * from "./integrationAuth";
export * from "./integrations";
export * from "./keys";
export * from "./kms";
export * from "./ldapConfig";
export * from "./oidcConfig";
export * from "./organization";

@ -0,0 +1,8 @@
export {
useAddExternalKms,
useLoadProjectKmsBackup,
useRemoveExternalKms,
useUpdateExternalKms,
useUpdateProjectKms
} from "./mutations";
export { useGetActiveProjectKms, useGetExternalKmsById, useGetExternalKmsList } from "./queries";

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

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

@ -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;
caCrl: boolean;
instanceUserManagement: boolean;
externalKms: boolean;
};

@ -16,8 +16,6 @@ export {
useGetMyIp,
useGetMyOrganizationProjects,
useGetMySessions,
useGetOrgMembership,
useGetOrgMembershipProjectMemberships,
useGetOrgUsers,
useGetUser,
useGetUserAction,
@ -25,5 +23,6 @@ export {
useRegisterUserAction,
useRevokeMySessions,
useUpdateMfaEnabled,
useUpdateOrgMembership,
useUpdateUserAuthMethods} from "./queries";
useUpdateOrgUserRole,
useUpdateUserAuthMethods
} from "./queries";

@ -57,9 +57,8 @@ export const useAddUserToWsNonE2EE = () => {
});
return data;
},
onSuccess: (_, { orgId, projectId }) => {
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(projectId));
queryClient.invalidateQueries(userKeys.allOrgMembershipProjectMemberships(orgId));
}
});
};

@ -13,8 +13,7 @@ import {
OrgUser,
RenameUserDTO,
TokenVersion,
TWorkspaceUser,
UpdateOrgMembershipDTO,
UpdateOrgUserRoleDTO,
User,
UserEnc
} from "./types";
@ -24,13 +23,6 @@ export const userKeys = {
getPrivateKey: ["user"] as const,
userAction: ["user-action"] as const,
userProjectFavorites: (orgId: string) => [{ orgId }, "user-project-favorites"] as const,
getOrgMembership: (orgId: string, orgMembershipId: string) =>
[{ orgId, orgMembershipId }, "org-membership"] as const,
allOrgMembershipProjectMemberships: (orgId: string) => [orgId, "all-user-memberships"] as const,
forOrgMembershipProjectMemberships: (orgId: string, orgMembershipId: string) =>
[...userKeys.allOrgMembershipProjectMemberships(orgId), { orgMembershipId }] as const,
getOrgMembershipProjectMemberships: (orgId: string, username: string) =>
[{ orgId, username }, "org-membership-project-memberships"] as const,
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
myIp: ["ip"] as const,
myAPIKeys: ["api-keys"] as const,
@ -175,41 +167,6 @@ export const useAddUserToOrg = () => {
});
};
export const useGetOrgMembership = (organizationId: string, orgMembershipId: string) => {
return useQuery({
queryKey: userKeys.getOrgMembership(organizationId, orgMembershipId),
queryFn: async () => {
const {
data: { membership }
} = await apiRequest.get<{ membership: OrgUser }>(
`/api/v2/organizations/${organizationId}/memberships/${orgMembershipId}`
);
return membership;
},
enabled: Boolean(organizationId) && Boolean(orgMembershipId)
});
};
export const useGetOrgMembershipProjectMemberships = (
organizationId: string,
orgMembershipId: string
) => {
return useQuery({
queryKey: userKeys.forOrgMembershipProjectMemberships(organizationId, orgMembershipId),
queryFn: async () => {
const {
data: { memberships }
} = await apiRequest.get<{ memberships: TWorkspaceUser[] }>(
`/api/v2/organizations/${organizationId}/memberships/${orgMembershipId}/project-memberships`
);
return memberships;
},
enabled: Boolean(organizationId) && Boolean(orgMembershipId)
});
};
export const useDeleteOrgMembership = () => {
const queryClient = useQueryClient();
@ -223,43 +180,24 @@ export const useDeleteOrgMembership = () => {
});
};
export const useDeactivateOrgMembership = () => {
export const useUpdateOrgUserRole = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, DeletOrgMembershipDTO>({
mutationFn: ({ membershipId, orgId }) => {
return apiRequest.post(
`/api/v2/organizations/${orgId}/memberships/${membershipId}/deactivate`
);
},
onSuccess: (_, { orgId, membershipId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(orgId));
queryClient.invalidateQueries(userKeys.getOrgMembership(orgId, membershipId));
}
});
};
export const useUpdateOrgMembership = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, UpdateOrgMembershipDTO>({
mutationFn: ({ organizationId, membershipId, role, isActive }) => {
return useMutation<{}, {}, UpdateOrgUserRoleDTO>({
mutationFn: ({ organizationId, membershipId, role }) => {
return apiRequest.patch(
`/api/v2/organizations/${organizationId}/memberships/${membershipId}`,
{
role,
isActive
role
}
);
},
onSuccess: (_, { organizationId, membershipId }) => {
onSuccess: (_, { organizationId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
queryClient.invalidateQueries(userKeys.getOrgMembership(organizationId, membershipId));
},
// to remove old states
onError: (_, { organizationId, membershipId }) => {
onError: (_, { organizationId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
queryClient.invalidateQueries(userKeys.getOrgMembership(organizationId, membershipId));
}
});
};

@ -49,7 +49,6 @@ export type OrgUser = {
user: {
username: string;
email?: string;
isEmailVerified: boolean;
firstName: string;
lastName: string;
id: string;
@ -83,11 +82,6 @@ export type TWorkspaceUser = {
id: string;
publicKey: string;
};
projectId: string;
project: {
id: string;
name: string;
};
inviteEmail: string;
organization: string;
roles: (
@ -133,14 +127,12 @@ export type AddUserToWsDTOE2EE = {
export type AddUserToWsDTONonE2EE = {
projectId: string;
usernames: string[];
orgId: string;
};
export type UpdateOrgMembershipDTO = {
export type UpdateOrgUserRoleDTO = {
organizationId: string;
membershipId: string;
role?: string;
isActive?: boolean;
role: string;
};
export type DeletOrgMembershipDTO = {

@ -11,7 +11,6 @@ import { IdentityMembership } from "../identities/types";
import { IntegrationAuth } from "../integrationAuth/types";
import { TIntegration } from "../integrations/types";
import { EncryptedSecret } from "../secrets/types";
import { userKeys } from "../users/queries";
import { TWorkspaceUser } from "../users/types";
import {
CreateEnvironmentDTO,
@ -226,18 +225,20 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
});
export const createWorkspace = ({
projectName
projectName,
kmsKeyId
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
return apiRequest.post("/api/v2/workspace", { projectName });
return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId });
};
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
mutationFn: async ({ projectName }) =>
mutationFn: async ({ projectName, kmsKeyId }) =>
createWorkspace({
projectName
projectName,
kmsKeyId
}),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
@ -386,7 +387,6 @@ export const useDeleteUserFromWorkspace = () => {
}: {
workspaceId: string;
usernames: string[];
orgId: string;
}) => {
const {
data: { deletedMembership }
@ -395,9 +395,8 @@ export const useDeleteUserFromWorkspace = () => {
});
return deletedMembership;
},
onSuccess: (_, { orgId, workspaceId }) => {
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(workspaceId));
queryClient.invalidateQueries(userKeys.allOrgMembershipProjectMemberships(orgId));
}
});
};

@ -48,6 +48,7 @@ export type TGetUpgradeProjectStatusDTO = {
// mutation dto
export type CreateWorkspaceDTO = {
projectName: string;
kmsKeyId?: 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 { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
Checkbox,
DropdownMenu,
@ -66,11 +70,13 @@ import {
useAddUserToWsNonE2EE,
useCreateWorkspace,
useGetAccessRequestsCount,
useGetExternalKmsList,
useGetOrgTrialUrl,
useGetSecretApprovalRequestCount,
useLogoutUser,
useSelectOrganization
} from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { Workspace } from "@app/hooks/api/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
@ -113,7 +119,8 @@ const formSchema = yup.object({
.label("Project Name")
.trim()
.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>;
@ -147,6 +154,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
const pendingRequestsCount = useMemo(() => {
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
@ -172,7 +180,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
reset,
handleSubmit
} = useForm<TAddProjectFormData>({
resolver: yupResolver(formSchema)
resolver: yupResolver(formSchema),
defaultValues: {
kmsKeyId: INTERNAL_KMS_KEY_ID
}
});
const { t } = useTranslation();
@ -245,7 +256,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
putUserInOrg();
}, [router.query.id]);
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
const onCreateProject = async ({ name, addMembers, kmsKeyId }: TAddProjectFormData) => {
// type check
if (!currentOrg) return;
if (!user) return;
@ -255,7 +266,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
project: { id: newProjectId }
}
} = await createWs.mutateAsync({
projectName: name
projectName: name,
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined
});
if (addMembers) {
@ -264,8 +276,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
usernames: orgUsers
.map((member) => member.user.username)
.filter((username) => username !== user.username),
projectId: newProjectId,
orgId: currentOrg.id
projectId: newProjectId
});
}
@ -889,24 +900,67 @@ export const AppLayout = ({ children }: LayoutProps) => {
)}
/>
</div>
<div className="mt-7 flex items-center">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="mr-4"
type="submit"
>
Create Project
</Button>
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose("addNewWs")}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
<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
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="ml-4"
type="submit"
>
Create Project
</Button>
</div>
</div>
</form>
</ModalContent>

@ -1,20 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { UserPage } from "@app/views/Org/UserPage";
export default function User() {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<UserPage />
</>
);
}
User.requireAuth = true;

@ -36,6 +36,10 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
Checkbox,
FormControl,
@ -43,6 +47,8 @@ import {
Input,
Modal,
ModalContent,
Select,
SelectItem,
Skeleton,
UpgradePlanModal
} from "@app/components/v2";
@ -59,8 +65,10 @@ import {
fetchOrgUsers,
useAddUserToWsNonE2EE,
useCreateWorkspace,
useGetExternalKmsList,
useRegisterUserAction
} from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { Workspace } from "@app/hooks/api/types";
@ -473,7 +481,8 @@ const formSchema = yup.object({
.label("Project Name")
.trim()
.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>;
@ -506,7 +515,10 @@ const OrganizationPage = withPermission(
reset,
handleSubmit
} = useForm<TAddProjectFormData>({
resolver: yupResolver(formSchema)
resolver: yupResolver(formSchema),
defaultValues: {
kmsKeyId: INTERNAL_KMS_KEY_ID
}
});
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
@ -521,7 +533,9 @@ const OrganizationPage = withPermission(
(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
if (!currentOrg) return;
if (!user) return;
@ -531,7 +545,8 @@ const OrganizationPage = withPermission(
project: { id: newProjectId }
}
} = await createWs.mutateAsync({
projectName: name
projectName: name,
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined
});
if (addMembers) {
@ -541,8 +556,7 @@ const OrganizationPage = withPermission(
usernames: orgUsers
.map((member) => member.user.username)
.filter((username) => username !== user.username),
projectId: newProjectId,
orgId: currentOrg.id
projectId: newProjectId
});
}
@ -1064,24 +1078,64 @@ const OrganizationPage = withPermission(
)}
/>
</div>
<div className="mt-7 flex items-center">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="mr-4"
type="submit"
>
Create Project
</Button>
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose("addNewWs")}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
<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
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="ml-4"
type="submit"
>
Create Project
</Button>
</div>
</div>
</form>
</ModalContent>

@ -1,4 +1,4 @@
import { faFolder } from "@fortawesome/free-solid-svg-icons";
import { faKey } from "@fortawesome/free-solid-svg-icons";
import {
EmptyState,
@ -37,7 +37,7 @@ export const IdentityProjectsTable = ({ identityId, handlePopUpOpen }: Props) =>
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="identity-project-memberships" />}
{isLoading && <TableSkeleton columns={2} innerKey="identity-project-memberships" />}
{!isLoading &&
projectMemberships?.map((membership) => {
return (
@ -51,7 +51,7 @@ export const IdentityProjectsTable = ({ identityId, handlePopUpOpen }: Props) =>
</TBody>
</Table>
{!isLoading && !projectMemberships?.length && (
<EmptyState title="This identity has not been assigned to any projects" icon={faFolder} />
<EmptyState title="This identity has not been assigned to any projects" icon={faKey} />
)}
</TableContainer>
);

@ -16,7 +16,7 @@ import {
useOrganization,
useSubscription
} from "@app/context";
import { useDeleteOrgMembership, useUpdateOrgMembership } from "@app/hooks/api";
import { useDeleteOrgMembership } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { AddOrgMemberModal } from "./AddOrgMemberModal";
@ -32,13 +32,11 @@ export const OrgMembersSection = () => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addMember",
"removeMember",
"deactivateMember",
"upgradePlan",
"setUpEmail"
] as const);
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const isMoreUsersAllowed = subscription?.memberLimit
? subscription.membersUsed < subscription.memberLimit
@ -67,29 +65,6 @@ export const OrgMembersSection = () => {
handlePopUpOpen("addMember");
};
const onDeactivateMemberSubmit = async (orgMembershipId: string) => {
try {
await updateOrgMembership({
organizationId: orgId,
membershipId: orgMembershipId,
isActive: false
});
createNotification({
text: "Successfully deactivated user in organization",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to deactivate user in organization",
type: "error"
});
}
handlePopUpClose("deactivateMember");
};
const onRemoveMemberSubmit = async (orgMembershipId: string) => {
try {
await deleteMutateAsync({
@ -153,20 +128,6 @@ export const OrgMembersSection = () => {
)
}
/>
<DeleteActionModal
isOpen={popUp.deactivateMember.isOpen}
title={`Are you sure want to deactivate member with username ${
(popUp?.deactivateMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deactivateMember", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeactivateMemberSubmit(
(popUp?.deactivateMember?.data as { orgMembershipId: string })?.orgMembershipId
)
}
buttonText="Deactivate"
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}

@ -1,18 +1,13 @@
import { useCallback, useMemo, useState } from "react";
import { useRouter } from "next/router";
import { faEllipsis, faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons";
import { faMagnifyingGlass, faUsers, faXmark } 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,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Select,
SelectItem,
@ -37,13 +32,13 @@ import {
useFetchServerStatus,
useGetOrgRoles,
useGetOrgUsers,
useUpdateOrgMembership
useUpdateOrgUserRole
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["removeMember", "deactivateMember", "upgradePlan"]>,
popUpName: keyof UsePopUpState<["removeMember", "upgradePlan"]>,
data?: {
orgMembershipId?: string;
username?: string;
@ -54,7 +49,6 @@ type Props = {
};
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Props) => {
const router = useRouter();
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { user } = useUser();
@ -69,14 +63,14 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId);
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg();
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const { mutateAsync: updateUserOrgRole } = useUpdateOrgUserRole();
const onRoleChange = async (membershipId: string, role: string) => {
if (!currentOrg?.id) return;
try {
// TODO: replace hardcoding default role
const isCustomRole = !["admin", "member", "no-access"].includes(role);
const isCustomRole = !["admin", "member"].includes(role);
if (isCustomRole && subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan", {
@ -85,7 +79,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
return;
}
await updateOrgMembership({
await updateUserOrgRole({
organizationId: currentOrg?.id,
membershipId,
role
@ -182,11 +176,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
const email = u?.email || inviteEmail;
const username = u?.username ?? inviteEmail ?? "-";
return (
<Tr
key={`org-membership-${orgMembershipId}`}
className="h-10 w-full cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
onClick={() => router.push(`/org/${orgId}/memberships/${orgMembershipId}`)}
>
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
<Td className={isActive ? "" : "text-mineshaft-400"}>{name}</Td>
<Td className={isActive ? "" : "text-mineshaft-400"}>{username}</Td>
<Td>
@ -248,117 +238,34 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
</Td>
<Td>
{userId !== u?.id && (
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="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}
a={OrgPermissionSubjects.Member}
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<IconButton
onClick={() => {
if (currentOrg?.authEnforced) {
createNotification({
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
type: "error"
});
return;
}
handlePopUpOpen("removeMember", { orgMembershipId, username });
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
router.push(`/org/${orgId}/memberships/${orgMembershipId}`);
}}
disabled={!isAllowed}
>
Edit User
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={
isActive
? twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)
: ""
}
onClick={async (e) => {
e.stopPropagation();
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
type: "error"
});
return;
}
if (!isActive) {
// activate user
await updateOrgMembership({
organizationId: orgId,
membershipId: orgMembershipId,
isActive: true
});
return;
}
// deactivate user
handlePopUpOpen("deactivateMember", {
orgMembershipId,
username
});
}}
disabled={!isAllowed}
>
{`${isActive ? "Deactivate" : "Activate"} User`}
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
type: "error"
});
return;
}
handlePopUpOpen("removeMember", {
orgMembershipId,
username
});
}}
disabled={!isAllowed}
>
Remove User
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</OrgPermissionCan>
)}
</Td>
</Tr>

@ -1,288 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useRouter } from "next/router";
import { faChevronLeft, faEllipsis } 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,
Tooltip,
UpgradePlanModal
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useUser
} from "@app/context";
import { withPermission } from "@app/hoc";
import {
useDeleteOrgMembership,
useGetOrgMembership,
useUpdateOrgMembership
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { UserDetailsSection, UserOrgMembershipModal, UserProjectsSection } from "./components";
export const UserPage = withPermission(
() => {
const router = useRouter();
const membershipId = router.query.membershipId as string;
const { user } = useUser();
const { currentOrg } = useOrganization();
const userId = user?.id || "";
const orgId = currentOrg?.id || "";
const { data: membership } = useGetOrgMembership(orgId, membershipId);
const { mutateAsync: deleteOrgMembership } = useDeleteOrgMembership();
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"removeMember",
"orgMembership",
"deactivateMember",
"upgradePlan"
] as const);
const onDeactivateMemberSubmit = async (orgMembershipId: string) => {
try {
await updateOrgMembership({
organizationId: orgId,
membershipId: orgMembershipId,
isActive: false
});
createNotification({
text: "Successfully deactivated user in organization",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to deactivate user in organization",
type: "error"
});
}
handlePopUpClose("deactivateMember");
};
const onRemoveMemberSubmit = async (orgMembershipId: string) => {
try {
await deleteOrgMembership({
orgId,
membershipId: orgMembershipId
});
createNotification({
text: "Successfully removed user from org",
type: "success"
});
handlePopUpClose("removeMember");
router.push(`/org/${orgId}/members`);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to remove user from the organization",
type: "error"
});
}
handlePopUpClose("removeMember");
};
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{membership && (
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
router.push(`/org/${orgId}/members`);
}}
className="mb-4"
>
Users
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">
{membership.user.firstName || membership.user.lastName
? `${membership.user.firstName} ${membership.user.lastName}`
: "-"}
</p>
{userId !== membership.user.id && (
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() =>
handlePopUpOpen("orgMembership", {
membershipId: membership.id,
role: membership.role
})
}
disabled={!isAllowed}
>
Edit User
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={
membership.isActive
? twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)
: ""
}
onClick={async () => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
}
if (!membership.isActive) {
// activate user
await updateOrgMembership({
organizationId: orgId,
membershipId,
isActive: true
});
return;
}
// deactivate user
handlePopUpOpen("deactivateMember", {
orgMembershipId: membershipId,
username: membership.user.username
});
}}
disabled={!isAllowed}
>
{`${membership.isActive ? "Deactivate" : "Activate"} User`}
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
}
handlePopUpOpen("removeMember", {
orgMembershipId: membershipId,
username: membership.user.username
});
}}
disabled={!isAllowed}
>
Remove User
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<div className="flex">
<div className="mr-4 w-96">
<UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />
</div>
<UserProjectsSection membershipId={membershipId} />
</div>
</div>
)}
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
title={`Are you sure want to remove member with username ${
(popUp?.removeMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveMemberSubmit(
(popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
)
}
/>
<DeleteActionModal
isOpen={popUp.deactivateMember.isOpen}
title={`Are you sure want to deactivate member with username ${
(popUp?.deactivateMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deactivateMember", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeactivateMemberSubmit(
(popUp?.deactivateMember?.data as { orgMembershipId: string })?.orgMembershipId
)
}
buttonText="Deactivate"
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
<UserOrgMembershipModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Member }
);

@ -1,195 +0,0 @@
import {
faCheck,
faCheckCircle,
faCircleXmark,
faCopy,
faPencil} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, IconButton, Tooltip } from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useUser
} from "@app/context";
import { useTimedReset } from "@app/hooks";
import {
useAddUserToOrg,
useFetchServerStatus,
useGetOrgMembership,
useGetOrgRoles
} from "@app/hooks/api";
import { OrgUser } from "@app/hooks/api/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
membershipId: string;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["orgMembership"]>, data?: {}) => void;
};
export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) => {
const [copyTextUsername, isCopyingUsername, setCopyTextUsername] = useTimedReset<string>({
initialState: "Copy username to clipboard"
});
const { user } = useUser();
const { currentOrg } = useOrganization();
const userId = user?.id || "";
const orgId = currentOrg?.id || "";
const { data: roles } = useGetOrgRoles(orgId);
const { data: serverDetails } = useFetchServerStatus();
const { data: membership } = useGetOrgMembership(orgId, membershipId);
const { mutateAsync: inviteUser, isLoading } = useAddUserToOrg();
const onResendInvite = async (email: string) => {
try {
const { data } = await inviteUser({
organizationId: orgId,
inviteeEmail: email
});
// setCompleteInviteLink(data?.completeInviteLink || "");
if (!data.completeInviteLink) {
createNotification({
text: `Successfully resent invite to ${email}`,
type: "success"
});
}
} catch (err) {
console.error(err);
createNotification({
text: `Failed to resend invite to ${email}`,
type: "error"
});
}
};
const getStatus = (m: OrgUser) => {
if (!m.isActive) {
return "Deactivated";
}
return m.status === "invited" ? "Invited" : "Active";
};
const roleName = roles?.find((r) => r.slug === membership?.role)?.name;
return membership ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Details</h3>
{userId !== membership.user.id && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => {
return (
<Tooltip content="Edit Membership">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
handlePopUpOpen("orgMembership", {
membershipId: membership.id,
role: membership.role
});
}}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
</OrgPermissionCan>
)}
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">
{membership.user.firstName || membership.user.lastName
? `${membership.user.firstName} ${membership.user.lastName}`
: "-"}
</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Username</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{membership.user.username}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextUsername}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText("");
setCopyTextUsername("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingUsername ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Email</p>
<div className="flex items-center">
<p className="mr-2 text-sm text-mineshaft-300">{membership.user.email ?? "-"}</p>
<Tooltip
content={
membership.user.isEmailVerified
? "Email has been verified"
: "Email has not been verified"
}
>
<FontAwesomeIcon
size="sm"
icon={membership.user.isEmailVerified ? faCheckCircle : faCircleXmark}
/>
</Tooltip>
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Organization Role</p>
<p className="text-sm text-mineshaft-300">{roleName ?? "-"}</p>
</div>
<div>
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
<p className="text-sm text-mineshaft-300">{getStatus(membership)}</p>
</div>
{membership.isActive &&
(membership.status === "invited" || membership.status === "verified") &&
membership.user.email &&
serverDetails?.emailConfigured && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => {
return (
<Button
isDisabled={!isAllowed}
className="mt-4 w-full"
colorSchema="primary"
type="submit"
isLoading={isLoading}
onClick={() => {
onResendInvite(membership.user.email as string);
}}
>
Resend Invite
</Button>
);
}}
</OrgPermissionCan>
)}
</div>
</div>
) : (
<div />
);
};

@ -1,160 +0,0 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import { useGetOrgRoles, useUpdateOrgMembership } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
role: z.string()
});
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["orgMembership"]>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>, data?: {}) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["orgMembership"]>, state?: boolean) => void;
};
export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { data: roles } = useGetOrgRoles(orgId);
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema)
});
const popUpData = popUp?.orgMembership?.data as {
membershipId: string;
role: string;
};
useEffect(() => {
if (!roles?.length) return;
if (popUpData) {
reset({
role: popUpData.role
});
} else {
reset({
role: roles[0].slug
});
}
}, [popUp?.orgMembership?.data, roles]);
const onFormSubmit = async ({ role }: FormData) => {
try {
if (!orgId) return;
await updateOrgMembership({
organizationId: orgId,
membershipId: popUpData.membershipId,
role
});
handlePopUpToggle("orgMembership", false);
createNotification({
text: "Successfully updated user organization role",
type: "success"
});
reset();
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to update user organization role";
createNotification({
text,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.orgMembership?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("orgMembership", isOpen);
reset();
}}
>
<ModalContent title="Update Membership">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="role"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Update Organization Role"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
const isCustomRole = !["admin", "member", "no-access"].includes(e);
if (isCustomRole && subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan", {
description:
"You can assign custom roles to members if you upgrade your Infisical plan."
});
return;
}
onChange(e);
}}
className="w-full"
>
{(roles || []).map(({ name, slug }) => (
<SelectItem value={slug} key={`st-role-${slug}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Update
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("orgMembership", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};

@ -1,145 +0,0 @@
import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import { useAddUserToWsNonE2EE, useGetOrgMembershipProjectMemberships } from "@app/hooks/api";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
.object({
projectId: z.string()
})
.required();
type FormData = z.infer<typeof schema>;
type Props = {
membershipId: string;
popUp: UsePopUpState<["addUserToProject"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["addUserToProject"]>,
state?: boolean
) => void;
};
export const UserAddToProjectModal = ({ membershipId, popUp, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { workspaces } = useWorkspace();
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
const popupData = popUp.addUserToProject.data as {
username: string;
};
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema)
});
const { data: projectMemberships } = useGetOrgMembershipProjectMemberships(orgId, membershipId);
const filteredWorkspaces = useMemo(() => {
const wsWorkspaceIds = new Map();
projectMemberships?.forEach((projectMembership) => {
wsWorkspaceIds.set(projectMembership.project.id, true);
});
return (workspaces || []).filter(
({ id, orgId: projectOrgId, version }) =>
!wsWorkspaceIds.has(id) && projectOrgId === currentOrg?.id && version === ProjectVersion.V2
);
}, [workspaces, projectMemberships]);
const onFormSubmit = async ({ projectId }: FormData) => {
try {
await addUserToWorkspaceNonE2EE({
projectId,
usernames: [popupData.username],
orgId
});
createNotification({
text: "Successfully added user to project",
type: "success"
});
reset();
handlePopUpToggle("addUserToProject", false);
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to add identity to project";
createNotification({
text,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.addUserToProject?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addUserToProject", isOpen);
reset();
}}
>
<ModalContent title="Add User to Project">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="projectId"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Project" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(filteredWorkspaces || []).map(({ id, name }) => (
<SelectItem value={id} key={`project-${id}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Add
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("addUserToProject", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};

@ -1,90 +0,0 @@
import { useMemo } from "react";
import { useRouter } from "next/router";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { TWorkspaceUser } from "@app/hooks/api/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
membership: TWorkspaceUser;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromProject"]>, data?: {}) => void;
};
const formatRoleName = (role: string, customRoleName?: string) => {
if (role === ProjectMembershipRole.Custom) return customRoleName;
if (role === ProjectMembershipRole.Admin) return "Admin";
if (role === ProjectMembershipRole.Member) return "Developer";
if (role === ProjectMembershipRole.Viewer) return "Viewer";
if (role === ProjectMembershipRole.NoAccess) return "No Access";
return role;
};
export const UserProjectRow = ({
membership: { id, project, user, roles },
handlePopUpOpen
}: Props) => {
const { workspaces } = useWorkspace();
const router = useRouter();
const isAccessible = useMemo(() => {
const workspaceIds = new Map();
workspaces?.forEach((workspace) => {
workspaceIds.set(workspace.id, true);
});
return workspaceIds.has(project.id);
}, [workspaces, project]);
return (
<Tr
className="group h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
key={`user-project-membership-${id}`}
onClick={() => {
if (isAccessible) {
router.push(`/project/${project.id}/members`);
return;
}
createNotification({
text: "Unable to access project",
type: "error"
});
}}
>
<Td>{project.name}</Td>
<Td>{`${formatRoleName(roles[0].role, roles[0].customRoleName)}${
roles.length > 1 ? ` (+${roles.length - 1})` : ""
}`}</Td>
<Td>
{isAccessible && (
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content="Remove">
<IconButton
colorSchema="danger"
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeUserFromProject", {
username: user.username,
projectId: project.id,
projectName: project.name
});
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</div>
)}
</Td>
</Tr>
);
};

@ -1,98 +0,0 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal, IconButton } from "@app/components/v2";
import { useOrganization, useUser } from "@app/context";
import { useDeleteUserFromWorkspace, useGetOrgMembership } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { UserAddToProjectModal } from "./UserAddToProjectModal";
import { UserProjectsTable } from "./UserProjectsTable";
type Props = {
membershipId: string;
};
export const UserProjectsSection = ({ membershipId }: Props) => {
const { user } = useUser();
const { currentOrg } = useOrganization();
const userId = user?.id || "";
const orgId = currentOrg?.id || "";
const { data: membership } = useGetOrgMembership(orgId, membershipId);
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addUserToProject",
"removeUserFromProject"
] as const);
const handleRemoveUser = async (projectId: string, username: string) => {
try {
await removeUserFromWorkspace({ workspaceId: projectId, usernames: [username], orgId });
createNotification({
text: "Successfully removed user from project",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to remove user from the project",
type: "error"
});
}
handlePopUpClose("removeUserFromProject");
};
return membership ? (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Projects</h3>
{userId !== membership.user.id && membership.status !== "invited" && (
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
handlePopUpOpen("addUserToProject", {
username: membership.user.username
});
}}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
)}
</div>
<div className="py-4">
<UserProjectsTable membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />
</div>
<UserAddToProjectModal
membershipId={membershipId}
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.removeUserFromProject.isOpen}
deleteKey="remove"
title={`Do you want to remove this user from ${
(popUp?.removeUserFromProject?.data as { projectName: string })?.projectName || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("removeUserFromProject", isOpen)}
onDeleteApproved={() => {
const popupData = popUp?.removeUserFromProject?.data as {
username: string;
projectId: string;
projectName: string;
};
return handleRemoveUser(popupData.projectId, popupData.username);
}}
/>
</div>
) : (
<div />
);
};

@ -1,62 +0,0 @@
import { faFolder } from "@fortawesome/free-solid-svg-icons";
import {
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Th,
THead,
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useGetOrgMembershipProjectMemberships } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { UserProjectRow } from "./UserProjectRow";
type Props = {
membershipId: string;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromProject"]>, data?: {}) => void;
};
export const UserProjectsTable = ({ membershipId, handlePopUpOpen }: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { data: projectMemberships, isLoading } = useGetOrgMembershipProjectMemberships(
orgId,
membershipId
);
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Role</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={3} innerKey="user-project-memberships" />}
{!isLoading &&
projectMemberships?.map((membership) => {
return (
<UserProjectRow
key={`user-project-membership-${membership.id}`}
membership={membership}
handlePopUpOpen={handlePopUpOpen}
/>
);
})}
</TBody>
</Table>
{!isLoading && !projectMemberships?.length && (
<EmptyState title="This user has not been assigned to any projects" icon={faFolder} />
)}
</TableContainer>
);
};

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

@ -1,3 +0,0 @@
export { UserDetailsSection } from "./UserDetailsSection";
export { UserOrgMembershipModal } from "./UserOrgMembershipModal";
export { UserProjectsSection } from "./UserProjectsSection";

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

@ -6,15 +6,14 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { Button,FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import {
useAddUserToWsE2EE,
useAddUserToWsNonE2EE,
useGetOrgUsers,
useGetUserWsKey,
useGetWorkspaceUsers
} from "@app/hooks/api";
useGetWorkspaceUsers} from "@app/hooks/api";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
@ -77,8 +76,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
} else if (currentWorkspace.version === ProjectVersion.V2) {
await addUserToWorkspaceNonE2EE({
projectId: workspaceId,
usernames: [orgUser.user.username],
orgId
usernames: [orgUser.user.username]
});
} else {
createNotification({

@ -4,12 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useWorkspace
} from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub , useOrganization, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteUserFromWorkspace } from "@app/hooks/api";
@ -35,11 +30,7 @@ export const MembersSection = () => {
if (!currentWorkspace?.id) return;
try {
await removeUserFromWorkspace({
workspaceId: currentWorkspace.id,
usernames: [username],
orgId: currentOrg.id
});
await removeUserFromWorkspace({ workspaceId: currentWorkspace.id, usernames: [username] });
createNotification({
text: "Successfully removed user from project",
type: "success"

@ -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&apos;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 { OrgAuthTab } from "../OrgAuthTab";
import { OrgEncryptionTab } from "../OrgEncryptionTab";
import { OrgGeneralTab } from "../OrgGeneralTab";
const tabs = [
{ name: "General", key: "tab-org-general" },
{ name: "Security", key: "tab-org-security" },
{ name: "Encryption", key: "tab-org-encryption" },
{ name: "Audit Log Streams", key: "tag-audit-log-streams" }
];
export const OrgTabGroup = () => {
@ -19,8 +21,9 @@ export const OrgTabGroup = () => {
{({ selected }) => (
<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}
</button>
@ -35,6 +38,9 @@ export const OrgTabGroup = () => {
<Tab.Panel>
<OrgAuthTab />
</Tab.Panel>
<Tab.Panel>
<OrgEncryptionTab />
</Tab.Panel>
<Tab.Panel>
<AuditLogStreamsTab />
</Tab.Panel>

@ -2,12 +2,14 @@ import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { Tab } from "@headlessui/react";
import { EncryptionTab } from "./components/EncryptionTab";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
import { WebhooksTab } from "./components/WebhooksTab";
const tabs = [
{ 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 = () => {
@ -25,8 +27,9 @@ export const ProjectSettingsPage = () => {
{({ selected }) => (
<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}
</button>
@ -38,6 +41,9 @@ export const ProjectSettingsPage = () => {
<Tab.Panel>
<ProjectGeneralTab />
</Tab.Panel>
<Tab.Panel>
<EncryptionTab />
</Tab.Panel>
<Tab.Panel>
<WebhooksTab />
</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&apos;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&apos;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";