Compare commits

..

21 Commits

Author SHA1 Message Date
Sheen Capadngan
0a1242db75 misc: added pg queue init flag 2024-12-05 15:52:17 +08:00
Maidul Islam
095b26c8c9 Merge pull request #2841 from Infisical/integration-error-improvement
Improvement: Integration Error - Handle Response Data Empty String
2024-12-04 23:39:33 -05:00
Daniel Hougaard
4ace30aecd Merge pull request #2839 from Infisical/omar/eng-1966-click-to-copy-req-id-on-toast
Improvement(notifications): Add copyable request IDs to server side errors
2024-12-05 04:04:33 +04:00
McPizza
8b2a866994 fix nits 2024-12-04 23:32:55 +00:00
Daniel Hougaard
b4386af2e0 Merge pull request #2840 from Infisical/daniel/updated-java-sdk-docs
docs(java-sdk): updated for v3.0.0
2024-12-05 01:20:43 +04:00
Daniel Hougaard
2b44e32ac1 docs(java-sdk): updated for v3.0.0 2024-12-05 01:13:36 +04:00
Maidul Islam
ec5e6eb7b4 Merge pull request #2837 from Infisical/misc/use-pg-queue-for-audit-logs-with-flag
misc: pg-queue for audit logs
2024-12-04 14:25:33 -05:00
McPizza0
48cb5f6e9b feat(notifications): add copyable request IDs 2024-12-04 16:24:48 +00:00
Sheen Capadngan
0842901d4f misc: always initialize pg-boss 2024-12-04 23:21:37 +08:00
Sheen Capadngan
32d6826ade fix: resolve e2e 2024-12-04 22:52:30 +08:00
Sheen Capadngan
a750f48922 misc: finalized structure 2024-12-04 22:49:28 +08:00
Maidul Islam
67662686f3 Merge pull request #2836 from akhilmhdh/feat/dynamic-secret-safe-chars
feat: updated random pass generator of dynamic secret to use safe chars
2024-12-04 09:32:59 -05:00
Sheen Capadngan
11c96245a7 misc: added error listener 2024-12-04 22:27:07 +08:00
Sheen Capadngan
a63191e11d misc: use pg queue for audit logs when enabled 2024-12-04 22:22:34 +08:00
=
7a13c155f5 feat: updated random pass generator of dynamic secret to use safe characters 2024-12-04 15:15:53 +05:30
McPizza
5ceb30f43f feat(KMS): New external KMS support for Google GCP KMS (#2825)
* feat(KMS): New external KMS support for Google GCP KMS
2024-12-03 18:14:42 +01:00
McPizza
7728a4793b fix: Schema validation errors correctly returned as 422 (#2828)
* fix: Schema validation errors correctly returned as 422
2024-12-03 18:12:29 +01:00
Maidul Islam
d3523ed1d6 Merge pull request #2833 from akhilmhdh/fix/create-project
fix: resolved reduntant min membership check over project creation
2024-12-03 11:11:08 -05:00
=
35a9b2a38d fix: resolved reduntant min membership check over project create for identity 2024-12-03 21:13:16 +05:30
Scott Wilson
16a9f8c194 Merge pull request #2829 from Infisical/minor-ui-fixes
Improvements: Truncate Filterable Select List Options and Fix Null Display of User Last Name
2024-12-02 16:18:29 -08:00
Scott Wilson
9557639bfe truncate filter select list options and fix display of null last name for users 2024-12-02 16:06:15 -08:00
63 changed files with 1575 additions and 166 deletions

View File

@@ -10,12 +10,15 @@ export const mockQueue = (): TQueueServiceFactory => {
queue: async (name, jobData) => {
job[name] = jobData;
},
queuePg: async () => {},
initialize: async () => {},
shutdown: async () => undefined,
stopRepeatableJob: async () => true,
start: (name, jobFn) => {
queues[name] = jobFn;
workers[name] = jobFn;
},
startPg: async () => {},
listen: (name, event) => {
events[name] = event;
},

View File

@@ -53,7 +53,7 @@ export default {
extension: "ts"
});
const smtp = mockSmtpServer();
const queue = queueServiceFactory(cfg.REDIS_URL);
const queue = queueServiceFactory(cfg.REDIS_URL, cfg.DB_CONNECTION_URI);
const keyStore = keyStoreFactory(cfg.REDIS_URL);
const hsmModule = initializeHsmModule();

View File

@@ -28,6 +28,7 @@
"@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
"@google-cloud/kms": "^4.5.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/auth-app": "^7.1.1",
"@octokit/plugin-retry": "^5.0.5",
@@ -92,6 +93,7 @@
"passport-google-oauth20": "^2.0.0",
"passport-ldapauth": "^3.0.1",
"pg": "^8.11.3",
"pg-boss": "^10.1.5",
"pg-query-stream": "^4.5.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2",
@@ -5598,6 +5600,18 @@
"yaml": "^2.2.2"
}
},
"node_modules/@google-cloud/kms": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz",
"integrity": "sha512-i2vC0DI7bdfEhQszqASTw0KVvbB7HsO2CwTBod423NawAu7FWi+gVVa7NLfXVNGJaZZayFfci2Hu+om/HmyEjQ==",
"license": "Apache-2.0",
"dependencies": {
"google-gax": "^4.0.3"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@google-cloud/paginator": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz",
@@ -12259,14 +12273,6 @@
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"engines": {
"node": ">=4"
}
},
"node_modules/bullmq": {
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.4.2.tgz",
@@ -15086,6 +15092,44 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/google-gax": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.4.1.tgz",
"integrity": "sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.10.9",
"@grpc/proto-loader": "^0.7.13",
"@types/long": "^4.0.0",
"abort-controller": "^3.0.0",
"duplexify": "^4.0.0",
"google-auth-library": "^9.3.0",
"node-fetch": "^2.7.0",
"object-hash": "^3.0.0",
"proto3-json-serializer": "^2.0.2",
"protobufjs": "^7.3.2",
"retry-request": "^7.0.0",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/google-gax/node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
"license": "MIT"
},
"node_modules/google-gax/node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/googleapis": {
"version": "137.1.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz",
@@ -18185,11 +18229,6 @@
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
"node_modules/packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -18408,15 +18447,13 @@
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"node_modules/pg": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
"integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==",
"version": "8.13.1",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz",
"integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==",
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.6.2",
"pg-pool": "^3.6.1",
"pg-protocol": "^1.6.0",
"pg-connection-string": "^2.7.0",
"pg-pool": "^3.7.0",
"pg-protocol": "^1.7.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
@@ -18435,6 +18472,19 @@
}
}
},
"node_modules/pg-boss": {
"version": "10.1.5",
"resolved": "https://registry.npmjs.org/pg-boss/-/pg-boss-10.1.5.tgz",
"integrity": "sha512-H87NL6c7N6nTCSCePh16EaSQVSFevNXWdJuzY6PZz4rw+W/nuMKPfI/vYyXS0AdT1g1Q3S3EgeOYOHcB7ZVToQ==",
"dependencies": {
"cron-parser": "^4.9.0",
"pg": "^8.13.0",
"serialize-error": "^8.1.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/pg-cloudflare": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
@@ -18471,17 +18521,17 @@
}
},
"node_modules/pg-pool": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz",
"integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==",
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz",
"integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q=="
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz",
"integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ=="
},
"node_modules/pg-query-stream": {
"version": "4.5.3",
@@ -18510,9 +18560,9 @@
}
},
"node_modules/pg/node_modules/pg-connection-string": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz",
"integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA=="
},
"node_modules/pgpass": {
"version": "1.0.5",
@@ -19223,6 +19273,18 @@
"node": ">=6"
}
},
"node_modules/proto3-json-serializer": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz",
"integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==",
"license": "Apache-2.0",
"dependencies": {
"protobufjs": "^7.2.5"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/protobufjs": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
@@ -20111,6 +20173,20 @@
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"node_modules/serialize-error": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz",
"integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==",
"dependencies": {
"type-fest": "^0.20.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
@@ -22130,7 +22206,6 @@
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"engines": {
"node": ">=10"
},

View File

@@ -136,6 +136,7 @@
"@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
"@google-cloud/kms": "^4.5.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/auth-app": "^7.1.1",
"@octokit/plugin-retry": "^5.0.5",
@@ -200,6 +201,7 @@
"passport-google-oauth20": "^2.0.0",
"passport-ldapauth": "^3.0.1",
"pg": "^8.11.3",
"pg-boss": "^10.1.5",
"pg-query-stream": "^4.5.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2",

View File

@@ -4,9 +4,15 @@ import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
ExternalKmsAwsSchema,
ExternalKmsGcpCredentialSchema,
ExternalKmsGcpSchema,
ExternalKmsInputSchema,
ExternalKmsInputUpdateSchema
ExternalKmsInputUpdateSchema,
KmsGcpKeyFetchAuthType,
KmsProviders,
TExternalKmsGcpCredentialSchema
} from "@app/ee/services/external-kms/providers/model";
import { NotFoundError } from "@app/lib/errors";
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";
@@ -44,7 +50,8 @@ const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({
statusDetails: true,
provider: true
}).extend({
providerInput: ExternalKmsAwsSchema
// for GCP, we don't return the credential object as it is sensitive data that should not be exposed
providerInput: z.union([ExternalKmsAwsSchema, ExternalKmsGcpSchema.pick({ gcpRegion: true, keyName: true })])
})
});
@@ -286,4 +293,67 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
return { externalKms };
}
});
server.route({
method: "POST",
url: "/gcp/keys",
config: {
rateLimit: writeLimit
},
schema: {
body: z.discriminatedUnion("authMethod", [
z.object({
authMethod: z.literal(KmsGcpKeyFetchAuthType.Credential),
region: z.string().trim().min(1),
credential: ExternalKmsGcpCredentialSchema
}),
z.object({
authMethod: z.literal(KmsGcpKeyFetchAuthType.Kms),
region: z.string().trim().min(1),
kmsId: z.string().trim().min(1)
})
]),
response: {
200: z.object({
keys: z.string().array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { region, authMethod } = req.body;
let credentialJson: TExternalKmsGcpCredentialSchema | undefined;
if (authMethod === KmsGcpKeyFetchAuthType.Credential) {
credentialJson = req.body.credential;
} else if (authMethod === KmsGcpKeyFetchAuthType.Kms) {
const externalKms = await server.services.externalKms.findById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.kmsId
});
if (!externalKms || externalKms.external.provider !== KmsProviders.Gcp) {
throw new NotFoundError({ message: "KMS not found or not of type GCP" });
}
credentialJson = externalKms.external.providerInput.credential as TExternalKmsGcpCredentialSchema;
}
if (!credentialJson) {
throw new NotFoundError({
message: "Something went wrong while fetching the GCP credential, please check inputs and try again"
});
}
const results = await server.services.externalKms.fetchGcpKeys({
credential: credentialJson,
gcpRegion: region
});
return results;
}
});
};

