Compare commits
21 Commits
integratio
...
misc/add-p
Author | SHA1 | Date | |
---|---|---|---|
|
0a1242db75 | ||
|
095b26c8c9 | ||
|
4ace30aecd | ||
|
8b2a866994 | ||
|
b4386af2e0 | ||
|
2b44e32ac1 | ||
|
ec5e6eb7b4 | ||
|
48cb5f6e9b | ||
|
0842901d4f | ||
|
32d6826ade | ||
|
a750f48922 | ||
|
67662686f3 | ||
|
11c96245a7 | ||
|
a63191e11d | ||
|
7a13c155f5 | ||
|
5ceb30f43f | ||
|
7728a4793b | ||
|
d3523ed1d6 | ||
|
35a9b2a38d | ||
|
16a9f8c194 | ||
|
9557639bfe |
@@ -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;
|
||||
},
|
||||
|
@@ -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();
|
||||
|
137
backend/package-lock.json
generated
@@ -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"
|
||||
},
|
||||
|
@@ -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",
|
||||
|
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -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;
|
||||
|
@@ -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 };
|
||||
};
|
||||
|
@@ -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 };
|
||||
};
|
||||
|
@@ -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);
|
||||
};
|
||||
|
||||
|
@@ -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 };
|
||||
};
|
||||
|
@@ -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);
|
||||
};
|
||||
|
||||
|
@@ -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);
|
||||
};
|
||||
|
||||
|
@@ -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 };
|
||||
};
|
||||
|
@@ -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)();
|
||||
};
|
||||
|
||||
|
@@ -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);
|
||||
};
|
||||
|
||||
|
@@ -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);
|
||||
};
|
||||
|
||||
|
@@ -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
|
||||
};
|
||||
};
|
||||
|
113
backend/src/ee/services/external-kms/providers/gcp-kms.ts
Normal 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
|
||||
};
|
||||
};
|
@@ -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>;
|
||||
|
||||
|
@@ -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(
|
||||
|
@@ -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();
|
||||
|
@@ -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
|
||||
};
|
||||
};
|
||||
|
@@ -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
|
||||
});
|
||||
|
@@ -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,
|
||||
|
@@ -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),
|
||||
|
@@ -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.");
|
||||
}
|
||||
|
@@ -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
|
||||
);
|
||||
|
@@ -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.">
|
||||

|
||||

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

|
||||

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

|
||||

|
||||
Choose 'AWS KMS' from the list of encryption providers.
|
||||
</Step>
|
||||
<Step title="Provide the inputs for AWS KMS">
|
||||
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.
|
||||
|
132
docs/documentation/platform/kms-configuration/gcp-kms.mdx
Normal 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.
|
||||

|
||||
|
||||
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**.
|
||||

|
||||
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**.
|
||||

|
||||
|
||||
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.">
|
||||

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

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

|
||||
Choose 'GCP KMS' from the list of encryption providers.
|
||||
</Step>
|
||||
<Step title="Provide the inputs for GCP KMS">
|
||||
|
||||

|
||||
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">
|
||||

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

|
||||
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>
|
@@ -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.
|
||||
|
Before Width: | Height: | Size: 348 KiB |
BIN
docs/images/platform/kms/encryption-modal-provider-select.png
Normal file
After Width: | Height: | Size: 590 KiB |
Before Width: | Height: | Size: 694 KiB After Width: | Height: | Size: 694 KiB |
Before Width: | Height: | Size: 482 KiB After Width: | Height: | Size: 482 KiB |
BIN
docs/images/platform/kms/gcp/gcp-add-modal-filled.png
Normal file
After Width: | Height: | Size: 611 KiB |
BIN
docs/images/platform/kms/gcp/keyring-create.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
docs/images/platform/kms/gcp/project-settings.png
Normal file
After Width: | Height: | Size: 978 KiB |
BIN
docs/images/platform/kms/gcp/select-gcp-kms-in-project.png
Normal file
After Width: | Height: | Size: 974 KiB |
BIN
docs/images/platform/kms/gcp/service-account-form.png
Normal file
After Width: | Height: | Size: 122 KiB |
BIN
docs/images/platform/kms/gcp/service-account-permissions.png
Normal file
After Width: | Height: | Size: 122 KiB |
@@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@@ -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.
|
||||
*/}
|
@@ -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">
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
55
frontend/src/components/v2/CopyButton/CopyButton.tsx
Normal 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";
|
2
frontend/src/components/v2/CopyButton/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export type { CopyButtonProps } from "./CopyButton";
|
||||
export { CopyButton } from "./CopyButton";
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
@@ -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
|
||||
)}
|
||||
>
|
||||
|
@@ -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}`
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@@ -1,5 +1,6 @@
|
||||
export {
|
||||
useAddExternalKms,
|
||||
useExternalKmsFetchGcpKeys,
|
||||
useLoadProjectKmsBackup,
|
||||
useRemoveExternalKms,
|
||||
useUpdateExternalKms,
|
||||
|
@@ -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));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
},
|
||||
|
@@ -54,7 +54,7 @@ export type TApiErrors =
|
||||
requestId: string;
|
||||
error: ApiErrorTypes.ValidationError;
|
||||
message: ZodIssue[];
|
||||
statusCode: 401;
|
||||
statusCode: 422;
|
||||
}
|
||||
| {
|
||||
requestId: string;
|
||||
|
@@ -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}`
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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 (
|
||||
|
@@ -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 && (
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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"
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
|
@@ -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>
|
||||
);
|
||||
|