View File

@@ -1,6 +1,7 @@
import { RawAxiosRequestHeaders } from "axios";
import { SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
@@ -20,27 +21,130 @@ type TAuditLogQueueServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TAuditLogQueueServiceFactory = ReturnType<typeof auditLogQueueServiceFactory>;
export type TAuditLogQueueServiceFactory = Awaited<ReturnType<typeof auditLogQueueServiceFactory>>;
// keep this timeout 5s it must be fast because else the queue will take time to finish
// audit log is a crowded queue thus needs to be fast
export const AUDIT_LOG_STREAM_TIMEOUT = 5 * 1000;
export const auditLogQueueServiceFactory = ({
export const auditLogQueueServiceFactory = async ({
auditLogDAL,
queueService,
projectDAL,
licenseService,
auditLogStreamDAL
}: TAuditLogQueueServiceFactoryDep) => {
const appCfg = getConfig();
const pushToLog = async (data: TCreateAuditLogDTO) => {
await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, {
removeOnFail: {
count: 3
},
removeOnComplete: true
});
if (appCfg.USE_PG_QUEUE && appCfg.SHOULD_INIT_PG_QUEUE) {
await queueService.queuePg<QueueName.AuditLog>(QueueJobs.AuditLog, data, {
retryLimit: 10,
retryBackoff: true
});
} else {
await queueService.queue<QueueName.AuditLog>(QueueName.AuditLog, QueueJobs.AuditLog, data, {
removeOnFail: {
count: 3
},
removeOnComplete: true
});
}
};
if (appCfg.SHOULD_INIT_PG_QUEUE) {
await queueService.startPg<QueueName.AuditLog>(
QueueJobs.AuditLog,
async ([job]) => {
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
let { orgId } = job.data;
const MS_IN_DAY = 24 * 60 * 60 * 1000;
let project;
if (!orgId) {
// it will never be undefined for both org and project id
// TODO(akhilmhdh): use caching here in dal to avoid db calls
project = await projectDAL.findById(projectId as string);
orgId = project.orgId;
}
const plan = await licenseService.getPlan(orgId);
if (plan.auditLogsRetentionDays === 0) {
// skip inserting if audit log retention is 0 meaning its not supported
return;
}
// For project actions, set TTL to project-level audit log retention config
// This condition ensures that the plan's audit log retention days cannot be bypassed
const ttlInDays =
project?.auditLogsRetentionDays && project.auditLogsRetentionDays < plan.auditLogsRetentionDays
? project.auditLogsRetentionDays
: plan.auditLogsRetentionDays;
const ttl = ttlInDays * MS_IN_DAY;
const auditLog = await auditLogDAL.create({
actor: actor.type,
actorMetadata: actor.metadata,
userAgent,
projectId,
projectName: project?.name,
ipAddress,
orgId,
eventType: event.type,
expiresAt: new Date(Date.now() + ttl),
eventMetadata: event.metadata,
userAgentType
});
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
await Promise.allSettled(
logStreams.map(
async ({
url,
encryptedHeadersTag,
encryptedHeadersIV,
encryptedHeadersKeyEncoding,
encryptedHeadersCiphertext
}) => {
const streamHeaders =
encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag
? (JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding,
iv: encryptedHeadersIV,
tag: encryptedHeadersTag,
ciphertext: encryptedHeadersCiphertext
})
) as LogStreamHeaders[])
: [];
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
if (streamHeaders.length)
streamHeaders.forEach(({ key, value }) => {
headers[key] = value;
});
return request.post(url, auditLog, {
headers,
// request timeout
timeout: AUDIT_LOG_STREAM_TIMEOUT,
// connection timeout
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
});
}
)
);
},
{
batchSize: 1,
workerCount: 30,
pollingIntervalSeconds: 0.5
}
);
}
queueService.start(QueueName.AuditLog, async (job) => {
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
let { orgId } = job.data;

View File

@@ -127,7 +127,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin
};
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 64)();
};
@@ -211,7 +211,7 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
return { entityId };
};
const renew = async (inputs: unknown, entityId: string) => {
const renew = async (_inputs: unknown, entityId: string) => {
// No renewal necessary
return { entityId };
};

View File

@@ -9,7 +9,7 @@ const MSFT_GRAPH_API_URL = "https://graph.microsoft.com/v1.0/";
const MSFT_LOGIN_URL = "https://login.microsoftonline.com";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 64)();
};
@@ -122,7 +122,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
return users;
};
const renew = async (inputs: unknown, entityId: string) => {
const renew = async (_inputs: unknown, entityId: string) => {
// No renewal necessary
return { entityId };
};

View File

@@ -9,7 +9,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models";
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 48)(size);
};

View File

@@ -8,7 +8,7 @@ import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 64)();
};
@@ -95,7 +95,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
return { entityId };
};
const renew = async (inputs: unknown, entityId: string) => {
const renew = async (_inputs: unknown, entityId: string) => {
// No renewal necessary
return { entityId };
};

View File

@@ -8,7 +8,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models";
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 48)(size);
};

View File

@@ -8,7 +8,7 @@ import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models";
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 48)(size);
};

View File

@@ -11,7 +11,7 @@ import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 64)();
};
@@ -141,7 +141,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
return { entityId };
};
const renew = async (inputs: unknown, entityId: string) => {
const renew = async (_inputs: unknown, entityId: string) => {
// No renewal necessary
return { entityId };
};

View File

@@ -10,7 +10,7 @@ import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 64)();
};

View File

@@ -12,7 +12,7 @@ import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models";
const noop = () => {};
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 48)(size);
};

View File

@@ -14,7 +14,7 @@ const generatePassword = (provider: SqlProviders) => {
// oracle has limit of 48 password length
const size = provider === SqlProviders.Oracle ? 30 : 48;
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 48)(size);
};

View File

@@ -20,7 +20,8 @@ import {
TUpdateExternalKmsDTO
} from "./external-kms-types";
import { AwsKmsProviderFactory } from "./providers/aws-kms";
import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model";
import { GcpKmsProviderFactory } from "./providers/gcp-kms";
import { ExternalKmsAwsSchema, ExternalKmsGcpSchema, KmsProviders, TExternalKmsGcpSchema } from "./providers/model";
type TExternalKmsServiceFactoryDep = {
externalKmsDAL: TExternalKmsDALFactory;
@@ -78,6 +79,13 @@ export const externalKmsServiceFactory = ({
await externalKms.validateConnection();
}
break;
case KmsProviders.Gcp:
{
const externalKms = await GcpKmsProviderFactory({ inputs: provider.inputs });
await externalKms.validateConnection();
sanitizedProviderInput = JSON.stringify(provider.inputs);
}
break;
default:
throw new BadRequestError({ message: "external kms provided is invalid" });
}
@@ -88,7 +96,7 @@ export const externalKmsServiceFactory = ({
});
const { cipherTextBlob: encryptedProviderInputs } = orgDataKeyEncryptor({
plainText: Buffer.from(sanitizedProviderInput, "utf8")
plainText: Buffer.from(sanitizedProviderInput)
});
const externalKms = await externalKmsDAL.transaction(async (tx) => {
@@ -162,7 +170,7 @@ export const externalKmsServiceFactory = ({
case KmsProviders.Aws:
{
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
JSON.parse(decryptedProviderInputBlob.toString())
);
const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs };
const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput });
@@ -170,6 +178,17 @@ export const externalKmsServiceFactory = ({
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
}
break;
case KmsProviders.Gcp:
{
const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync(
JSON.parse(decryptedProviderInputBlob.toString())
);
const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs };
const externalKms = await GcpKmsProviderFactory({ inputs: updatedProviderInput });
await externalKms.validateConnection();
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
}
break;
default:
throw new BadRequestError({ message: "external kms provided is invalid" });
}
@@ -178,7 +197,7 @@ export const externalKmsServiceFactory = ({
let encryptedProviderInputs: Buffer | undefined;
if (sanitizedProviderInput) {
const { cipherTextBlob } = orgDataKeyEncryptor({
plainText: Buffer.from(sanitizedProviderInput, "utf8")
plainText: Buffer.from(sanitizedProviderInput)
});
encryptedProviderInputs = cipherTextBlob;
}
@@ -271,10 +290,17 @@ export const externalKmsServiceFactory = ({
switch (externalKmsDoc.provider) {
case KmsProviders.Aws: {
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
JSON.parse(decryptedProviderInputBlob.toString())
);
return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } };
}
case KmsProviders.Gcp: {
const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync(
JSON.parse(decryptedProviderInputBlob.toString())
);
return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } };
}
default:
throw new BadRequestError({ message: "external kms provided is invalid" });
}
@@ -312,21 +338,34 @@ export const externalKmsServiceFactory = ({
switch (externalKmsDoc.provider) {
case KmsProviders.Aws: {
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
JSON.parse(decryptedProviderInputBlob.toString())
);
return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } };
}
case KmsProviders.Gcp: {
const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync(
JSON.parse(decryptedProviderInputBlob.toString())
);
return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } };
}
default:
throw new BadRequestError({ message: "external kms provided is invalid" });
}
};
const fetchGcpKeys = async ({ credential, gcpRegion }: Pick<TExternalKmsGcpSchema, "credential" | "gcpRegion">) => {
const externalKms = await GcpKmsProviderFactory({ inputs: { credential, gcpRegion, keyName: "" } });
return externalKms.getKeysList();
};
return {
create,
updateById,
deleteById,
list,
findById,
findByName
findByName,
fetchGcpKeys
};
};

View File

@@ -0,0 +1,113 @@
import { KeyManagementServiceClient } from "@google-cloud/kms";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { ExternalKmsGcpSchema, TExternalKmsGcpClientSchema, TExternalKmsProviderFns } from "./model";
const getGcpKmsClient = async ({ credential, gcpRegion }: TExternalKmsGcpClientSchema) => {
const gcpKmsClient = new KeyManagementServiceClient({
credentials: credential
});
const projectId = credential.project_id;
const locationName = gcpKmsClient.locationPath(projectId, gcpRegion);
return {
gcpKmsClient,
locationName
};
};
type GcpKmsProviderArgs = {
inputs: unknown;
};
type TGcpKmsProviderFactoryReturn = TExternalKmsProviderFns & {
getKeysList: () => Promise<{ keys: string[] }>;
};
export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Promise<TGcpKmsProviderFactoryReturn> => {
const { credential, gcpRegion, keyName } = await ExternalKmsGcpSchema.parseAsync(inputs);
const { gcpKmsClient, locationName } = await getGcpKmsClient({
credential,
gcpRegion
});
const validateConnection = async () => {
try {
await gcpKmsClient.listKeyRings({
parent: locationName
});
return true;
} catch (error) {
throw new BadRequestError({
message: "Cannot connect to GCP KMS"
});
}
};
// Used when adding the KMS to fetch the list of keys in specified region
const getKeysList = async () => {
try {
const [keyRings] = await gcpKmsClient.listKeyRings({
parent: locationName
});
const validKeyRings = keyRings
.filter(
(keyRing): keyRing is { name: string } =>
keyRing !== null && typeof keyRing === "object" && "name" in keyRing && typeof keyRing.name === "string"
)
.map((keyRing) => keyRing.name);
const keyList: string[] = [];
const keyListPromises = validKeyRings.map((keyRingName) =>
gcpKmsClient
.listCryptoKeys({
parent: keyRingName
})
.then(([cryptoKeys]) =>
cryptoKeys
.filter(
(key): key is { name: string } =>
key !== null && typeof key === "object" && "name" in key && typeof key.name === "string"
)
.map((key) => key.name)
)
);
const cryptoKeyLists = await Promise.all(keyListPromises);
keyList.push(...cryptoKeyLists.flat());
return { keys: keyList };
} catch (error) {
logger.error(error, "Could not validate GCP KMS connection and credentials");
throw new BadRequestError({
message: "Could not validate GCP KMS connection and credentials",
error
});
}
};
const encrypt = async (data: Buffer) => {
const encryptedText = await gcpKmsClient.encrypt({
name: keyName,
plaintext: data
});
if (!encryptedText[0].ciphertext) throw new Error("encryption failed");
return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext) };
};
const decrypt = async (encryptedBlob: Buffer) => {
const decryptedText = await gcpKmsClient.decrypt({
name: keyName,
ciphertext: encryptedBlob
});
if (!decryptedText[0].plaintext) throw new Error("decryption failed");
return { data: Buffer.from(decryptedText[0].plaintext) };
};
return {
validateConnection,
getKeysList,
encrypt,
decrypt
};
};

View File

@@ -1,13 +1,23 @@
import { z } from "zod";
export enum KmsProviders {
Aws = "aws"
Aws = "aws",
Gcp = "gcp"
}
export enum KmsAwsCredentialType {
AssumeRole = "assume-role",
AccessKey = "access-key"
}
// Google uses snake_case for their enum values and we need to match that
export enum KmsGcpCredentialType {
ServiceAccount = "service_account"
}
export enum KmsGcpKeyFetchAuthType {
Credential = "credential",
Kms = "kmsId"
}
export const ExternalKmsAwsSchema = z.object({
credential: z
@@ -42,14 +52,44 @@ export const ExternalKmsAwsSchema = z.object({
});
export type TExternalKmsAwsSchema = z.infer<typeof ExternalKmsAwsSchema>;
export const ExternalKmsGcpCredentialSchema = z.object({
type: z.literal(KmsGcpCredentialType.ServiceAccount),
project_id: z.string().min(1),
private_key_id: z.string().min(1),
private_key: z.string().min(1),
client_email: z.string().min(1),
client_id: z.string().min(1),
auth_uri: z.string().min(1),
token_uri: z.string().min(1),
auth_provider_x509_cert_url: z.string().min(1),
client_x509_cert_url: z.string().min(1),
universe_domain: z.string().min(1)
});
export type TExternalKmsGcpCredentialSchema = z.infer<typeof ExternalKmsGcpCredentialSchema>;
export const ExternalKmsGcpSchema = z.object({
credential: ExternalKmsGcpCredentialSchema.describe("GCP Service Account JSON credential to connect"),
gcpRegion: z.string().trim().describe("GCP region where the KMS key is located"),
keyName: z.string().trim().describe("GCP key name")
});
export type TExternalKmsGcpSchema = z.infer<typeof ExternalKmsGcpSchema>;
const ExternalKmsGcpClientSchema = ExternalKmsGcpSchema.pick({ gcpRegion: true }).extend({
credential: ExternalKmsGcpCredentialSchema
});
export type TExternalKmsGcpClientSchema = z.infer<typeof ExternalKmsGcpClientSchema>;
// The root schema of the JSON
export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema })
z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema }),
z.object({ type: z.literal(KmsProviders.Gcp), inputs: ExternalKmsGcpSchema })
]);
export type TExternalKmsInputSchema = z.infer<typeof ExternalKmsInputSchema>;
export const ExternalKmsInputUpdateSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema.partial() })
z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema.partial() }),
z.object({ type: z.literal(KmsProviders.Gcp), inputs: ExternalKmsGcpSchema.partial() })
]);
export type TExternalKmsInputUpdateSchema = z.infer<typeof ExternalKmsInputUpdateSchema>;

View File

@@ -178,7 +178,10 @@ const envSchema = z
HSM_LIB_PATH: zpStr(z.string().optional()),
HSM_PIN: zpStr(z.string().optional()),
HSM_KEY_LABEL: zpStr(z.string().optional()),
HSM_SLOT: z.coerce.number().optional().default(0)
HSM_SLOT: z.coerce.number().optional().default(0),
USE_PG_QUEUE: zodStrBool.default("false"),
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false")
})
// To ensure that basic encryption is always possible.
.refine(

View File

@@ -55,7 +55,10 @@ const run = async () => {
}
const smtp = smtpServiceFactory(formatSmtpConfig());
const queue = queueServiceFactory(appCfg.REDIS_URL);
const queue = queueServiceFactory(appCfg.REDIS_URL, appCfg.DB_CONNECTION_URI);
await queue.initialize();
const keyStore = keyStoreFactory(appCfg.REDIS_URL);
const hsmModule = initializeHsmModule();

View File

@@ -1,5 +1,6 @@
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
import Redis from "ioredis";
import PgBoss, { WorkOptions } from "pg-boss";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
@@ -7,6 +8,8 @@ import {
TScanFullRepoEventPayload,
TScanPushEventPayload
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import {
TFailedIntegrationSyncEmailsPayload,
TIntegrationSyncPayload,
@@ -184,17 +187,39 @@ export type TQueueJobTypes = {
};
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
export const queueServiceFactory = (redisUrl: string) => {
export const queueServiceFactory = (redisUrl: string, dbConnectionUrl: string) => {
const connection = new Redis(redisUrl, { maxRetriesPerRequest: null });
const queueContainer = {} as Record<
QueueName,
Queue<TQueueJobTypes[QueueName]["payload"], void, TQueueJobTypes[QueueName]["name"]>
>;
const pgBoss = new PgBoss({
connectionString: dbConnectionUrl,
archiveCompletedAfterSeconds: 60,
archiveFailedAfterSeconds: 1000, // we want to keep failed jobs for a longer time so that it can be retried
deleteAfterSeconds: 30
});
const queueContainerPg = {} as Record<QueueJobs, boolean>;
const workerContainer = {} as Record<
QueueName,
Worker<TQueueJobTypes[QueueName]["payload"], void, TQueueJobTypes[QueueName]["name"]>
>;
const initialize = async () => {
const appCfg = getConfig();
if (appCfg.SHOULD_INIT_PG_QUEUE) {
logger.info("Initializing pg-queue...");
await pgBoss.start();
pgBoss.on("error", (error) => {
logger.error(error, "pg-queue error");
});
}
};
const start = <T extends QueueName>(
name: T,
jobFn: (job: Job<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>, token?: string) => Promise<void>,
@@ -215,6 +240,27 @@ export const queueServiceFactory = (redisUrl: string) => {
});
};
const startPg = async <T extends QueueName>(
jobName: QueueJobs,
jobsFn: (jobs: PgBoss.Job<TQueueJobTypes[T]["payload"]>[]) => Promise<void>,
options: WorkOptions & {
workerCount: number;
}
) => {
if (queueContainerPg[jobName]) {
throw new Error(`${jobName} queue is already initialized`);
}
await pgBoss.createQueue(jobName);
queueContainerPg[jobName] = true;
await Promise.all(
Array.from({ length: options.workerCount }).map(() =>
pgBoss.work<TQueueJobTypes[T]["payload"]>(jobName, options, jobsFn)
)
);
};
const listen = <
T extends QueueName,
U extends keyof WorkerListener<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>
@@ -238,6 +284,18 @@ export const queueServiceFactory = (redisUrl: string) => {
await q.add(job, data, opts);
};
const queuePg = async <T extends QueueName>(
job: TQueueJobTypes[T]["name"],
data: TQueueJobTypes[T]["payload"],
opts?: PgBoss.SendOptions & { jobId?: string }
) => {
await pgBoss.send({
name: job,
data,
options: opts
});
};
const stopRepeatableJob = async <T extends QueueName>(
name: T,
job: TQueueJobTypes[T]["name"],
@@ -274,5 +332,17 @@ export const queueServiceFactory = (redisUrl: string) => {
await Promise.all(Object.values(workerContainer).map((worker) => worker.close()));
};
return { start, listen, queue, shutdown, stopRepeatableJob, stopRepeatableJobByJobId, clearQueue, stopJobById };
return {
initialize,
start,
listen,
queue,
shutdown,
stopRepeatableJob,
stopRepeatableJobByJobId,
clearQueue,
stopJobById,
startPg,
queuePg
};
};

View File

@@ -27,6 +27,7 @@ enum HttpStatusCodes {
NotFound = 404,
Unauthorized = 401,
Forbidden = 403,
UnprocessableContent = 422,
// eslint-disable-next-line @typescript-eslint/no-shadow
InternalServerError = 500,
GatewayTimeout = 504,
@@ -66,9 +67,9 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
error: error.name
});
} else if (error instanceof ZodError) {
void res.status(HttpStatusCodes.Unauthorized).send({
void res.status(HttpStatusCodes.UnprocessableContent).send({
requestId: req.id,
statusCode: HttpStatusCodes.Unauthorized,
statusCode: HttpStatusCodes.UnprocessableContent,
error: "ValidationFailure",
message: error.issues
});

View File

@@ -394,13 +394,14 @@ export const registerRoutes = async (
permissionService
});
const auditLogQueue = auditLogQueueServiceFactory({
const auditLogQueue = await auditLogQueueServiceFactory({
auditLogDAL,
queueService,
projectDAL,
licenseService,
auditLogStreamDAL
});
const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue });
const auditLogStreamService = auditLogStreamServiceFactory({
licenseService,

View File

@@ -44,7 +44,7 @@ export const DefaultResponseErrorsSchema = {
401: z.object({
requestId: z.string(),
statusCode: z.literal(401),
message: z.any(),
message: z.string(),
error: z.string()
}),
403: z.object({
@@ -54,6 +54,13 @@ export const DefaultResponseErrorsSchema = {
details: z.any().optional(),
error: z.string()
}),
// Zod errors return a message of varying shapes and sizes, so z.any() is used here
422: z.object({
requestId: z.string(),
statusCode: z.literal(422),
message: z.any(),
error: z.string()
}),
500: z.object({
requestId: z.string(),
statusCode: z.literal(500),

View File

@@ -4,8 +4,10 @@ import { z } from "zod";
import { KmsKeysSchema, TKmsRootConfig } from "@app/db/schemas";
import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms";
import { GcpKmsProviderFactory } from "@app/ee/services/external-kms/providers/gcp-kms";
import {
ExternalKmsAwsSchema,
ExternalKmsGcpSchema,
KmsProviders,
TExternalKmsProviderFns
} from "@app/ee/services/external-kms/providers/model";
@@ -291,6 +293,16 @@ export const kmsServiceFactory = ({
});
break;
}
case KmsProviders.Gcp: {
const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync(
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
);
externalKms = await GcpKmsProviderFactory({
inputs: decryptedProviderInput
});
break;
}
default:
throw new Error("Invalid KMS provider.");
}
@@ -353,6 +365,16 @@ export const kmsServiceFactory = ({
});
break;
}
case KmsProviders.Gcp: {
const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync(
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
);
externalKms = await GcpKmsProviderFactory({
inputs: decryptedProviderInput
});
break;
}
default:
throw new Error("Invalid KMS provider.");
}

View File

@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion, TProjectEnvironments } from "@app/db/schemas";
import { ProjectMembershipRole, ProjectVersion, TProjectEnvironments } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -9,7 +9,6 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
@@ -370,20 +369,6 @@ export const projectServiceFactory = ({
});
}
// Get the role permission for the identity
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
OrgMembershipRole.Member,
organization.id
);
// Identity has to be at least a member in order to create projects
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPrivilege)
throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role"
});
const isCustomRole = Boolean(customRole);
const identityProjectMembership = await identityProjectDAL.create(
{
identityId: actorId,
@@ -395,8 +380,7 @@ export const projectServiceFactory = ({
await identityProjectMembershipRoleDAL.create(
{
projectMembershipId: identityProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin,
customRoleId: customRole?.id
role: ProjectMembershipRole.Admin
},
tx
);

View File

@@ -74,22 +74,22 @@ Next, you will need to follow the steps listed below to add AWS KMS for your org
<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)
![Open encryption org settings](../../../images/platform/kms/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)
![Add encryption org settings](../../../images/platform/kms/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)
![Select Encryption Provider](../../../images/platform/kms/encryption-modal-provider-select.png)
Choose 'AWS KMS' from the list of encryption providers.
</Step>
<Step title="Provide the inputs for AWS KMS">
Selecting AWS as the provider will require you input the following fields.
Selecting AWS as the provider will require you input the following fields.
<ParamField path="Alias" type="string" required>
Name for referencing the AWS KMS key within the organization.
</ParamField>
<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.

View File

@@ -0,0 +1,132 @@
---
title: "GCP Key Management Service"
description: "Learn how to manage encryption using GCP KMS"
---
To enhance the security of your Infisical projects, you can now encrypt your secrets using an external Key Management Service (KMS).
When external KMS is configured for your project, all encryption and decryption operations will be handled by the chosen KMS.
This guide will walk you through the steps needed to configure external KMS support with Google Cloud KMS.
## Prerequisites
Before you begin, you'll first need to set up a GCP Service Account, add a KMS key and set the required permissions.
<Steps>
<Step title="Create a GCP Service Account">
1. Navigate to the [Create Service Account](https://console.cloud.google.com/iam-admin/serviceaccounts/create) page in your GCP Console.
![GCP Service Account Creation](/images/platform/kms/gcp/service-account-form.png)
2. Give the service account a suitable **name** and **description**. Then click **Create and Continue**.
3. Under **Grant this service account access to project**, click **Select a role** and select the
**Cloud KMS Viewer** and **Cloud KMS CryptoKey Encrypter/Decrypter*** roles, then click **Continue**.
![GCP Service Account Permissions](/images/platform/kms/gcp/service-account-permissions.png)
3. You can skip the **Grant users access to this service account** options.
4. Click Done.
5. You should see the service account in the list of service accounts. Click it to view the service account details.
6. Select the **Keys** tab, click **Add Key**, select **Create new key**, select **JSON** as the key type, then click **Create**.
7. You will be prompted to download a JSON file that we will need later on.
<Info>
Remember to keep the JSON file in a secure location. It will be used to authenticate your GCP service account.
Once you have successfully set up GCP KMS with Infisical, you should permanently delete the JSON file.
</Info>
</Step>
<Step title="Add a GCP KMS Key">
1. Navigate to the [KMS](https://console.cloud.google.com/security/kms) page in your GCP Console.
<Info>
If you have not used GCP KMS before, you will be redirected to the **Cloud Key Management Service (KMS) API** page.
Click **Enable** to enable the KMS API, then continue the steps below.
It may take a few minutes for the API to be enabled and KMS section of the Cloud Console to become viewable.
</Info>
2. In the KMS section, click **Create Key Ring**.
![GCP Create Key Ring](/images/platform/kms/gcp/keyring-create.png)
3. Give the key ring a **Name** and select a **Region**, then click **Create**.
<Info>
We don't currently support multi-region key rings.
</Info>
4. On the "Create Key" page, give the key a **Name** and set the **Protection Level** based on your requirements (or use default *Software*), then click **Continue**.
5. Under **Key Material**, select **Generated Key**, then click **Continue**.
6. Under **Purpose**, select **Symmetric encrypt/decrypt**, then click **Continue**.
7. For **Key Rotation Period**, select **Never (manual rotation)**, then click **Continue** followed by **Create**.
8. You should see the key in the list of keys. We're now ready to set it up in Infisical.
</Step>
</Steps>
## Setup GCP KMS in the Organization Settings
Next, you will need to follow the steps listed below to add GCP KMS for your organization.
<Steps>
<Step title="Navigate to the organization settings and select the 'Encryption' tab.">
![Open encryption org settings](../../../images/platform/kms/encryption-org-settings.png)
</Step>
<Step title="Click on the 'Add' button">
![Add encryption org settings](../../../images/platform/kms/encryption-org-settings-add.png)
Click the 'Add' button to begin adding a new external KMS.
</Step>
<Step title="Select 'GCP KMS'">
![Select Encryption Provider](../../../images/platform/kms/encryption-modal-provider-select.png)
Choose 'GCP KMS' from the list of encryption providers.
</Step>
<Step title="Provide the inputs for GCP KMS">
![GCP Create KMS Modal](/images/platform/kms/gcp/gcp-add-modal-filled.png)
Selecting GCP as the provider will require you input the following fields.
<ParamField path="Alias" type="string" required>
Name for referencing the GCP KMS key within the organization.
</ParamField>
<ParamField path="Description" type="string">
Short description of the GCP KMS key.
</ParamField>
<ParamField path="GCP Region" type="dropdown" required>
The GCP region where the GCP KMS key ring is located.
</ParamField>
<ParamField path="Service Account Credential JSON" type="file" required>
Upload the JSON file you downloaded earlier when creating the GCP service account.
</ParamField>
<ParamField path="GCP Key Name" type="dropdown" required>
This field will be populated with the list of GCP KMS keys in the selected region. Select the key you created earlier.
</ParamField>
</Step>
<Step title="Click Save">
Save your configuration to apply the settings.
</Step>
</Steps>
You now have a GCP KMS Key configured at the organization level. You can assign these GCP KMS keys to existing Infisical projects by visiting the 'Project Settings' page.
## Assign GCP KMS Key to an Existing Project
To assign the GCP KMS key you added to your organization, follow the steps below.
<Steps>
<Step title="Open Project Settings and select to the Encryption Tab">
![Open encryption project
settings](../../../images/platform/kms/gcp/project-settings.png)
</Step>
<Step title="Under the Key Management section, select your newly added GCP KMS key from the dropdown">
![Select encryption project
settings](../../../images/platform/kms/gcp/select-gcp-kms-in-project.png)
Choose the GCP KMS key you configured earlier.
</Step>
<Step title="Click Save">
Once you have selected the KMS of choice, click save.
</Step>
</Steps>

View File

@@ -25,4 +25,4 @@ For existing projects, you can configure the KMS from the Project Settings page.
## External KMS
Infisical supports the use of external KMS solutions to enhance security and compliance. You can configure your project to use services like [AWS Key Management Service](./aws-kms) for managing encryption.
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) or [GCP Key Management Service](./gcp-kms) for managing encryption.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

View File

Before

Width:  |  Height:  |  Size: 694 KiB

After

Width:  |  Height:  |  Size: 694 KiB

View File

Before

Width:  |  Height:  |  Size: 482 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -32,7 +32,10 @@
"thumbsRating": true
},
"api": {
"baseUrl": ["https://app.infisical.com", "http://localhost:8080"]
"baseUrl": [
"https://app.infisical.com",
"http://localhost:8080"
]
},
"topbarLinks": [
{
@@ -73,7 +76,9 @@
"documentation/getting-started/introduction",
{
"group": "Quickstart",
"pages": ["documentation/guides/local-development"]
"pages": [
"documentation/guides/local-development"
]
},
{
"group": "Guides",
@@ -127,7 +132,8 @@
"pages": [
"documentation/platform/kms-configuration/overview",
"documentation/platform/kms-configuration/aws-kms",
"documentation/platform/kms-configuration/aws-hsm"
"documentation/platform/kms-configuration/aws-hsm",
"documentation/platform/kms-configuration/gcp-kms"
]
},
{
@@ -461,20 +467,24 @@
},
{
"group": "Build Tool Integrations",
"pages": ["integrations/build-tools/gradle"]
"pages": [
"integrations/build-tools/gradle"
]
},
{
"group": "",
"pages": ["sdks/overview"]
"pages": [
"sdks/overview"
]
},
{
"group": "SDK's",
"pages": [
"sdks/languages/node",
"sdks/languages/python",
"sdks/languages/java",
"sdks/languages/go",
"sdks/languages/ruby",
"sdks/languages/java",
"sdks/languages/csharp"
]
},
@@ -485,7 +495,9 @@
"api-reference/overview/authentication",
{
"group": "Examples",
"pages": ["api-reference/overview/examples/integration"]
"pages": [
"api-reference/overview/examples/integration"
]
}
]
},
@@ -760,11 +772,15 @@
},
{
"group": "Service Tokens",
"pages": ["api-reference/endpoints/service-tokens/get"]
"pages": [
"api-reference/endpoints/service-tokens/get"
]
},
{
"group": "Audit Logs",
"pages": ["api-reference/endpoints/audit-logs/export-audit-log"]
"pages": [
"api-reference/endpoints/audit-logs/export-audit-log"
]
}
]
},
@@ -863,7 +879,9 @@
},
{
"group": "",
"pages": ["changelog/overview"]
"pages": [
"changelog/overview"
]
},
{
"group": "Contributing",
@@ -887,7 +905,9 @@
},
{
"group": "Contributing to SDK",
"pages": ["contributing/sdk/developing"]
"pages": [
"contributing/sdk/developing"
]
}
]
}
@@ -911,13 +931,22 @@
{
"title": "PRODUCT",
"links": [
{ "label": "Secret Management", "url": "https://infisical.com/" },
{ "label": "Secret Scanning", "url": "https://infisical.com/radar" },
{
"label": "Secret Management",
"url": "https://infisical.com/"
},
{
"label": "Secret Scanning",
"url": "https://infisical.com/radar"
},
{
"label": "Share Secrets",
"url": "https://app.infisical.com/share-secret"
},
{ "label": "Pricing", "url": "https://infisical.com/pricing" },
{
"label": "Pricing",
"url": "https://infisical.com/pricing"
},
{
"label": "Security",
"url": "https://infisical.com/docs/internals/security"
@@ -1061,4 +1090,4 @@
}
]
}
}
}

View File

@@ -1,9 +1,12 @@
---
title: "Infisical Java SDK"
sidebarTitle: "Java"
url: "https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-nodejs-sdk"
icon: "java"
---
{
/*
If you're working with Java, the official [Infisical Java SDK](https://github.com/Infisical/sdk/tree/main/languages/java) package is the easiest way to fetch and work with secrets for your application.
- [Maven Package](https://github.com/Infisical/sdk/packages/2019741)
@@ -568,4 +571,5 @@ String decryptedString = client.decryptSymmetric(decryptOptions);
</ParamField>
#### Returns (string)
`Plaintext` (string): The decrypted plaintext.
`Plaintext` (string): The decrypted plaintext.
*/}

View File

@@ -16,7 +16,7 @@ From local development to production, Infisical SDKs provide the easiest way for
<Card href="https://github.com/Infisical/python-sdk-official" title="Python" icon="python" color="#4c8abe">
Manage secrets for your Python application on demand
</Card>
<Card href="/sdks/languages/java" title="Java" icon="java" color="#e41f23">
<Card href="https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-nodejs-sdk" title="Java" icon="java" color="#e41f23">
Manage secrets for your Java application on demand
</Card>
<Card href="/sdks/languages/go" title="Go" icon="golang" color="#367B99">

View File

@@ -1,18 +1,60 @@
import { ReactNode } from "react";
import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toastify";
import { faCopy, IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import { CopyButton } from "../v2/CopyButton";
export type TNotification = {
title?: string;
text: ReactNode;
children?: ReactNode;
callToAction?: ReactNode;
copyActions?: { icon?: IconDefinition; value: string; name: string; label?: string }[];
};
export const NotificationContent = ({ title, text, children }: TNotification) => {
export const NotificationContent = ({
title,
text,
children,
callToAction,
copyActions
}: TNotification) => {
return (
<div className="msg-container">
{title && <div className="text-md mb-1 font-medium">{title}</div>}
<div className={title ? "text-sm text-neutral-400" : "text-md"}>{text}</div>
{children && <div className="mt-2">{children}</div>}
{(callToAction || copyActions) && (
<div
className={twMerge(
"mt-2 flex h-7 w-full flex-row items-end gap-2",
callToAction ? "justify-between" : "justify-end"
)}
>
{callToAction}
{copyActions && (
<div className="flex h-7 flex-row items-center gap-2">
{copyActions.map((action) => (
<div className="flex flex-row items-center gap-2" key={`copy-${action.name}`}>
{action.label && (
<span className="ml-2 text-xs text-mineshaft-400">{action.label}</span>
)}
<CopyButton
value={action.value}
name={action.name}
size="xs"
variant="plain"
color="text-mineshaft-400"
icon={action.icon ?? faCopy}
/>
</div>
))}
</div>
)}
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,55 @@
import { faCheck, faCopy, IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { useTimedReset } from "@app/hooks";
import { IconButton } from "../IconButton";
import { Tooltip } from "../Tooltip";
export type CopyButtonProps = {
value: string;
size?: "xs" | "sm" | "md" | "lg";
variant?: "solid" | "outline" | "plain" | "star" | "outline_bg";
color?: string;
name?: string;
icon?: IconDefinition;
};
export const CopyButton = ({
value,
size = "sm",
variant = "solid",
color,
name,
icon = faCopy
}: CopyButtonProps) => {
const [copyText, isCopying, setCopyText] = useTimedReset<string>({
initialState: name ? `Copy ${name}` : "Copy to clipboard"
});
async function handleCopyText() {
setCopyText("Copied");
navigator.clipboard.writeText(value);
}
return (
<div>
<Tooltip content={copyText} size={size === "xs" || size === "sm" ? "sm" : "md"}>
<IconButton
ariaLabel={copyText}
variant={variant}
className={twMerge("group relative", color)}
size={size}
onClick={() => {
handleCopyText();
}}
>
<FontAwesomeIcon icon={isCopying ? faCheck : icon} />
</IconButton>
</Tooltip>
</div>
);
};
CopyButton.displayName = "CopyButton";

View File

@@ -0,0 +1,2 @@
export type { CopyButtonProps } from "./CopyButton";
export { CopyButton } from "./CopyButton";

View File

@@ -36,10 +36,12 @@ export const MultiValueRemove = (props: MultiValueRemoveProps) => {
export const Option = <T,>({ isSelected, children, ...props }: OptionProps<T>) => {
return (
<components.Option isSelected={isSelected} {...props}>
{children}
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
<div className="flex flex-row items-center justify-between">
<p className="truncate">{children}</p>
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</div>
</components.Option>
);
};

View File

@@ -13,6 +13,7 @@ export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "
position?: "top" | "bottom" | "left" | "right";
isDisabled?: boolean;
center?: boolean;
size?: "sm" | "md";
};
export const Tooltip = ({
@@ -26,6 +27,7 @@ export const Tooltip = ({
asChild = true,
isDisabled,
position = "top",
size = "md",
...props
}: TooltipProps) =>
// just render children if tooltip content is empty
@@ -43,7 +45,7 @@ export const Tooltip = ({
sideOffset={5}
{...props}
className={twMerge(
`z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md
`z-50 max-w-[15rem] select-none border border-mineshaft-600 bg-mineshaft-800 font-light text-bunker-200 shadow-md
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
@@ -51,6 +53,8 @@ export const Tooltip = ({
`,
isDisabled && "!hidden",
center && "text-center",
size === "sm" && "rounded-sm py-1 px-2 text-xs",
size === "md" && "rounded-md py-2 px-4 text-sm",
className
)}
>

View File

@@ -177,11 +177,21 @@ export const useGetProjectSecretsOverview = (
}),
onError: (error) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as { message: string };
const { message, requestId } = error.response?.data as {
message: string;
requestId: string;
};
createNotification({
title: "Error fetching secret details",
type: "error",
text: serverResponse.message
text: message,
copyActions: [
{
value: requestId,
name: "Request ID",
label: `Request ID: ${requestId}`
}
]
});
}
},
@@ -270,11 +280,21 @@ export const useGetProjectSecretsDetails = (
}),
onError: (error) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as { message: string };
const { message, requestId } = error.response?.data as {
message: string;
requestId: string;
};
createNotification({
title: "Error fetching secret details",
type: "error",
text: serverResponse.message
text: message,
copyActions: [
{
value: requestId,
name: "Request ID",
label: `Request ID: ${requestId}`
}
]
});
}
},
@@ -355,11 +375,21 @@ export const useGetProjectSecretsQuickSearch = (
}),
onError: (error) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as { message: string };
const { message, requestId } = error.response?.data as {
message: string;
requestId: string;
};
createNotification({
title: "Error fetching secrets deep search",
type: "error",
text: serverResponse.message
text: message,
copyActions: [
{
value: requestId,
name: "Request ID",
label: `Request ID: ${requestId}`
}
]
});
}
},

View File

@@ -1,5 +1,6 @@
export {
useAddExternalKms,
useExternalKmsFetchGcpKeys,
useLoadProjectKmsBackup,
useRemoveExternalKms,
useUpdateExternalKms,

View File

@@ -3,7 +3,13 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { kmsKeys } from "./queries";
import { AddExternalKmsType, KmsType } from "./types";
import {
AddExternalKmsType,
ExternalKmsGcpSchemaType,
KmsGcpKeyFetchAuthType,
KmsType,
UpdateExternalKmsType
} from "./types";
export const useAddExternalKms = (orgId: string) => {
const queryClient = useQueryClient();
@@ -33,7 +39,7 @@ export const useUpdateExternalKms = (orgId: string) => {
provider
}: {
kmsId: string;
} & AddExternalKmsType) => {
} & UpdateExternalKmsType) => {
const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, {
name,
description,
@@ -96,3 +102,44 @@ export const useLoadProjectKmsBackup = (projectId: string) => {
}
});
};
export const useExternalKmsFetchGcpKeys = (orgId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
gcpRegion,
...rest
}: Pick<ExternalKmsGcpSchemaType, "gcpRegion"> &
(
| (Pick<ExternalKmsGcpSchemaType, KmsGcpKeyFetchAuthType.Credential> & {
[KmsGcpKeyFetchAuthType.Kms]?: never;
})
| {
[KmsGcpKeyFetchAuthType.Kms]: string;
[KmsGcpKeyFetchAuthType.Credential]?: never;
}
)): Promise<{ keys: string[] }> => {
const {
[KmsGcpKeyFetchAuthType.Credential]: credential,
[KmsGcpKeyFetchAuthType.Kms]: kmsId
} = rest;
if ((credential && kmsId) || (!credential && !kmsId)) {
throw new Error(
`Either '${KmsGcpKeyFetchAuthType.Credential}' or '${KmsGcpKeyFetchAuthType.Kms}' must be provided, but not both.`
);
}
const { data } = await apiRequest.post("/api/v1/external-kms/gcp/keys", {
authMethod: credential ? KmsGcpKeyFetchAuthType.Credential : KmsGcpKeyFetchAuthType.Kms,
region: gcpRegion,
...rest
});
return data;
},
onSuccess: () => {
queryClient.invalidateQueries(kmsKeys.getExternalKmsList(orgId));
}
});
};

View File

@@ -35,7 +35,8 @@ export enum KmsType {
}
export enum ExternalKmsProvider {
AWS = "aws"
Aws = "aws",
Gcp = "gcp"
}
export const INTERNAL_KMS_KEY_ID = "internal";
@@ -44,6 +45,10 @@ export enum KmsAwsCredentialType {
AssumeRole = "assume-role",
AccessKey = "access-key"
}
// Google uses snake_case for their enum values and we need to match that
export enum KmsGcpCredentialType {
ServiceAccount = "service_account"
}
export const ExternalKmsAwsSchema = z.object({
credential: z
@@ -83,8 +88,34 @@ export const ExternalKmsAwsSchema = z.object({
)
});
export const ExternalKmsGcpCredentialSchema = z.object({
type: z.literal(KmsGcpCredentialType.ServiceAccount),
project_id: z.string().min(1),
private_key_id: z.string().min(1),
private_key: z.string().min(1),
client_email: z.string().min(1),
client_id: z.string().min(1),
auth_uri: z.string().min(1),
token_uri: z.string().min(1),
auth_provider_x509_cert_url: z.string().min(1),
client_x509_cert_url: z.string().min(1),
universe_domain: z.string().min(1)
});
export type ExternalKmsGcpCredentialSchemaType = z.infer<typeof ExternalKmsGcpCredentialSchema>;
export const ExternalKmsGcpSchema = z.object({
credential: ExternalKmsGcpCredentialSchema.describe(
"GCP Service Account JSON credential to connect"
),
gcpRegion: z.string().min(1).trim().describe("GCP region where the KMS key is located"),
keyName: z.string().min(1).trim().describe("GCP key name")
});
export type ExternalKmsGcpSchemaType = z.infer<typeof ExternalKmsGcpSchema>;
export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(ExternalKmsProvider.AWS), inputs: ExternalKmsAwsSchema })
z.object({ type: z.literal(ExternalKmsProvider.Aws), inputs: ExternalKmsAwsSchema }),
z.object({ type: z.literal(ExternalKmsProvider.Gcp), inputs: ExternalKmsGcpSchema })
]);
export const AddExternalKmsSchema = z.object({
@@ -100,3 +131,71 @@ export const AddExternalKmsSchema = z.object({
});
export type AddExternalKmsType = z.infer<typeof AddExternalKmsSchema>;
// we need separate schema for update because the credential field is not required on GCP
export const ExternalKmsUpdateInputSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(ExternalKmsProvider.Aws), inputs: ExternalKmsAwsSchema }),
z.object({
type: z.literal(ExternalKmsProvider.Gcp),
inputs: ExternalKmsGcpSchema.pick({ gcpRegion: true, keyName: true })
})
]);
export const UpdateExternalKmsSchema = z.object({
name: z
.string()
.trim()
.min(1)
.refine((v) => slugify(v) === v, {
message: "Alias must be a valid slug"
}),
description: z.string().trim().optional(),
provider: ExternalKmsUpdateInputSchema
});
export type UpdateExternalKmsType = z.infer<typeof UpdateExternalKmsSchema>;
const GCP_CREDENTIAL_MAX_FILE_SIZE = 8 * 1024; // 8KB
const GCP_CREDENTIAL_ACCEPTED_FILE_TYPES = ["application/json"];
const AddExternalKmsGcpFormSchemaStandardInputs = z.object({
keyObject: z
.object({ label: z.string().trim(), value: z.string().trim() })
.describe("GCP key name"),
gcpRegion: z.object({ label: z.string().trim(), value: z.string().trim() }).describe("GCP Region")
});
export const AddExternalKmsGcpFormSchema = z.discriminatedUnion("formType", [
z
.object({
formType: z.literal("newGcpKms"),
// `FileList` is a browser-only (window-specific) type, so we need to handle it differently on the server to avoid SSR errors
credentialFile:
typeof window === "undefined"
? z.any()
: z
.instanceof(FileList)
.refine((files) => files?.length === 1, "Image is required.")
.refine(
(files) => files?.[0]?.size <= GCP_CREDENTIAL_MAX_FILE_SIZE,
"Max file size is 8KB."
)
.refine(
(files) => GCP_CREDENTIAL_ACCEPTED_FILE_TYPES.includes(files?.[0]?.type),
"Only .json files are accepted."
)
})
.merge(AddExternalKmsGcpFormSchemaStandardInputs)
.merge(AddExternalKmsSchema.pick({ name: true, description: true })),
z
.object({ formType: z.literal("updateGcpKms") })
.merge(AddExternalKmsGcpFormSchemaStandardInputs)
.merge(AddExternalKmsSchema.pick({ name: true, description: true }))
]);
export type AddExternalKmsGcpFormSchemaType = z.infer<typeof AddExternalKmsGcpFormSchema>;
export enum KmsGcpKeyFetchAuthType {
Credential = "credential",
Kms = "kmsId"
}

View File

@@ -117,11 +117,21 @@ export const useGetProjectSecrets = ({
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
onError: (error) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as { message: string };
const { message, requestId } = error.response?.data as {
message: string;
requestId: string;
};
createNotification({
title: "Error fetching secrets",
type: "error",
text: serverResponse.message
text: message,
copyActions: [
{
value: requestId,
name: "Request ID",
label: `Request ID: ${requestId}`
}
]
});
}
},
@@ -148,15 +158,24 @@ export const useGetProjectSecretsAllEnv = ({
enabled: Boolean(workspaceId && environment),
onError: (error: unknown) => {
if (axios.isAxiosError(error) && !isErrorHandled) {
const serverResponse = error.response?.data as { message: string };
if (serverResponse.message !== ERROR_NOT_ALLOWED_READ_SECRETS) {
const { message, requestId } = error.response?.data as {
message: string;
requestId: string;
};
if (message !== ERROR_NOT_ALLOWED_READ_SECRETS) {
createNotification({
title: "Error fetching secrets",
type: "error",
text: serverResponse.message
text: message,
copyActions: [
{
value: requestId,
name: "Request ID",
label: `Request ID: ${requestId}`
}
]
});
}
setIsErrorHandled.on();
}
},

View File

@@ -54,7 +54,7 @@ export type TApiErrors =
requestId: string;
error: ApiErrorTypes.ValidationError;
message: ZodIssue[];
statusCode: 401;
statusCode: 422;
}
| {
requestId: string;

View File

@@ -32,13 +32,8 @@ export const queryClient = new QueryClient({
{
title: "Validation Error",
type: "error",
text: (
<div>
<p>Please check the input and try again.</p>
<p className="mt-2 text-xs">Request ID: {serverResponse.requestId}</p>
</div>
),
children: (
text: "Please check the input and try again.",
callToAction: (
<Modal>
<ModalTrigger>
<Button variant="outline_bg" size="xs">
@@ -66,7 +61,14 @@ export const queryClient = new QueryClient({
</TableContainer>
</ModalContent>
</Modal>
)
),
copyActions: [
{
value: serverResponse.requestId,
name: "Request ID",
label: `Request ID: ${serverResponse.requestId}`
}
]
},
{ closeOnClick: false }
);
@@ -77,9 +79,8 @@ export const queryClient = new QueryClient({
{
title: "Forbidden Access",
type: "error",
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`,
children: serverResponse?.details?.length ? (
text: `${serverResponse.message}.`,
callToAction: serverResponse?.details?.length ? (
<Modal>
<ModalTrigger>
<Button variant="outline_bg" size="xs">
@@ -165,7 +166,14 @@ export const queryClient = new QueryClient({
</div>
</ModalContent>
</Modal>
) : undefined
) : undefined,
copyActions: [
{
value: serverResponse.requestId,
name: "Request ID",
label: `Request ID: ${serverResponse.requestId}`
}
]
},
{ closeOnClick: false }
);
@@ -174,7 +182,14 @@ export const queryClient = new QueryClient({
createNotification({
title: "Bad Request",
type: "error",
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`
text: `${serverResponse.message}.`,
copyActions: [
{
value: serverResponse.requestId,
name: "Request ID",
label: `Request ID: ${serverResponse.requestId}`
}
]
});
}
}

View File

@@ -296,7 +296,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
status,
isActive
}) => {
const name = u && u.firstName ? `${u.firstName} ${u.lastName}` : "-";
const name =
u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : "-";
const email = u?.email || inviteEmail;
const username = u?.username ?? inviteEmail ?? "-";
return (

View File

@@ -123,7 +123,7 @@ export const UserPage = withPermission(
<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}`
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
: "-"}
</p>
{userId !== membership.user.id && (

View File

@@ -118,7 +118,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
<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}`
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
: "-"}
</p>
</div>

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import { faAws } from "@fortawesome/free-brands-svg-icons";
import { faAws, faGoogle } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
@@ -7,6 +7,7 @@ import { Modal, ModalContent } from "@app/components/v2";
import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
import { AwsKmsForm } from "./AwsKmsForm";
import { GcpKmsForm } from "./GcpKmsForm";
type Props = {
isOpen?: boolean;
@@ -21,8 +22,13 @@ enum WizardSteps {
const EXTERNAL_KMS_LIST = [
{
icon: faAws,
provider: ExternalKmsProvider.AWS,
provider: ExternalKmsProvider.Aws,
title: "AWS KMS"
},
{
icon: faGoogle,
provider: ExternalKmsProvider.Gcp,
title: "GCP KMS"
}
];
@@ -42,6 +48,7 @@ export const AddExternalKmsForm = ({ isOpen, onToggle }: Props) => {
title="Add a Key Management System"
subTitle="Configure an external key management system (KMS)"
className="my-4"
bodyClassName="overflow-visible"
>
<AnimatePresence exitBeforeEnter>
{wizardStep === WizardSteps.SelectProvider && (
@@ -79,7 +86,7 @@ export const AddExternalKmsForm = ({ isOpen, onToggle }: Props) => {
</motion.div>
)}
{wizardStep === WizardSteps.ProviderInputs &&
selectedProvider === ExternalKmsProvider.AWS && (
selectedProvider === ExternalKmsProvider.Aws && (
<motion.div
key="kms-aws"
transition={{ duration: 0.1 }}
@@ -90,6 +97,18 @@ export const AddExternalKmsForm = ({ isOpen, onToggle }: Props) => {
<AwsKmsForm onCancel={() => onToggle(false)} onCompleted={() => onToggle(false)} />
</motion.div>
)}
{wizardStep === WizardSteps.ProviderInputs &&
selectedProvider === ExternalKmsProvider.Gcp && (
<motion.div
key="kms-gcp"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<GcpKmsForm onCancel={() => onToggle(false)} onCompleted={() => onToggle(false)} />
</motion.div>
)}
</AnimatePresence>
</ModalContent>
</Modal>

View File

@@ -64,7 +64,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
name: kms?.name,
description: kms?.description ?? "",
provider: {
type: ExternalKmsProvider.AWS,
type: ExternalKmsProvider.Aws,
inputs: {
credential: {
type: kms?.external?.providerInput?.credential?.type,
@@ -88,7 +88,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
const selectedAwsAuthType = watch("provider.inputs.credential.type");
const handleAddAwsKms = async (data: AddExternalKmsType) => {
const handleAwsKmsFormSubmit = async (data: AddExternalKmsType) => {
const { name, description, provider } = data;
try {
if (kms) {
@@ -123,7 +123,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
};
return (
<form onSubmit={handleSubmit(handleAddAwsKms)} autoComplete="off">
<form onSubmit={handleSubmit(handleAwsKmsFormSubmit)} autoComplete="off">
<Controller
control={control}
name="name"

View File

@@ -0,0 +1,360 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications";
import { Badge, Button, FilterableSelect, FormControl, Input } from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useAddExternalKms,
useExternalKmsFetchGcpKeys,
useUpdateExternalKms
} from "@app/hooks/api";
import {
AddExternalKmsGcpFormSchema,
AddExternalKmsGcpFormSchemaType,
ExternalKmsGcpCredentialSchema,
ExternalKmsGcpCredentialSchemaType,
ExternalKmsProvider,
Kms
} from "@app/hooks/api/kms/types";
type Props = {
onCompleted: () => void;
onCancel: () => void;
kms?: Kms;
};
const GCP_REGIONS = [
{ label: "Johannesburg", value: "africa-south1" },
{ label: "Taiwan", value: "asia-east1" },
{ label: "Hong Kong", value: "asia-east2" },
{ label: "Tokyo", value: "asia-northeast1" },
{ label: "Osaka", value: "asia-northeast2" },
{ label: "Seoul", value: "asia-northeast3" },
{ label: "Mumbai", value: "asia-south1" },
{ label: "Delhi", value: "asia-south2" },
{ label: "Singapore", value: "asia-southeast1" },
{ label: "Jakarta", value: "asia-southeast2" },
{ label: "Sydney", value: "australia-southeast1" },
{ label: "Melbourne", value: "australia-southeast2" },
{ label: "Warsaw", value: "europe-central2" },
{ label: "Finland", value: "europe-north1" },
{ label: "Belgium", value: "europe-west1" },
{ label: "London", value: "europe-west2" },
{ label: "Frankfurt", value: "europe-west3" },
{ label: "Netherlands", value: "europe-west4" },
{ label: "Zurich", value: "europe-west6" },
{ label: "Milan", value: "europe-west8" },
{ label: "Paris", value: "europe-west9" },
{ label: "Berlin", value: "europe-west10" },
{ label: "Turin", value: "europe-west12" },
{ label: "Madrid", value: "europe-southwest1" },
{ label: "Doha", value: "me-central1" },
{ label: "Dammam", value: "me-central2" },
{ label: "Tel Aviv", value: "me-west1" },
{ label: "Montréal", value: "northamerica-northeast1" },
{ label: "Toronto", value: "northamerica-northeast2" },
{ label: "São Paulo", value: "southamerica-east1" },
{ label: "Santiago", value: "southamerica-west1" },
{ label: "Iowa", value: "us-central1" },
{ label: "South Carolina", value: "us-east1" },
{ label: "North Virginia", value: "us-east4" },
{ label: "Columbus", value: "us-east5" },
{ label: "Dallas", value: "us-south1" },
{ label: "Oregon", value: "us-west1" },
{ label: "Los Angeles", value: "us-west2" },
{ label: "Salt Lake City", value: "us-west3" },
{ label: "Las Vegas", value: "us-west4" }
];
const formatOptionLabel = ({ value, label }: { value: string; label: string }) => (
<div className="flex w-full flex-row items-center justify-between">
<span>{label}</span>
<Badge variant="success">{value}</Badge>
</div>
);
export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
const [isCredentialValid, setIsCredentialValid] = useState<boolean>(false);
const [keys, setKeys] = useState<{ value: string; label: string }[]>([]);
const {
control,
handleSubmit,
setError,
clearErrors,
getValues,
resetField,
setValue,
formState: { isSubmitting }
} = useForm<AddExternalKmsGcpFormSchemaType>({
resolver: zodResolver(AddExternalKmsGcpFormSchema),
defaultValues: {
formType: kms ? "updateGcpKms" : "newGcpKms",
name: kms?.name ?? "",
description: kms?.description ?? "",
gcpRegion: kms
? {
label:
GCP_REGIONS.find((r) => r.value === kms.external.providerInput.gcpRegion)?.label ??
"",
value: kms.external.providerInput.gcpRegion
}
: undefined,
keyObject: undefined
}
});
const { currentOrg } = useOrganization();
const { mutateAsync: addGcpExternalKms } = useAddExternalKms(currentOrg?.id!);
const { mutateAsync: updateGcpExternalKms } = useUpdateExternalKms(currentOrg?.id!);
const { mutateAsync: fetchGcpKeys, isLoading: isFetchGcpKeysLoading } =
useExternalKmsFetchGcpKeys(currentOrg?.id!);
// transforms the credential file into a JSON object
async function getCredentialFileJson(): Promise<ExternalKmsGcpCredentialSchemaType | null> {
const files = getValues("credentialFile");
if (!files || !files.length) {
return null;
}
const file = files[0];
if (file.type !== "application/json") {
setError("credentialFile", {
message: "Only .json files are accepted."
});
return null;
}
const jsonContents = await file.text();
const parsedJson = ExternalKmsGcpCredentialSchema.safeParse(JSON.parse(jsonContents));
if (!parsedJson.success) {
setError("credentialFile", {
message: "Invalid Service Account credential JSON."
});
return null;
}
clearErrors("credentialFile");
return parsedJson.data;
}
// handles the form submission
const handleGcpKmsFormSubmit = async (data: AddExternalKmsGcpFormSchemaType) => {
const { name, description, gcpRegion: gcpRegionObject, keyObject } = data;
const gcpRegion = gcpRegionObject.value;
if (!keys.find((k) => k.value === keyObject?.value)) {
setError("keyObject", {
message: "Please select a valid key."
});
resetField("keyObject");
return;
}
try {
if (kms) {
await updateGcpExternalKms({
kmsId: kms.id,
name,
description,
provider: {
type: ExternalKmsProvider.Gcp,
inputs: {
gcpRegion,
keyName: keyObject?.value
}
}
});
createNotification({
text: "Successfully updated GCP External KMS",
type: "success"
});
} else {
const credentialJson = await getCredentialFileJson();
if (!credentialJson) {
return;
}
await addGcpExternalKms({
name,
description,
provider: {
type: ExternalKmsProvider.Gcp,
inputs: {
gcpRegion,
keyName: keyObject?.value,
credential: credentialJson
}
}
});
createNotification({
text: "Successfully added GCP External KMS",
type: "success"
});
}
onCompleted();
} catch (err) {
console.error(err);
}
};
const fetchGCPKeys = async () => {
// @ts-expect-error - issue with the way react-select renders the placeholder. We need to set the value to null explicitly otherwise it will not re-render
setValue("keyObject", null);
setKeys([]);
const credentialJson = kms ? undefined : await getCredentialFileJson();
if (!kms && !credentialJson) {
return;
}
const gcpRegion = getValues("gcpRegion").value;
if (!gcpRegion.length) {
setError("gcpRegion", {
message: "Please select a GCP region to fetch GCP Keys."
});
return;
}
const res = await fetchGcpKeys({
gcpRegion,
...(kms ? { kmsId: kms.id } : { credential: credentialJson! })
});
setIsCredentialValid(!!res.keys);
const returnedKeys = res.keys.map((key) => {
const parts = key.split("/");
const keyLabel = `${parts[5]}/${parts[7]}`;
return {
value: key,
label: keyLabel
};
});
setKeys(returnedKeys);
if (kms) {
const existingKey = returnedKeys.find((k) => k.value === kms.external.providerInput.keyName);
if (existingKey) {
setValue("keyObject", existingKey);
}
}
};
const getPlaceholderText = () => {
if (isFetchGcpKeysLoading) {
return "Loading keys in this region...";
}
if (!isCredentialValid) {
return "Upload a valid credential file";
}
if (keys.length) {
return "Select a key";
}
return "No valid keys found in this region";
};
useEffect(() => {
if (kms && !isCredentialValid) {
fetchGCPKeys();
}
}, [kms]);
return (
<form onSubmit={handleSubmit(handleGcpKmsFormSubmit)} autoComplete="off">
<Controller
control={control}
name="name"
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="gcpRegion"
render={({ field, fieldState: { error } }) => (
<FormControl label="GCP Region" errorText={error?.message} isError={Boolean(error)}>
<FilterableSelect
className="w-full"
placeholder="Select a GCP region"
name="gcpRegion"
options={GCP_REGIONS}
value={field.value}
onChange={(e) => {
resetField("keyObject");
field.onChange(e);
fetchGCPKeys();
}}
formatOptionLabel={formatOptionLabel}
/>
</FormControl>
)}
/>
{!kms && (
<Controller
control={control}
name="credentialFile"
render={({ field: { value, onChange, ref, ...rest }, fieldState: { error } }) => (
<FormControl
label="Service Account Credential JSON"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...rest}
ref={ref}
type="file"
accept=".json"
placeholder=""
value={value?.filename}
onChange={(e) => {
onChange(e.target.files);
fetchGCPKeys();
}}
/>
</FormControl>
)}
/>
)}
<Controller
control={control}
name="keyObject"
render={({ field, fieldState: { error } }) => (
<FormControl label="GCP Key Name" errorText={error?.message} isError={Boolean(error)}>
<FilterableSelect
className="w-full"
placeholder={getPlaceholderText()}
isDisabled={!isCredentialValid || !keys.length}
name="key"
options={keys}
value={field.value}
onChange={field.onChange}
/>
</FormControl>
)}
/>
{kms && (
<span className="text-xs text-mineshaft-300">
To change your GCP credentials, create a new external KMS and assign it to project you
want to use it with.
</span>
)}
<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>
);
};

View File

@@ -1,4 +1,4 @@
import { faAws } from "@fortawesome/free-brands-svg-icons";
import { faAws, faGoogle } 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";
@@ -117,9 +117,12 @@ export const OrgEncryptionTab = withPermission(
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 && (
{kms.externalKms.provider === ExternalKmsProvider.Aws && (
<FontAwesomeIcon icon={faAws} />
)}
{kms.externalKms.provider === ExternalKmsProvider.Gcp && (
<FontAwesomeIcon icon={faGoogle} />
)}
<div className="ml-2">{kms.externalKms.provider.toUpperCase()}</div>
</Td>
<Td>{kms.name}</Td>

View File

@@ -3,6 +3,7 @@ import { useGetExternalKmsById } from "@app/hooks/api";
import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
import { AwsKmsForm } from "./AwsKmsForm";
import { GcpKmsForm } from "./GcpKmsForm";
type Props = {
isOpen: boolean;
@@ -14,15 +15,22 @@ export const UpdateExternalKmsForm = ({ isOpen, kmsId, onOpenChange }: Props) =>
const { data: externalKms, isLoading } = useGetExternalKmsById(kmsId);
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Edit configuration">
<ModalContent title="Edit configuration" bodyClassName="overflow-visible">
{isLoading && <ContentLoader />}
{externalKms?.external?.provider === ExternalKmsProvider.AWS && (
{externalKms?.external?.provider === ExternalKmsProvider.Aws && (
<AwsKmsForm
kms={externalKms}
onCancel={() => onOpenChange(false)}
onCompleted={() => onOpenChange(false)}
/>
)}
{externalKms?.external?.provider === ExternalKmsProvider.Gcp && (
<GcpKmsForm
kms={externalKms}
onCancel={() => onOpenChange(false)}
onCompleted={() => onOpenChange(false)}
/>
)}
</ModalContent>
</Modal>
);