Compare commits
137 Commits
octopus-de
...
misc/add-p
Author | SHA1 | Date | |
---|---|---|---|
|
0a1242db75 | ||
|
095b26c8c9 | ||
|
fcdfcd0219 | ||
|
4ace30aecd | ||
|
8b2a866994 | ||
|
b4386af2e0 | ||
|
2b44e32ac1 | ||
|
ec5e6eb7b4 | ||
|
48cb5f6e9b | ||
|
0842901d4f | ||
|
32d6826ade | ||
|
a750f48922 | ||
|
67662686f3 | ||
|
11c96245a7 | ||
|
a63191e11d | ||
|
7a13c155f5 | ||
|
5ceb30f43f | ||
|
7728a4793b | ||
|
d3523ed1d6 | ||
|
35a9b2a38d | ||
|
16a9f8c194 | ||
|
9557639bfe | ||
|
1049f95952 | ||
|
e618d5ca5f | ||
|
d659250ce8 | ||
|
87363eabfe | ||
|
d1b9c316d8 | ||
|
b9867c0d06 | ||
|
afa2f383c5 | ||
|
39f7354fec | ||
|
c46c0cb1e8 | ||
|
6905ffba4e | ||
|
64fd423c61 | ||
|
da1a7466d1 | ||
|
d3f3f34129 | ||
|
c8fba7ce4c | ||
|
82c3e943eb | ||
|
dc3903ff15 | ||
|
a9c01dcf1f | ||
|
ae51fbb8f2 | ||
|
62910e93ca | ||
|
586b9d9a56 | ||
|
9e3c632a1f | ||
|
bb094f60c1 | ||
|
6d709fba62 | ||
|
27beca7099 | ||
|
28e7e4c52d | ||
|
cfc0ca1f03 | ||
|
b96593d0ab | ||
|
2de5896ba4 | ||
|
3455ad3898 | ||
|
c7a32a3b05 | ||
|
1ebfed8c11 | ||
|
a18f3c2919 | ||
|
16d215b588 | ||
|
a852b15a1e | ||
|
cacd9041b0 | ||
|
cfeffebd46 | ||
|
1dceedcdb4 | ||
|
14f03c38c3 | ||
|
be9f096e75 | ||
|
49133a044f | ||
|
b7fe3743db | ||
|
c5fded361c | ||
|
e676acbadf | ||
|
9b31a7bbb1 | ||
|
345be85825 | ||
|
f82b11851a | ||
|
b466b3073b | ||
|
46105fc315 | ||
|
3cf8fd2ff8 | ||
|
5277a50b3e | ||
|
dab8f0b261 | ||
|
4293665130 | ||
|
8afa65c272 | ||
|
4c739fd57f | ||
|
bcc2840020 | ||
|
8b3af92d23 | ||
|
9ca58894f0 | ||
|
d131314de0 | ||
|
9c03144f19 | ||
|
5495ffd78e | ||
|
a200469c72 | ||
|
85c3074216 | ||
|
cfc55ff283 | ||
|
7179b7a540 | ||
|
6c4cb5e084 | ||
|
9cfb044178 | ||
|
105eb70fd9 | ||
|
18a2547b24 | ||
|
588b3c77f9 | ||
|
a04834c7c9 | ||
|
9df9f4a5da | ||
|
afdc704423 | ||
|
57261cf0c8 | ||
|
06f6004993 | ||
|
f3bfb9cc5a | ||
|
48fb77be49 | ||
|
c3956c60e9 | ||
|
f55bcb93ba | ||
|
d3fb2a6a74 | ||
|
6a23b74481 | ||
|
602cf4b3c4 | ||
|
84ff71fef2 | ||
|
add5742b8c | ||
|
68f3964206 | ||
|
90374971ae | ||
|
3a1eadba8c | ||
|
5305017ce2 | ||
|
cf5f49d14e | ||
|
4f4b5be8ea | ||
|
ecea79f040 | ||
|
586b901318 | ||
|
ad8d247cdc | ||
|
3b47d7698b | ||
|
aa9a86df71 | ||
|
33411335ed | ||
|
ca55f19926 | ||
|
3794521c56 | ||
|
728f023263 | ||
|
229706f57f | ||
|
6cf2488326 | ||
|
2c402fbbb6 | ||
|
92ce05283b | ||
|
39d92ce6ff | ||
|
44a026446e | ||
|
bbf52c9a48 | ||
|
539e5b1907 | ||
|
44b02d5324 | ||
|
3d6ea3251e | ||
|
be39e63832 | ||
|
464a3ccd53 | ||
|
46ad1d47a9 | ||
|
63fac39fff | ||
|
7c62a776fb | ||
|
ed7fc0e5cd | ||
|
1ae6213387 |
@@ -74,8 +74,8 @@ CAPTCHA_SECRET=
|
|||||||
|
|
||||||
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
||||||
|
|
||||||
OTEL_TELEMETRY_COLLECTION_ENABLED=
|
OTEL_TELEMETRY_COLLECTION_ENABLED=false
|
||||||
OTEL_EXPORT_TYPE=
|
OTEL_EXPORT_TYPE=prometheus
|
||||||
OTEL_EXPORT_OTLP_ENDPOINT=
|
OTEL_EXPORT_OTLP_ENDPOINT=
|
||||||
OTEL_OTLP_PUSH_INTERVAL=
|
OTEL_OTLP_PUSH_INTERVAL=
|
||||||
|
|
||||||
|
@@ -10,12 +10,15 @@ export const mockQueue = (): TQueueServiceFactory => {
|
|||||||
queue: async (name, jobData) => {
|
queue: async (name, jobData) => {
|
||||||
job[name] = jobData;
|
job[name] = jobData;
|
||||||
},
|
},
|
||||||
|
queuePg: async () => {},
|
||||||
|
initialize: async () => {},
|
||||||
shutdown: async () => undefined,
|
shutdown: async () => undefined,
|
||||||
stopRepeatableJob: async () => true,
|
stopRepeatableJob: async () => true,
|
||||||
start: (name, jobFn) => {
|
start: (name, jobFn) => {
|
||||||
queues[name] = jobFn;
|
queues[name] = jobFn;
|
||||||
workers[name] = jobFn;
|
workers[name] = jobFn;
|
||||||
},
|
},
|
||||||
|
startPg: async () => {},
|
||||||
listen: (name, event) => {
|
listen: (name, event) => {
|
||||||
events[name] = event;
|
events[name] = event;
|
||||||
},
|
},
|
||||||
|
86
backend/e2e-test/routes/v3/secret-recursive.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { createFolder, deleteFolder } from "e2e-test/testUtils/folders";
|
||||||
|
import { createSecretV2, deleteSecretV2, getSecretsV2 } from "e2e-test/testUtils/secrets";
|
||||||
|
|
||||||
|
import { seedData1 } from "@app/db/seed-data";
|
||||||
|
|
||||||
|
describe("Secret Recursive Testing", async () => {
|
||||||
|
const projectId = seedData1.projectV3.id;
|
||||||
|
const folderAndSecretNames = [
|
||||||
|
{ name: "deep1", path: "/", expectedSecretCount: 4 },
|
||||||
|
{ name: "deep21", path: "/deep1", expectedSecretCount: 2 },
|
||||||
|
{ name: "deep3", path: "/deep1/deep2", expectedSecretCount: 1 },
|
||||||
|
{ name: "deep22", path: "/deep2", expectedSecretCount: 1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const rootFolderIds: string[] = [];
|
||||||
|
for (const folder of folderAndSecretNames) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const createdFolder = await createFolder({
|
||||||
|
authToken: jwtAuthToken,
|
||||||
|
environmentSlug: "prod",
|
||||||
|
workspaceId: projectId,
|
||||||
|
secretPath: folder.path,
|
||||||
|
name: folder.name
|
||||||
|
});
|
||||||
|
|
||||||
|
if (folder.path === "/") {
|
||||||
|
rootFolderIds.push(createdFolder.id);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await createSecretV2({
|
||||||
|
secretPath: folder.path,
|
||||||
|
authToken: jwtAuthToken,
|
||||||
|
environmentSlug: "prod",
|
||||||
|
workspaceId: projectId,
|
||||||
|
key: folder.name,
|
||||||
|
value: folder.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
await Promise.all(
|
||||||
|
rootFolderIds.map((id) =>
|
||||||
|
deleteFolder({
|
||||||
|
authToken: jwtAuthToken,
|
||||||
|
secretPath: "/",
|
||||||
|
id,
|
||||||
|
workspaceId: projectId,
|
||||||
|
environmentSlug: "prod"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await deleteSecretV2({
|
||||||
|
authToken: jwtAuthToken,
|
||||||
|
secretPath: "/",
|
||||||
|
workspaceId: projectId,
|
||||||
|
environmentSlug: "prod",
|
||||||
|
key: folderAndSecretNames[0].name
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each(folderAndSecretNames)("$path recursive secret fetching", async ({ path, expectedSecretCount }) => {
|
||||||
|
const secrets = await getSecretsV2({
|
||||||
|
authToken: jwtAuthToken,
|
||||||
|
secretPath: path,
|
||||||
|
workspaceId: projectId,
|
||||||
|
environmentSlug: "prod",
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(secrets.secrets.length).toEqual(expectedSecretCount);
|
||||||
|
expect(secrets.secrets.sort((a, b) => a.secretKey.localeCompare(b.secretKey))).toEqual(
|
||||||
|
folderAndSecretNames
|
||||||
|
.filter((el) => el.path.startsWith(path))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((el) =>
|
||||||
|
expect.objectContaining({
|
||||||
|
secretKey: el.name,
|
||||||
|
secretValue: el.name
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@@ -97,6 +97,7 @@ export const getSecretsV2 = async (dto: {
|
|||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
authToken: string;
|
authToken: string;
|
||||||
|
recursive?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const getSecretsResponse = await testServer.inject({
|
const getSecretsResponse = await testServer.inject({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -109,7 +110,8 @@ export const getSecretsV2 = async (dto: {
|
|||||||
environment: dto.environmentSlug,
|
environment: dto.environmentSlug,
|
||||||
secretPath: dto.secretPath,
|
secretPath: dto.secretPath,
|
||||||
expandSecretReferences: "true",
|
expandSecretReferences: "true",
|
||||||
include_imports: "true"
|
include_imports: "true",
|
||||||
|
recursive: String(dto.recursive || false)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(getSecretsResponse.statusCode).toBe(200);
|
expect(getSecretsResponse.statusCode).toBe(200);
|
||||||
|
@@ -53,7 +53,7 @@ export default {
|
|||||||
extension: "ts"
|
extension: "ts"
|
||||||
});
|
});
|
||||||
const smtp = mockSmtpServer();
|
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 keyStore = keyStoreFactory(cfg.REDIS_URL);
|
||||||
|
|
||||||
const hsmModule = initializeHsmModule();
|
const hsmModule = initializeHsmModule();
|
||||||
|
137
backend/package-lock.json
generated
@@ -28,6 +28,7 @@
|
|||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
"@fastify/swagger": "^8.14.0",
|
"@fastify/swagger": "^8.14.0",
|
||||||
"@fastify/swagger-ui": "^2.1.0",
|
"@fastify/swagger-ui": "^2.1.0",
|
||||||
|
"@google-cloud/kms": "^4.5.0",
|
||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@octokit/auth-app": "^7.1.1",
|
"@octokit/auth-app": "^7.1.1",
|
||||||
"@octokit/plugin-retry": "^5.0.5",
|
"@octokit/plugin-retry": "^5.0.5",
|
||||||
@@ -92,6 +93,7 @@
|
|||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"passport-ldapauth": "^3.0.1",
|
"passport-ldapauth": "^3.0.1",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
|
"pg-boss": "^10.1.5",
|
||||||
"pg-query-stream": "^4.5.3",
|
"pg-query-stream": "^4.5.3",
|
||||||
"picomatch": "^3.0.1",
|
"picomatch": "^3.0.1",
|
||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
@@ -5598,6 +5600,18 @@
|
|||||||
"yaml": "^2.2.2"
|
"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": {
|
"node_modules/@google-cloud/paginator": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
"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": {
|
"node_modules/bullmq": {
|
||||||
"version": "5.4.2",
|
"version": "5.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.4.2.tgz",
|
||||||
@@ -15086,6 +15092,44 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"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": {
|
"node_modules/googleapis": {
|
||||||
"version": "137.1.0",
|
"version": "137.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz",
|
||||||
@@ -18185,11 +18229,6 @@
|
|||||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||||
"license": "BlueOak-1.0.0"
|
"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": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -18408,15 +18447,13 @@
|
|||||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||||
},
|
},
|
||||||
"node_modules/pg": {
|
"node_modules/pg": {
|
||||||
"version": "8.11.3",
|
"version": "8.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz",
|
||||||
"integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==",
|
"integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-writer": "2.0.0",
|
"pg-connection-string": "^2.7.0",
|
||||||
"packet-reader": "1.0.0",
|
"pg-pool": "^3.7.0",
|
||||||
"pg-connection-string": "^2.6.2",
|
"pg-protocol": "^1.7.0",
|
||||||
"pg-pool": "^3.6.1",
|
|
||||||
"pg-protocol": "^1.6.0",
|
|
||||||
"pg-types": "^2.1.0",
|
"pg-types": "^2.1.0",
|
||||||
"pgpass": "1.x"
|
"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": {
|
"node_modules/pg-cloudflare": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
|
||||||
@@ -18471,17 +18521,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pg-pool": {
|
"node_modules/pg-pool": {
|
||||||
"version": "3.6.1",
|
"version": "3.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz",
|
||||||
"integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==",
|
"integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"pg": ">=8.0"
|
"pg": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pg-protocol": {
|
"node_modules/pg-protocol": {
|
||||||
"version": "1.6.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz",
|
||||||
"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q=="
|
"integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ=="
|
||||||
},
|
},
|
||||||
"node_modules/pg-query-stream": {
|
"node_modules/pg-query-stream": {
|
||||||
"version": "4.5.3",
|
"version": "4.5.3",
|
||||||
@@ -18510,9 +18560,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pg/node_modules/pg-connection-string": {
|
"node_modules/pg/node_modules/pg-connection-string": {
|
||||||
"version": "2.6.2",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz",
|
||||||
"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
|
"integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA=="
|
||||||
},
|
},
|
||||||
"node_modules/pgpass": {
|
"node_modules/pgpass": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
@@ -19223,6 +19273,18 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/protobufjs": {
|
||||||
"version": "7.4.0",
|
"version": "7.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
|
||||||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
"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": {
|
"node_modules/serve-static": {
|
||||||
"version": "1.16.2",
|
"version": "1.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||||
@@ -22130,7 +22206,6 @@
|
|||||||
"version": "0.20.2",
|
"version": "0.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
|
@@ -136,6 +136,7 @@
|
|||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
"@fastify/swagger": "^8.14.0",
|
"@fastify/swagger": "^8.14.0",
|
||||||
"@fastify/swagger-ui": "^2.1.0",
|
"@fastify/swagger-ui": "^2.1.0",
|
||||||
|
"@google-cloud/kms": "^4.5.0",
|
||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@octokit/auth-app": "^7.1.1",
|
"@octokit/auth-app": "^7.1.1",
|
||||||
"@octokit/plugin-retry": "^5.0.5",
|
"@octokit/plugin-retry": "^5.0.5",
|
||||||
@@ -200,6 +201,7 @@
|
|||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"passport-ldapauth": "^3.0.1",
|
"passport-ldapauth": "^3.0.1",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
|
"pg-boss": "^10.1.5",
|
||||||
"pg-query-stream": "^4.5.3",
|
"pg-query-stream": "^4.5.3",
|
||||||
"picomatch": "^3.0.1",
|
"picomatch": "^3.0.1",
|
||||||
"pino": "^8.16.2",
|
"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 { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import {
|
import {
|
||||||
ExternalKmsAwsSchema,
|
ExternalKmsAwsSchema,
|
||||||
|
ExternalKmsGcpCredentialSchema,
|
||||||
|
ExternalKmsGcpSchema,
|
||||||
ExternalKmsInputSchema,
|
ExternalKmsInputSchema,
|
||||||
ExternalKmsInputUpdateSchema
|
ExternalKmsInputUpdateSchema,
|
||||||
|
KmsGcpKeyFetchAuthType,
|
||||||
|
KmsProviders,
|
||||||
|
TExternalKmsGcpCredentialSchema
|
||||||
} from "@app/ee/services/external-kms/providers/model";
|
} from "@app/ee/services/external-kms/providers/model";
|
||||||
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@@ -44,7 +50,8 @@ const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({
|
|||||||
statusDetails: true,
|
statusDetails: true,
|
||||||
provider: true
|
provider: true
|
||||||
}).extend({
|
}).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 };
|
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 { RawAxiosRequestHeaders } from "axios";
|
||||||
|
|
||||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
@@ -20,27 +21,130 @@ type TAuditLogQueueServiceFactoryDep = {
|
|||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
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
|
// 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
|
// audit log is a crowded queue thus needs to be fast
|
||||||
export const AUDIT_LOG_STREAM_TIMEOUT = 5 * 1000;
|
export const AUDIT_LOG_STREAM_TIMEOUT = 5 * 1000;
|
||||||
export const auditLogQueueServiceFactory = ({
|
|
||||||
|
export const auditLogQueueServiceFactory = async ({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
auditLogStreamDAL
|
auditLogStreamDAL
|
||||||
}: TAuditLogQueueServiceFactoryDep) => {
|
}: TAuditLogQueueServiceFactoryDep) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const pushToLog = async (data: TCreateAuditLogDTO) => {
|
const pushToLog = async (data: TCreateAuditLogDTO) => {
|
||||||
await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, {
|
if (appCfg.USE_PG_QUEUE && appCfg.SHOULD_INIT_PG_QUEUE) {
|
||||||
removeOnFail: {
|
await queueService.queuePg<QueueName.AuditLog>(QueueJobs.AuditLog, data, {
|
||||||
count: 3
|
retryLimit: 10,
|
||||||
},
|
retryBackoff: true
|
||||||
removeOnComplete: 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) => {
|
queueService.start(QueueName.AuditLog, async (job) => {
|
||||||
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
|
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
|
||||||
let { orgId } = job.data;
|
let { orgId } = job.data;
|
||||||
|
@@ -112,7 +112,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
})
|
})
|
||||||
) as object;
|
) as object;
|
||||||
|
|
||||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
const selectedTTL = ttl || dynamicSecretCfg.defaultTTL;
|
||||||
const { maxTTL } = dynamicSecretCfg;
|
const { maxTTL } = dynamicSecretCfg;
|
||||||
const expireAt = new Date(new Date().getTime() + ms(selectedTTL));
|
const expireAt = new Date(new Date().getTime() + ms(selectedTTL));
|
||||||
if (maxTTL) {
|
if (maxTTL) {
|
||||||
@@ -187,7 +187,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
})
|
})
|
||||||
) as object;
|
) as object;
|
||||||
|
|
||||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
const selectedTTL = ttl || dynamicSecretCfg.defaultTTL;
|
||||||
const { maxTTL } = dynamicSecretCfg;
|
const { maxTTL } = dynamicSecretCfg;
|
||||||
const expireAt = new Date(dynamicSecretLease.expireAt.getTime() + ms(selectedTTL));
|
const expireAt = new Date(dynamicSecretLease.expireAt.getTime() + ms(selectedTTL));
|
||||||
if (maxTTL) {
|
if (maxTTL) {
|
||||||
|
@@ -127,7 +127,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin
|
|||||||
};
|
};
|
||||||
|
|
||||||
const generatePassword = () => {
|
const generatePassword = () => {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||||
return customAlphabet(charset, 64)();
|
return customAlphabet(charset, 64)();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
const renew = async (_inputs: unknown, entityId: string) => {
|
||||||
// No renewal necessary
|
// No renewal necessary
|
||||||
return { entityId };
|
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 MSFT_LOGIN_URL = "https://login.microsoftonline.com";
|
||||||
|
|
||||||
const generatePassword = () => {
|
const generatePassword = () => {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||||
return customAlphabet(charset, 64)();
|
return customAlphabet(charset, 64)();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
|||||||
return users;
|
return users;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
const renew = async (_inputs: unknown, entityId: string) => {
|
||||||
// No renewal necessary
|
// No renewal necessary
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
@@ -9,7 +9,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
|||||||
import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models";
|
import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
const generatePassword = (size = 48) => {
|
const generatePassword = (size = 48) => {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||||
return customAlphabet(charset, 48)(size);
|
return customAlphabet(charset, 48)(size);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
|||||||
import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models";
|
import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
const generatePassword = () => {
|
const generatePassword = () => {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||||
return customAlphabet(charset, 64)();
|
return customAlphabet(charset, 64)();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
|||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
const renew = async (_inputs: unknown, entityId: string) => {
|
||||||
// No renewal necessary
|
// No renewal necessary
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
@@ -8,7 +8,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
|||||||
import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models";
|
import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
const generatePassword = (size = 48) => {
|
const generatePassword = (size = 48) => {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||||
return customAlphabet(charset, 48)(size);
|
return customAlphabet(charset, 48)(size);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
|||||||
import { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models";
|
import { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
const generatePassword = (size = 48) => {
|
const generatePassword = (size = 48) => {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||||
return customAlphabet(charset, 48)(size);
|
return customAlphabet(charset, 48)(size);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -11,7 +11,7 @@ import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
|||||||
import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models";
|
import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
const generatePassword = () => {
|
const generatePassword = () => {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||||
return customAlphabet(charset, 64)();
|
return customAlphabet(charset, 64)();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
|||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
const renew = async (_inputs: unknown, entityId: string) => {
|
||||||
// No renewal necessary
|
// No renewal necessary
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
@@ -10,7 +10,7 @@ import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
|||||||
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
|
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
const generatePassword = () => {
|
const generatePassword = () => {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||||
return customAlphabet(charset, 64)();
|
return customAlphabet(charset, 64)();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -12,7 +12,7 @@ import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models";
|
|||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
const generatePassword = (size = 48) => {
|
const generatePassword = (size = 48) => {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||||
return customAlphabet(charset, 48)(size);
|
return customAlphabet(charset, 48)(size);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -14,7 +14,7 @@ const generatePassword = (provider: SqlProviders) => {
|
|||||||
// oracle has limit of 48 password length
|
// oracle has limit of 48 password length
|
||||||
const size = provider === SqlProviders.Oracle ? 30 : 48;
|
const size = provider === SqlProviders.Oracle ? 30 : 48;
|
||||||
|
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||||
return customAlphabet(charset, 48)(size);
|
return customAlphabet(charset, 48)(size);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -20,7 +20,8 @@ import {
|
|||||||
TUpdateExternalKmsDTO
|
TUpdateExternalKmsDTO
|
||||||
} from "./external-kms-types";
|
} from "./external-kms-types";
|
||||||
import { AwsKmsProviderFactory } from "./providers/aws-kms";
|
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 = {
|
type TExternalKmsServiceFactoryDep = {
|
||||||
externalKmsDAL: TExternalKmsDALFactory;
|
externalKmsDAL: TExternalKmsDALFactory;
|
||||||
@@ -78,6 +79,13 @@ export const externalKmsServiceFactory = ({
|
|||||||
await externalKms.validateConnection();
|
await externalKms.validateConnection();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case KmsProviders.Gcp:
|
||||||
|
{
|
||||||
|
const externalKms = await GcpKmsProviderFactory({ inputs: provider.inputs });
|
||||||
|
await externalKms.validateConnection();
|
||||||
|
sanitizedProviderInput = JSON.stringify(provider.inputs);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
throw new BadRequestError({ message: "external kms provided is invalid" });
|
||||||
}
|
}
|
||||||
@@ -88,7 +96,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { cipherTextBlob: encryptedProviderInputs } = orgDataKeyEncryptor({
|
const { cipherTextBlob: encryptedProviderInputs } = orgDataKeyEncryptor({
|
||||||
plainText: Buffer.from(sanitizedProviderInput, "utf8")
|
plainText: Buffer.from(sanitizedProviderInput)
|
||||||
});
|
});
|
||||||
|
|
||||||
const externalKms = await externalKmsDAL.transaction(async (tx) => {
|
const externalKms = await externalKmsDAL.transaction(async (tx) => {
|
||||||
@@ -162,7 +170,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
case KmsProviders.Aws:
|
case KmsProviders.Aws:
|
||||||
{
|
{
|
||||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||||
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
JSON.parse(decryptedProviderInputBlob.toString())
|
||||||
);
|
);
|
||||||
const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs };
|
const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs };
|
||||||
const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput });
|
const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput });
|
||||||
@@ -170,6 +178,17 @@ export const externalKmsServiceFactory = ({
|
|||||||
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
|
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
|
||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
throw new BadRequestError({ message: "external kms provided is invalid" });
|
||||||
}
|
}
|
||||||
@@ -178,7 +197,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
let encryptedProviderInputs: Buffer | undefined;
|
let encryptedProviderInputs: Buffer | undefined;
|
||||||
if (sanitizedProviderInput) {
|
if (sanitizedProviderInput) {
|
||||||
const { cipherTextBlob } = orgDataKeyEncryptor({
|
const { cipherTextBlob } = orgDataKeyEncryptor({
|
||||||
plainText: Buffer.from(sanitizedProviderInput, "utf8")
|
plainText: Buffer.from(sanitizedProviderInput)
|
||||||
});
|
});
|
||||||
encryptedProviderInputs = cipherTextBlob;
|
encryptedProviderInputs = cipherTextBlob;
|
||||||
}
|
}
|
||||||
@@ -271,10 +290,17 @@ export const externalKmsServiceFactory = ({
|
|||||||
switch (externalKmsDoc.provider) {
|
switch (externalKmsDoc.provider) {
|
||||||
case KmsProviders.Aws: {
|
case KmsProviders.Aws: {
|
||||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||||
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
JSON.parse(decryptedProviderInputBlob.toString())
|
||||||
);
|
);
|
||||||
return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } };
|
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:
|
default:
|
||||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
throw new BadRequestError({ message: "external kms provided is invalid" });
|
||||||
}
|
}
|
||||||
@@ -312,21 +338,34 @@ export const externalKmsServiceFactory = ({
|
|||||||
switch (externalKmsDoc.provider) {
|
switch (externalKmsDoc.provider) {
|
||||||
case KmsProviders.Aws: {
|
case KmsProviders.Aws: {
|
||||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||||
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
JSON.parse(decryptedProviderInputBlob.toString())
|
||||||
);
|
);
|
||||||
return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } };
|
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:
|
default:
|
||||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
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 {
|
return {
|
||||||
create,
|
create,
|
||||||
updateById,
|
updateById,
|
||||||
deleteById,
|
deleteById,
|
||||||
list,
|
list,
|
||||||
findById,
|
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";
|
import { z } from "zod";
|
||||||
|
|
||||||
export enum KmsProviders {
|
export enum KmsProviders {
|
||||||
Aws = "aws"
|
Aws = "aws",
|
||||||
|
Gcp = "gcp"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum KmsAwsCredentialType {
|
export enum KmsAwsCredentialType {
|
||||||
AssumeRole = "assume-role",
|
AssumeRole = "assume-role",
|
||||||
AccessKey = "access-key"
|
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({
|
export const ExternalKmsAwsSchema = z.object({
|
||||||
credential: z
|
credential: z
|
||||||
@@ -42,14 +52,44 @@ export const ExternalKmsAwsSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type TExternalKmsAwsSchema = z.infer<typeof ExternalKmsAwsSchema>;
|
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
|
// The root schema of the JSON
|
||||||
export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
|
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 type TExternalKmsInputSchema = z.infer<typeof ExternalKmsInputSchema>;
|
||||||
|
|
||||||
export const ExternalKmsInputUpdateSchema = z.discriminatedUnion("type", [
|
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>;
|
export type TExternalKmsInputUpdateSchema = z.infer<typeof ExternalKmsInputUpdateSchema>;
|
||||||
|
|
||||||
|
@@ -31,7 +31,6 @@ export enum OrgPermissionSubjects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
| [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace]
|
|
||||||
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
|
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Role]
|
| [OrgPermissionActions, OrgPermissionSubjects.Role]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Member]
|
| [OrgPermissionActions, OrgPermissionSubjects.Member]
|
||||||
@@ -52,7 +51,6 @@ export type OrgPermissionSet =
|
|||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
// ws permissions
|
// ws permissions
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||||
// role permission
|
// role permission
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||||
@@ -135,7 +133,6 @@ export const orgAdminPermissions = buildAdminPermission();
|
|||||||
const buildMemberPermission = () => {
|
const buildMemberPermission = () => {
|
||||||
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||||
|
@@ -18,6 +18,7 @@ import { TGroupProjectDALFactory } from "@app/services/group-project/group-proje
|
|||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
import { deleteOrgMembershipFn } from "@app/services/org/org-fns";
|
import { deleteOrgMembershipFn } from "@app/services/org/org-fns";
|
||||||
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
|
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
|
||||||
|
import { OrgAuthMethod } from "@app/services/org/org-types";
|
||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
@@ -71,6 +72,7 @@ type TScimServiceFactoryDep = {
|
|||||||
| "deleteMembershipById"
|
| "deleteMembershipById"
|
||||||
| "transaction"
|
| "transaction"
|
||||||
| "updateMembershipById"
|
| "updateMembershipById"
|
||||||
|
| "findOrgById"
|
||||||
>;
|
>;
|
||||||
orgMembershipDAL: Pick<
|
orgMembershipDAL: Pick<
|
||||||
TOrgMembershipDALFactory,
|
TOrgMembershipDALFactory,
|
||||||
@@ -288,8 +290,7 @@ export const scimServiceFactory = ({
|
|||||||
const createScimUser = async ({ externalId, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
|
const createScimUser = async ({ externalId, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
|
||||||
if (!email) throw new ScimRequestError({ detail: "Invalid request. Missing email.", status: 400 });
|
if (!email) throw new ScimRequestError({ detail: "Invalid request. Missing email.", status: 400 });
|
||||||
|
|
||||||
const org = await orgDAL.findById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
|
|
||||||
if (!org)
|
if (!org)
|
||||||
throw new ScimRequestError({
|
throw new ScimRequestError({
|
||||||
detail: "Organization not found",
|
detail: "Organization not found",
|
||||||
@@ -302,13 +303,24 @@ export const scimServiceFactory = ({
|
|||||||
status: 403
|
status: 403
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!org.orgAuthMethod) {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "Neither SAML or OIDC SSO is configured",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
|
|
||||||
|
const aliasType = org.orgAuthMethod === OrgAuthMethod.OIDC ? UserAliasType.OIDC : UserAliasType.SAML;
|
||||||
|
const trustScimEmails =
|
||||||
|
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails;
|
||||||
|
|
||||||
const userAlias = await userAliasDAL.findOne({
|
const userAlias = await userAliasDAL.findOne({
|
||||||
externalId,
|
externalId,
|
||||||
orgId,
|
orgId,
|
||||||
aliasType: UserAliasType.SAML
|
aliasType
|
||||||
});
|
});
|
||||||
|
|
||||||
const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => {
|
const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => {
|
||||||
@@ -349,7 +361,7 @@ export const scimServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (serverCfg.trustSamlEmails) {
|
if (trustScimEmails) {
|
||||||
user = await userDAL.findOne(
|
user = await userDAL.findOne(
|
||||||
{
|
{
|
||||||
email,
|
email,
|
||||||
@@ -367,9 +379,9 @@ export const scimServiceFactory = ({
|
|||||||
);
|
);
|
||||||
user = await userDAL.create(
|
user = await userDAL.create(
|
||||||
{
|
{
|
||||||
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
|
username: trustScimEmails ? email : uniqueUsername,
|
||||||
email,
|
email,
|
||||||
isEmailVerified: serverCfg.trustSamlEmails,
|
isEmailVerified: trustScimEmails,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
authMethods: [],
|
authMethods: [],
|
||||||
@@ -382,7 +394,7 @@ export const scimServiceFactory = ({
|
|||||||
await userAliasDAL.create(
|
await userAliasDAL.create(
|
||||||
{
|
{
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
aliasType: UserAliasType.SAML,
|
aliasType,
|
||||||
externalId,
|
externalId,
|
||||||
emails: email ? [email] : [],
|
emails: email ? [email] : [],
|
||||||
orgId
|
orgId
|
||||||
@@ -437,7 +449,7 @@ export const scimServiceFactory = ({
|
|||||||
recipients: [email],
|
recipients: [email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
organizationName: org.name,
|
organizationName: org.name,
|
||||||
callback_url: `${appCfg.SITE_URL}/api/v1/sso/redirect/saml2/organizations/${org.slug}`
|
callback_url: `${appCfg.SITE_URL}/api/v1/sso/redirect/organizations/${org.slug}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -456,6 +468,14 @@ export const scimServiceFactory = ({
|
|||||||
|
|
||||||
// partial
|
// partial
|
||||||
const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
|
const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
|
||||||
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!org.orgAuthMethod) {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "Neither SAML or OIDC SSO is configured",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const [membership] = await orgDAL
|
const [membership] = await orgDAL
|
||||||
.findMembership({
|
.findMembership({
|
||||||
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
|
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
|
||||||
@@ -493,6 +513,9 @@ export const scimServiceFactory = ({
|
|||||||
scimPatch(scimUser, operations);
|
scimPatch(scimUser, operations);
|
||||||
|
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
|
const trustScimEmails =
|
||||||
|
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails;
|
||||||
|
|
||||||
await userDAL.transaction(async (tx) => {
|
await userDAL.transaction(async (tx) => {
|
||||||
await orgMembershipDAL.updateById(
|
await orgMembershipDAL.updateById(
|
||||||
membership.id,
|
membership.id,
|
||||||
@@ -508,7 +531,7 @@ export const scimServiceFactory = ({
|
|||||||
firstName: scimUser.name.givenName,
|
firstName: scimUser.name.givenName,
|
||||||
email: scimUser.emails[0].value,
|
email: scimUser.emails[0].value,
|
||||||
lastName: scimUser.name.familyName,
|
lastName: scimUser.name.familyName,
|
||||||
isEmailVerified: hasEmailChanged ? serverCfg.trustSamlEmails : true
|
isEmailVerified: hasEmailChanged ? trustScimEmails : true
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -526,6 +549,14 @@ export const scimServiceFactory = ({
|
|||||||
email,
|
email,
|
||||||
externalId
|
externalId
|
||||||
}: TReplaceScimUserDTO) => {
|
}: TReplaceScimUserDTO) => {
|
||||||
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!org.orgAuthMethod) {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "Neither SAML or OIDC SSO is configured",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const [membership] = await orgDAL
|
const [membership] = await orgDAL
|
||||||
.findMembership({
|
.findMembership({
|
||||||
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
|
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
|
||||||
@@ -555,7 +586,7 @@ export const scimServiceFactory = ({
|
|||||||
await userAliasDAL.update(
|
await userAliasDAL.update(
|
||||||
{
|
{
|
||||||
orgId,
|
orgId,
|
||||||
aliasType: UserAliasType.SAML,
|
aliasType: org.orgAuthMethod === OrgAuthMethod.OIDC ? UserAliasType.OIDC : UserAliasType.SAML,
|
||||||
userId: membership.userId
|
userId: membership.userId
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -576,7 +607,8 @@ export const scimServiceFactory = ({
|
|||||||
firstName,
|
firstName,
|
||||||
email,
|
email,
|
||||||
lastName,
|
lastName,
|
||||||
isEmailVerified: serverCfg.trustSamlEmails
|
isEmailVerified:
|
||||||
|
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
@@ -10,7 +10,7 @@ export const GITLAB_URL = "https://gitlab.com";
|
|||||||
export const IS_PACKAGED = (process as any)?.pkg !== undefined;
|
export const IS_PACKAGED = (process as any)?.pkg !== undefined;
|
||||||
|
|
||||||
const zodStrBool = z
|
const zodStrBool = z
|
||||||
.enum(["true", "false"])
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.transform((val) => val === "true");
|
.transform((val) => val === "true");
|
||||||
|
|
||||||
@@ -178,7 +178,10 @@ const envSchema = z
|
|||||||
HSM_LIB_PATH: zpStr(z.string().optional()),
|
HSM_LIB_PATH: zpStr(z.string().optional()),
|
||||||
HSM_PIN: zpStr(z.string().optional()),
|
HSM_PIN: zpStr(z.string().optional()),
|
||||||
HSM_KEY_LABEL: 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.
|
// To ensure that basic encryption is always possible.
|
||||||
.refine(
|
.refine(
|
||||||
|
@@ -55,7 +55,10 @@ const run = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const smtp = smtpServiceFactory(formatSmtpConfig());
|
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 keyStore = keyStoreFactory(appCfg.REDIS_URL);
|
||||||
|
|
||||||
const hsmModule = initializeHsmModule();
|
const hsmModule = initializeHsmModule();
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
|
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
|
||||||
import Redis from "ioredis";
|
import Redis from "ioredis";
|
||||||
|
import PgBoss, { WorkOptions } from "pg-boss";
|
||||||
|
|
||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
@@ -7,6 +8,8 @@ import {
|
|||||||
TScanFullRepoEventPayload,
|
TScanFullRepoEventPayload,
|
||||||
TScanPushEventPayload
|
TScanPushEventPayload
|
||||||
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
} 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 {
|
import {
|
||||||
TFailedIntegrationSyncEmailsPayload,
|
TFailedIntegrationSyncEmailsPayload,
|
||||||
TIntegrationSyncPayload,
|
TIntegrationSyncPayload,
|
||||||
@@ -184,17 +187,39 @@ export type TQueueJobTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
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 connection = new Redis(redisUrl, { maxRetriesPerRequest: null });
|
||||||
const queueContainer = {} as Record<
|
const queueContainer = {} as Record<
|
||||||
QueueName,
|
QueueName,
|
||||||
Queue<TQueueJobTypes[QueueName]["payload"], void, TQueueJobTypes[QueueName]["name"]>
|
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<
|
const workerContainer = {} as Record<
|
||||||
QueueName,
|
QueueName,
|
||||||
Worker<TQueueJobTypes[QueueName]["payload"], void, TQueueJobTypes[QueueName]["name"]>
|
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>(
|
const start = <T extends QueueName>(
|
||||||
name: T,
|
name: T,
|
||||||
jobFn: (job: Job<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>, token?: string) => Promise<void>,
|
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 = <
|
const listen = <
|
||||||
T extends QueueName,
|
T extends QueueName,
|
||||||
U extends keyof WorkerListener<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>
|
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);
|
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>(
|
const stopRepeatableJob = async <T extends QueueName>(
|
||||||
name: T,
|
name: T,
|
||||||
job: TQueueJobTypes[T]["name"],
|
job: TQueueJobTypes[T]["name"],
|
||||||
@@ -274,5 +332,17 @@ export const queueServiceFactory = (redisUrl: string) => {
|
|||||||
await Promise.all(Object.values(workerContainer).map((worker) => worker.close()));
|
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,
|
NotFound = 404,
|
||||||
Unauthorized = 401,
|
Unauthorized = 401,
|
||||||
Forbidden = 403,
|
Forbidden = 403,
|
||||||
|
UnprocessableContent = 422,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
InternalServerError = 500,
|
InternalServerError = 500,
|
||||||
GatewayTimeout = 504,
|
GatewayTimeout = 504,
|
||||||
@@ -66,9 +67,9 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
error: error.name
|
error: error.name
|
||||||
});
|
});
|
||||||
} else if (error instanceof ZodError) {
|
} else if (error instanceof ZodError) {
|
||||||
void res.status(HttpStatusCodes.Unauthorized).send({
|
void res.status(HttpStatusCodes.UnprocessableContent).send({
|
||||||
requestId: req.id,
|
requestId: req.id,
|
||||||
statusCode: HttpStatusCodes.Unauthorized,
|
statusCode: HttpStatusCodes.UnprocessableContent,
|
||||||
error: "ValidationFailure",
|
error: "ValidationFailure",
|
||||||
message: error.issues
|
message: error.issues
|
||||||
});
|
});
|
||||||
|
@@ -394,13 +394,14 @@ export const registerRoutes = async (
|
|||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
const auditLogQueue = auditLogQueueServiceFactory({
|
const auditLogQueue = await auditLogQueueServiceFactory({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
auditLogStreamDAL
|
auditLogStreamDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue });
|
const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue });
|
||||||
const auditLogStreamService = auditLogStreamServiceFactory({
|
const auditLogStreamService = auditLogStreamServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
|
@@ -44,7 +44,7 @@ export const DefaultResponseErrorsSchema = {
|
|||||||
401: z.object({
|
401: z.object({
|
||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
statusCode: z.literal(401),
|
statusCode: z.literal(401),
|
||||||
message: z.any(),
|
message: z.string(),
|
||||||
error: z.string()
|
error: z.string()
|
||||||
}),
|
}),
|
||||||
403: z.object({
|
403: z.object({
|
||||||
@@ -54,6 +54,13 @@ export const DefaultResponseErrorsSchema = {
|
|||||||
details: z.any().optional(),
|
details: z.any().optional(),
|
||||||
error: z.string()
|
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({
|
500: z.object({
|
||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
statusCode: z.literal(500),
|
statusCode: z.literal(500),
|
||||||
|
@@ -14,10 +14,12 @@ import { Strategy as GoogleStrategy } from "passport-google-oauth20";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { fetchGithubEmails } from "@app/lib/requests/github";
|
import { fetchGithubEmails } from "@app/lib/requests/github";
|
||||||
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { AuthMethod } from "@app/services/auth/auth-type";
|
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||||
|
import { OrgAuthMethod } from "@app/services/org/org-types";
|
||||||
|
|
||||||
export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -196,6 +198,44 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
handler: () => {}
|
handler: () => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/redirect/organizations/:orgSlug",
|
||||||
|
method: "GET",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
orgSlug: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
callback_port: z.string().optional()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const org = await server.services.org.findOrgBySlug(req.params.orgSlug);
|
||||||
|
if (org.orgAuthMethod === OrgAuthMethod.SAML) {
|
||||||
|
return res.redirect(
|
||||||
|
`${appCfg.SITE_URL}/api/v1/sso/redirect/saml2/organizations/${org.slug}?${
|
||||||
|
req.query.callback_port ? `callback_port=${req.query.callback_port}` : ""
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (org.orgAuthMethod === OrgAuthMethod.OIDC) {
|
||||||
|
return res.redirect(
|
||||||
|
`${appCfg.SITE_URL}/api/v1/sso/oidc/login?orgSlug=${org.slug}${
|
||||||
|
req.query.callback_port ? `&callbackPort=${req.query.callback_port}` : ""
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The organization does not have any SSO configured."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/github",
|
url: "/github",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@@ -120,7 +120,8 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
apiVersion: "authentication.k8s.io/v1",
|
apiVersion: "authentication.k8s.io/v1",
|
||||||
kind: "TokenReview",
|
kind: "TokenReview",
|
||||||
spec: {
|
spec: {
|
||||||
token: serviceAccountJwt
|
token: serviceAccountJwt,
|
||||||
|
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -473,7 +473,7 @@ const syncSecretsAzureKeyVault = async ({
|
|||||||
id: string; // secret URI
|
id: string; // secret URI
|
||||||
value: string;
|
value: string;
|
||||||
attributes: {
|
attributes: {
|
||||||
enabled: true;
|
enabled: boolean;
|
||||||
created: number;
|
created: number;
|
||||||
updated: number;
|
updated: number;
|
||||||
recoveryLevel: string;
|
recoveryLevel: string;
|
||||||
@@ -509,10 +509,19 @@ const syncSecretsAzureKeyVault = async ({
|
|||||||
|
|
||||||
const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`);
|
const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`);
|
||||||
|
|
||||||
|
const enabledAzureKeyVaultSecrets = getAzureKeyVaultSecrets.filter((secret) => secret.attributes.enabled);
|
||||||
|
|
||||||
|
// disabled keys to skip sending updates to
|
||||||
|
const disabledAzureKeyVaultSecretKeys = getAzureKeyVaultSecrets
|
||||||
|
.filter(({ attributes }) => !attributes.enabled)
|
||||||
|
.map((getAzureKeyVaultSecret) => {
|
||||||
|
return getAzureKeyVaultSecret.id.substring(getAzureKeyVaultSecret.id.lastIndexOf("/") + 1);
|
||||||
|
});
|
||||||
|
|
||||||
let lastSlashIndex: number;
|
let lastSlashIndex: number;
|
||||||
const res = (
|
const res = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
|
enabledAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
|
||||||
if (!lastSlashIndex) {
|
if (!lastSlashIndex) {
|
||||||
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/");
|
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/");
|
||||||
}
|
}
|
||||||
@@ -658,6 +667,7 @@ const syncSecretsAzureKeyVault = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
let isSecretSet = false;
|
let isSecretSet = false;
|
||||||
let maxTries = 6;
|
let maxTries = 6;
|
||||||
|
if (disabledAzureKeyVaultSecretKeys.includes(key)) return;
|
||||||
|
|
||||||
while (!isSecretSet && maxTries > 0) {
|
while (!isSecretSet && maxTries > 0) {
|
||||||
// try to set secret
|
// try to set secret
|
||||||
|
@@ -4,8 +4,10 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { KmsKeysSchema, TKmsRootConfig } from "@app/db/schemas";
|
import { KmsKeysSchema, TKmsRootConfig } from "@app/db/schemas";
|
||||||
import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms";
|
import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms";
|
||||||
|
import { GcpKmsProviderFactory } from "@app/ee/services/external-kms/providers/gcp-kms";
|
||||||
import {
|
import {
|
||||||
ExternalKmsAwsSchema,
|
ExternalKmsAwsSchema,
|
||||||
|
ExternalKmsGcpSchema,
|
||||||
KmsProviders,
|
KmsProviders,
|
||||||
TExternalKmsProviderFns
|
TExternalKmsProviderFns
|
||||||
} from "@app/ee/services/external-kms/providers/model";
|
} from "@app/ee/services/external-kms/providers/model";
|
||||||
@@ -291,6 +293,16 @@ export const kmsServiceFactory = ({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case KmsProviders.Gcp: {
|
||||||
|
const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync(
|
||||||
|
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
||||||
|
);
|
||||||
|
|
||||||
|
externalKms = await GcpKmsProviderFactory({
|
||||||
|
inputs: decryptedProviderInput
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error("Invalid KMS provider.");
|
throw new Error("Invalid KMS provider.");
|
||||||
}
|
}
|
||||||
@@ -353,6 +365,16 @@ export const kmsServiceFactory = ({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case KmsProviders.Gcp: {
|
||||||
|
const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync(
|
||||||
|
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
||||||
|
);
|
||||||
|
|
||||||
|
externalKms = await GcpKmsProviderFactory({
|
||||||
|
inputs: decryptedProviderInput
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error("Invalid KMS provider.");
|
throw new Error("Invalid KMS provider.");
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,8 @@ import { DatabaseError } from "@app/lib/errors";
|
|||||||
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt, withTransaction } from "@app/lib/knex";
|
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt, withTransaction } from "@app/lib/knex";
|
||||||
import { generateKnexQueryFromScim } from "@app/lib/knex/scim";
|
import { generateKnexQueryFromScim } from "@app/lib/knex/scim";
|
||||||
|
|
||||||
|
import { OrgAuthMethod } from "./org-types";
|
||||||
|
|
||||||
export type TOrgDALFactory = ReturnType<typeof orgDALFactory>;
|
export type TOrgDALFactory = ReturnType<typeof orgDALFactory>;
|
||||||
|
|
||||||
export const orgDALFactory = (db: TDbClient) => {
|
export const orgDALFactory = (db: TDbClient) => {
|
||||||
@@ -21,13 +23,78 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findOrgById = async (orgId: string) => {
|
const findOrgById = async (orgId: string) => {
|
||||||
try {
|
try {
|
||||||
const org = await db.replicaNode()(TableName.Organization).where({ id: orgId }).first();
|
const org = (await db
|
||||||
|
.replicaNode()(TableName.Organization)
|
||||||
|
.where({ [`${TableName.Organization}.id` as "id"]: orgId })
|
||||||
|
.leftJoin(TableName.SamlConfig, (qb) => {
|
||||||
|
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
|
||||||
|
`${TableName.SamlConfig}.isActive`,
|
||||||
|
"=",
|
||||||
|
db.raw("true")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.leftJoin(TableName.OidcConfig, (qb) => {
|
||||||
|
qb.on(`${TableName.OidcConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
|
||||||
|
`${TableName.OidcConfig}.isActive`,
|
||||||
|
"=",
|
||||||
|
db.raw("true")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.Organization))
|
||||||
|
.select(
|
||||||
|
db.raw(`
|
||||||
|
CASE
|
||||||
|
WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.SAML}'
|
||||||
|
WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.OIDC}'
|
||||||
|
ELSE ''
|
||||||
|
END as "orgAuthMethod"
|
||||||
|
`)
|
||||||
|
)
|
||||||
|
.first()) as TOrganizations & { orgAuthMethod?: string };
|
||||||
|
|
||||||
return org;
|
return org;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find org by id" });
|
throw new DatabaseError({ error, name: "Find org by id" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findOrgBySlug = async (orgSlug: string) => {
|
||||||
|
try {
|
||||||
|
const org = (await db
|
||||||
|
.replicaNode()(TableName.Organization)
|
||||||
|
.where({ [`${TableName.Organization}.slug` as "slug"]: orgSlug })
|
||||||
|
.leftJoin(TableName.SamlConfig, (qb) => {
|
||||||
|
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
|
||||||
|
`${TableName.SamlConfig}.isActive`,
|
||||||
|
"=",
|
||||||
|
db.raw("true")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.leftJoin(TableName.OidcConfig, (qb) => {
|
||||||
|
qb.on(`${TableName.OidcConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
|
||||||
|
`${TableName.OidcConfig}.isActive`,
|
||||||
|
"=",
|
||||||
|
db.raw("true")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.Organization))
|
||||||
|
.select(
|
||||||
|
db.raw(`
|
||||||
|
CASE
|
||||||
|
WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.SAML}'
|
||||||
|
WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.OIDC}'
|
||||||
|
ELSE ''
|
||||||
|
END as "orgAuthMethod"
|
||||||
|
`)
|
||||||
|
)
|
||||||
|
.first()) as TOrganizations & { orgAuthMethod?: string };
|
||||||
|
|
||||||
|
return org;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find org by slug" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// special query
|
// special query
|
||||||
const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => {
|
const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => {
|
||||||
try {
|
try {
|
||||||
@@ -398,6 +465,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
findAllOrgMembers,
|
findAllOrgMembers,
|
||||||
countAllOrgMembers,
|
countAllOrgMembers,
|
||||||
findOrgById,
|
findOrgById,
|
||||||
|
findOrgBySlug,
|
||||||
findAllOrgsByUserId,
|
findAllOrgsByUserId,
|
||||||
ghostUserExists,
|
ghostUserExists,
|
||||||
findOrgMembersByUsername,
|
findOrgMembersByUsername,
|
||||||
|
@@ -187,6 +187,15 @@ export const orgServiceFactory = ({
|
|||||||
return members;
|
return members;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findOrgBySlug = async (slug: string) => {
|
||||||
|
const org = await orgDAL.findOrgBySlug(slug);
|
||||||
|
if (!org) {
|
||||||
|
throw new NotFoundError({ message: `Organization with slug '${slug}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return org;
|
||||||
|
};
|
||||||
|
|
||||||
const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => {
|
const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => {
|
||||||
const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id));
|
const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id));
|
||||||
|
|
||||||
@@ -275,6 +284,7 @@ export const orgServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(orgId);
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
const currentOrg = await orgDAL.findOrgById(actorOrgId);
|
||||||
|
|
||||||
if (enforceMfa !== undefined) {
|
if (enforceMfa !== undefined) {
|
||||||
if (!plan.enforceMfa) {
|
if (!plan.enforceMfa) {
|
||||||
@@ -305,6 +315,11 @@ export const orgServiceFactory = ({
|
|||||||
"Failed to enable/disable SCIM provisioning due to plan restriction. Upgrade plan to enable/disable SCIM provisioning."
|
"Failed to enable/disable SCIM provisioning due to plan restriction. Upgrade plan to enable/disable SCIM provisioning."
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
|
||||||
|
if (scimEnabled && !currentOrg.orgAuthMethod) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Cannot enable SCIM when neither SAML or OIDC is configured."
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authEnforced) {
|
if (authEnforced) {
|
||||||
@@ -1132,6 +1147,7 @@ export const orgServiceFactory = ({
|
|||||||
createIncidentContact,
|
createIncidentContact,
|
||||||
deleteIncidentContact,
|
deleteIncidentContact,
|
||||||
getOrgGroups,
|
getOrgGroups,
|
||||||
listProjectMembershipsByOrgMembershipId
|
listProjectMembershipsByOrgMembershipId,
|
||||||
|
findOrgBySlug
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -74,3 +74,8 @@ export type TGetOrgGroupsDTO = TOrgPermission;
|
|||||||
export type TListProjectMembershipsByOrgMembershipIdDTO = {
|
export type TListProjectMembershipsByOrgMembershipIdDTO = {
|
||||||
orgMembershipId: string;
|
orgMembershipId: string;
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export enum OrgAuthMethod {
|
||||||
|
OIDC = "oidc",
|
||||||
|
SAML = "saml"
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
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 { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
||||||
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
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(
|
const identityProjectMembership = await identityProjectDAL.create(
|
||||||
{
|
{
|
||||||
identityId: actorId,
|
identityId: actorId,
|
||||||
@@ -395,8 +380,7 @@ export const projectServiceFactory = ({
|
|||||||
await identityProjectMembershipRoleDAL.create(
|
await identityProjectMembershipRoleDAL.create(
|
||||||
{
|
{
|
||||||
projectMembershipId: identityProjectMembership.id,
|
projectMembershipId: identityProjectMembership.id,
|
||||||
role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin,
|
role: ProjectMembershipRole.Admin
|
||||||
customRoleId: customRole?.id
|
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
@@ -365,9 +365,8 @@ export const recursivelyGetSecretPaths = async ({
|
|||||||
folderId: p.folderId
|
folderId: p.folderId
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const pathsInCurrentDirectory = paths.filter((folder) =>
|
// path relative will start with ../ if its outside directory
|
||||||
folder.path.startsWith(currentPath === "/" ? "" : currentPath)
|
const pathsInCurrentDirectory = paths.filter((folder) => !path.relative(currentPath, folder.path).startsWith(".."));
|
||||||
);
|
|
||||||
|
|
||||||
return pathsInCurrentDirectory;
|
return pathsInCurrentDirectory;
|
||||||
};
|
};
|
||||||
|
@@ -932,8 +932,12 @@ export const secretQueueFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
"Unknown error occurred.";
|
(err instanceof AxiosError
|
||||||
|
? err?.response?.data
|
||||||
|
? JSON.stringify(err?.response?.data)
|
||||||
|
: err?.message
|
||||||
|
: (err as Error)?.message) || "Unknown error occurred.";
|
||||||
|
|
||||||
await auditLogService.createAuditLog({
|
await auditLogService.createAuditLog({
|
||||||
projectId,
|
projectId,
|
||||||
|
@@ -10,7 +10,7 @@ require (
|
|||||||
github.com/fatih/semgroup v1.2.0
|
github.com/fatih/semgroup v1.2.0
|
||||||
github.com/gitleaks/go-gitdiff v0.8.0
|
github.com/gitleaks/go-gitdiff v0.8.0
|
||||||
github.com/h2non/filetype v1.1.3
|
github.com/h2non/filetype v1.1.3
|
||||||
github.com/infisical/go-sdk v0.3.8
|
github.com/infisical/go-sdk v0.4.3
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
||||||
github.com/muesli/mango-cobra v1.2.0
|
github.com/muesli/mango-cobra v1.2.0
|
||||||
|
@@ -265,8 +265,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
|||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/infisical/go-sdk v0.3.8 h1:0dGOhF3cwt0q5QzpnUs4lxwBiEza+DQYOyvEn7AfrM0=
|
github.com/infisical/go-sdk v0.4.3 h1:O5ZJ2eCBAZDE9PIAfBPq9Utb2CgQKrhmj9R0oFTRu4U=
|
||||||
github.com/infisical/go-sdk v0.3.8/go.mod h1:HHW7DgUqoolyQIUw/9HdpkZ3bDLwWyZ0HEtYiVaDKQw=
|
github.com/infisical/go-sdk v0.4.3/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M=
|
||||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||||
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
||||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
@@ -205,6 +205,25 @@ func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesR
|
|||||||
return workSpacesResponse, nil
|
return workSpacesResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CallGetProjectById(httpClient *resty.Client, id string) (Project, error) {
|
||||||
|
var projectResponse GetProjectByIdResponse
|
||||||
|
response, err := httpClient.
|
||||||
|
R().
|
||||||
|
SetResult(&projectResponse).
|
||||||
|
SetHeader("User-Agent", USER_AGENT).
|
||||||
|
Get(fmt.Sprintf("%v/v1/workspace/%s", config.INFISICAL_URL, id))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return Project{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.IsError() {
|
||||||
|
return Project{}, fmt.Errorf("CallGetProjectById: Unsuccessful response: [response=%v]", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectResponse.Project, nil
|
||||||
|
}
|
||||||
|
|
||||||
func CallIsAuthenticated(httpClient *resty.Client) bool {
|
func CallIsAuthenticated(httpClient *resty.Client) bool {
|
||||||
var workSpacesResponse GetWorkSpacesResponse
|
var workSpacesResponse GetWorkSpacesResponse
|
||||||
response, err := httpClient.
|
response, err := httpClient.
|
||||||
|
@@ -128,6 +128,10 @@ type GetWorkSpacesResponse struct {
|
|||||||
} `json:"workspaces"`
|
} `json:"workspaces"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetProjectByIdResponse struct {
|
||||||
|
Project Project `json:"workspace"`
|
||||||
|
}
|
||||||
|
|
||||||
type GetOrganizationsResponse struct {
|
type GetOrganizationsResponse struct {
|
||||||
Organizations []struct {
|
Organizations []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -163,6 +167,12 @@ type Secret struct {
|
|||||||
PlainTextKey string `json:"plainTextKey"`
|
PlainTextKey string `json:"plainTextKey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
}
|
||||||
|
|
||||||
type RawSecret struct {
|
type RawSecret struct {
|
||||||
SecretKey string `json:"secretKey,omitempty"`
|
SecretKey string `json:"secretKey,omitempty"`
|
||||||
SecretValue string `json:"secretValue,omitempty"`
|
SecretValue string `json:"secretValue,omitempty"`
|
||||||
|
571
cli/packages/cmd/dynamic_secrets.go
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2023 Infisical Inc.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Infisical/infisical-merge/packages/api"
|
||||||
|
"github.com/Infisical/infisical-merge/packages/config"
|
||||||
|
"github.com/Infisical/infisical-merge/packages/visualize"
|
||||||
|
|
||||||
|
// "github.com/Infisical/infisical-merge/packages/models"
|
||||||
|
"github.com/Infisical/infisical-merge/packages/util"
|
||||||
|
// "github.com/Infisical/infisical-merge/packages/visualize"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/posthog/posthog-go"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
infisicalSdk "github.com/infisical/go-sdk"
|
||||||
|
infisicalSdkModels "github.com/infisical/go-sdk/packages/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dynamicSecretCmd = &cobra.Command{
|
||||||
|
Example: `infisical dynamic-secrets`,
|
||||||
|
Short: "Used to list dynamic secrets",
|
||||||
|
Use: "dynamic-secrets",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: getDynamicSecretList,
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDynamicSecretList(cmd *cobra.Command, args []string) {
|
||||||
|
environmentName, _ := cmd.Flags().GetString("env")
|
||||||
|
if !cmd.Flags().Changed("env") {
|
||||||
|
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||||
|
if environmentFromWorkspace != "" {
|
||||||
|
environmentName = environmentFromWorkspace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
secretsPath, err := cmd.Flags().GetString("path")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse path flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
var infisicalToken string
|
||||||
|
httpClient := resty.New()
|
||||||
|
|
||||||
|
if projectId == "" {
|
||||||
|
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to get local project details")
|
||||||
|
}
|
||||||
|
projectId = workspaceFile.WorkspaceId
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||||
|
infisicalToken = token.Token
|
||||||
|
} else {
|
||||||
|
util.RequireLogin()
|
||||||
|
util.RequireLocalWorkspaceFile()
|
||||||
|
|
||||||
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
if loggedInUserDetails.LoginExpired {
|
||||||
|
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||||
|
}
|
||||||
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient.SetAuthToken(infisicalToken)
|
||||||
|
|
||||||
|
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||||
|
SiteUrl: config.INFISICAL_URL,
|
||||||
|
UserAgent: api.USER_AGENT,
|
||||||
|
AutoTokenRefresh: false,
|
||||||
|
})
|
||||||
|
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||||
|
|
||||||
|
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch project details")
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicSecretRootCredentials, err := infisicalClient.DynamicSecrets().List(infisicalSdk.ListDynamicSecretsRootCredentialsOptions{
|
||||||
|
ProjectSlug: projectDetails.Slug,
|
||||||
|
SecretPath: secretsPath,
|
||||||
|
EnvironmentSlug: environmentName,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch dynamic secret root credentials details")
|
||||||
|
}
|
||||||
|
|
||||||
|
visualize.PrintAllDynamicRootCredentials(dynamicSecretRootCredentials)
|
||||||
|
Telemetry.CaptureEvent("cli-command:dynamic-secrets", posthog.NewProperties().Set("count", len(dynamicSecretRootCredentials)).Set("version", util.CLI_VERSION))
|
||||||
|
}
|
||||||
|
|
||||||
|
var dynamicSecretLeaseCmd = &cobra.Command{
|
||||||
|
Example: `lease`,
|
||||||
|
Short: "Manage leases for dynamic secrets",
|
||||||
|
Use: "lease",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var dynamicSecretLeaseCreateCmd = &cobra.Command{
|
||||||
|
Example: `lease create <dynamic secret name>"`,
|
||||||
|
Short: "Used to lease dynamic secret by name",
|
||||||
|
Use: "create [dynamic-secret]",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: createDynamicSecretLeaseByName,
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||||
|
dynamicSecretRootCredentialName := args[0]
|
||||||
|
|
||||||
|
environmentName, _ := cmd.Flags().GetString("env")
|
||||||
|
if !cmd.Flags().Changed("env") {
|
||||||
|
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||||
|
if environmentFromWorkspace != "" {
|
||||||
|
environmentName = environmentFromWorkspace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl, err := cmd.Flags().GetString("ttl")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
secretsPath, err := cmd.Flags().GetString("path")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse path flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
plainOutput, err := cmd.Flags().GetBool("plain")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
var infisicalToken string
|
||||||
|
httpClient := resty.New()
|
||||||
|
|
||||||
|
if projectId == "" {
|
||||||
|
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to get local project details")
|
||||||
|
}
|
||||||
|
projectId = workspaceFile.WorkspaceId
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||||
|
infisicalToken = token.Token
|
||||||
|
} else {
|
||||||
|
util.RequireLogin()
|
||||||
|
util.RequireLocalWorkspaceFile()
|
||||||
|
|
||||||
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
if loggedInUserDetails.LoginExpired {
|
||||||
|
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||||
|
}
|
||||||
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient.SetAuthToken(infisicalToken)
|
||||||
|
|
||||||
|
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||||
|
SiteUrl: config.INFISICAL_URL,
|
||||||
|
UserAgent: api.USER_AGENT,
|
||||||
|
AutoTokenRefresh: false,
|
||||||
|
})
|
||||||
|
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||||
|
|
||||||
|
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch project details")
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicSecretRootCredential, err := infisicalClient.DynamicSecrets().GetByName(infisicalSdk.GetDynamicSecretRootCredentialByNameOptions{
|
||||||
|
DynamicSecretName: dynamicSecretRootCredentialName,
|
||||||
|
ProjectSlug: projectDetails.Slug,
|
||||||
|
SecretPath: secretsPath,
|
||||||
|
EnvironmentSlug: environmentName,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch dynamic secret root credentials details")
|
||||||
|
}
|
||||||
|
|
||||||
|
leaseCredentials, _, leaseDetails, err := infisicalClient.DynamicSecrets().Leases().Create(infisicalSdk.CreateDynamicSecretLeaseOptions{
|
||||||
|
DynamicSecretName: dynamicSecretRootCredential.Name,
|
||||||
|
ProjectSlug: projectDetails.Slug,
|
||||||
|
TTL: ttl,
|
||||||
|
SecretPath: secretsPath,
|
||||||
|
EnvironmentSlug: environmentName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "To lease dynamic secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
if plainOutput {
|
||||||
|
for key, value := range leaseCredentials {
|
||||||
|
if cred, ok := value.(string); ok {
|
||||||
|
fmt.Printf("%s=%s\n", key, cred)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Dynamic Secret Leasing")
|
||||||
|
fmt.Printf("Name: %s\n", dynamicSecretRootCredential.Name)
|
||||||
|
fmt.Printf("Provider: %s\n", dynamicSecretRootCredential.Type)
|
||||||
|
fmt.Printf("Lease ID: %s\n", leaseDetails.Id)
|
||||||
|
fmt.Printf("Expire At: %s\n", leaseDetails.ExpireAt.Local().Format("02-Jan-2006 03:04:05 PM"))
|
||||||
|
visualize.PrintAllDyamicSecretLeaseCredentials(leaseCredentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
Telemetry.CaptureEvent("cli-command:dynamic-secrets lease", posthog.NewProperties().Set("type", dynamicSecretRootCredential.Type).Set("version", util.CLI_VERSION))
|
||||||
|
}
|
||||||
|
|
||||||
|
var dynamicSecretLeaseRenewCmd = &cobra.Command{
|
||||||
|
Example: `lease renew <dynamic secret name>"`,
|
||||||
|
Short: "Used to renew dynamic secret lease by name",
|
||||||
|
Use: "renew [lease-id]",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: renewDynamicSecretLeaseByName,
|
||||||
|
}
|
||||||
|
|
||||||
|
func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||||
|
dynamicSecretLeaseId := args[0]
|
||||||
|
|
||||||
|
environmentName, _ := cmd.Flags().GetString("env")
|
||||||
|
if !cmd.Flags().Changed("env") {
|
||||||
|
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||||
|
if environmentFromWorkspace != "" {
|
||||||
|
environmentName = environmentFromWorkspace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl, err := cmd.Flags().GetString("ttl")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
secretsPath, err := cmd.Flags().GetString("path")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse path flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
var infisicalToken string
|
||||||
|
httpClient := resty.New()
|
||||||
|
|
||||||
|
if projectId == "" {
|
||||||
|
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to get local project details")
|
||||||
|
}
|
||||||
|
projectId = workspaceFile.WorkspaceId
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||||
|
infisicalToken = token.Token
|
||||||
|
} else {
|
||||||
|
util.RequireLogin()
|
||||||
|
util.RequireLocalWorkspaceFile()
|
||||||
|
|
||||||
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
if loggedInUserDetails.LoginExpired {
|
||||||
|
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||||
|
}
|
||||||
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient.SetAuthToken(infisicalToken)
|
||||||
|
|
||||||
|
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||||
|
SiteUrl: config.INFISICAL_URL,
|
||||||
|
UserAgent: api.USER_AGENT,
|
||||||
|
AutoTokenRefresh: false,
|
||||||
|
})
|
||||||
|
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||||
|
|
||||||
|
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch project details")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch dynamic secret root credentials details")
|
||||||
|
}
|
||||||
|
|
||||||
|
leaseDetails, err := infisicalClient.DynamicSecrets().Leases().RenewById(infisicalSdk.RenewDynamicSecretLeaseOptions{
|
||||||
|
ProjectSlug: projectDetails.Slug,
|
||||||
|
TTL: ttl,
|
||||||
|
SecretPath: secretsPath,
|
||||||
|
EnvironmentSlug: environmentName,
|
||||||
|
LeaseId: dynamicSecretLeaseId,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "To renew dynamic secret lease")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Successfully renewed dynamic secret lease")
|
||||||
|
visualize.PrintAllDynamicSecretLeases([]infisicalSdkModels.DynamicSecretLease{leaseDetails})
|
||||||
|
|
||||||
|
Telemetry.CaptureEvent("cli-command:dynamic-secrets lease renew", posthog.NewProperties().Set("version", util.CLI_VERSION))
|
||||||
|
}
|
||||||
|
|
||||||
|
var dynamicSecretLeaseRevokeCmd = &cobra.Command{
|
||||||
|
Example: `lease delete <dynamic secret name>"`,
|
||||||
|
Short: "Used to delete dynamic secret lease by name",
|
||||||
|
Use: "delete [lease-id]",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: revokeDynamicSecretLeaseByName,
|
||||||
|
}
|
||||||
|
|
||||||
|
func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||||
|
dynamicSecretLeaseId := args[0]
|
||||||
|
|
||||||
|
environmentName, _ := cmd.Flags().GetString("env")
|
||||||
|
if !cmd.Flags().Changed("env") {
|
||||||
|
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||||
|
if environmentFromWorkspace != "" {
|
||||||
|
environmentName = environmentFromWorkspace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
secretsPath, err := cmd.Flags().GetString("path")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse path flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
var infisicalToken string
|
||||||
|
httpClient := resty.New()
|
||||||
|
|
||||||
|
if projectId == "" {
|
||||||
|
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to get local project details")
|
||||||
|
}
|
||||||
|
projectId = workspaceFile.WorkspaceId
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||||
|
infisicalToken = token.Token
|
||||||
|
} else {
|
||||||
|
util.RequireLogin()
|
||||||
|
util.RequireLocalWorkspaceFile()
|
||||||
|
|
||||||
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
if loggedInUserDetails.LoginExpired {
|
||||||
|
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||||
|
}
|
||||||
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient.SetAuthToken(infisicalToken)
|
||||||
|
|
||||||
|
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||||
|
SiteUrl: config.INFISICAL_URL,
|
||||||
|
UserAgent: api.USER_AGENT,
|
||||||
|
AutoTokenRefresh: false,
|
||||||
|
})
|
||||||
|
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||||
|
|
||||||
|
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch project details")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch dynamic secret root credentials details")
|
||||||
|
}
|
||||||
|
|
||||||
|
leaseDetails, err := infisicalClient.DynamicSecrets().Leases().DeleteById(infisicalSdk.DeleteDynamicSecretLeaseOptions{
|
||||||
|
ProjectSlug: projectDetails.Slug,
|
||||||
|
SecretPath: secretsPath,
|
||||||
|
EnvironmentSlug: environmentName,
|
||||||
|
LeaseId: dynamicSecretLeaseId,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "To revoke dynamic secret lease")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Successfully revoked dynamic secret lease")
|
||||||
|
visualize.PrintAllDynamicSecretLeases([]infisicalSdkModels.DynamicSecretLease{leaseDetails})
|
||||||
|
|
||||||
|
Telemetry.CaptureEvent("cli-command:dynamic-secrets lease revoke", posthog.NewProperties().Set("version", util.CLI_VERSION))
|
||||||
|
}
|
||||||
|
|
||||||
|
var dynamicSecretLeaseListCmd = &cobra.Command{
|
||||||
|
Example: `lease list <dynamic secret name>"`,
|
||||||
|
Short: "Used to list leases of a dynamic secret by name",
|
||||||
|
Use: "list [dynamic-secret]",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: listDynamicSecretLeaseByName,
|
||||||
|
}
|
||||||
|
|
||||||
|
func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||||
|
dynamicSecretRootCredentialName := args[0]
|
||||||
|
|
||||||
|
environmentName, _ := cmd.Flags().GetString("env")
|
||||||
|
if !cmd.Flags().Changed("env") {
|
||||||
|
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||||
|
if environmentFromWorkspace != "" {
|
||||||
|
environmentName = environmentFromWorkspace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
secretsPath, err := cmd.Flags().GetString("path")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse path flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
var infisicalToken string
|
||||||
|
httpClient := resty.New()
|
||||||
|
|
||||||
|
if projectId == "" {
|
||||||
|
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to get local project details")
|
||||||
|
}
|
||||||
|
projectId = workspaceFile.WorkspaceId
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||||
|
infisicalToken = token.Token
|
||||||
|
} else {
|
||||||
|
util.RequireLogin()
|
||||||
|
util.RequireLocalWorkspaceFile()
|
||||||
|
|
||||||
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
if loggedInUserDetails.LoginExpired {
|
||||||
|
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||||
|
}
|
||||||
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient.SetAuthToken(infisicalToken)
|
||||||
|
|
||||||
|
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||||
|
SiteUrl: config.INFISICAL_URL,
|
||||||
|
UserAgent: api.USER_AGENT,
|
||||||
|
AutoTokenRefresh: false,
|
||||||
|
})
|
||||||
|
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||||
|
|
||||||
|
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch project details")
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicSecretLeases, err := infisicalClient.DynamicSecrets().Leases().List(infisicalSdk.ListDynamicSecretLeasesOptions{
|
||||||
|
DynamicSecretName: dynamicSecretRootCredentialName,
|
||||||
|
ProjectSlug: projectDetails.Slug,
|
||||||
|
SecretPath: secretsPath,
|
||||||
|
EnvironmentSlug: environmentName,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch dynamic secret leases list")
|
||||||
|
}
|
||||||
|
|
||||||
|
visualize.PrintAllDynamicSecretLeases(dynamicSecretLeases)
|
||||||
|
Telemetry.CaptureEvent("cli-command:dynamic-secrets lease list", posthog.NewProperties().Set("lease-count", len(dynamicSecretLeases)).Set("version", util.CLI_VERSION))
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dynamicSecretLeaseCreateCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
||||||
|
dynamicSecretLeaseCreateCmd.Flags().String("token", "", "Create dynamic secret leases using machine identity access token")
|
||||||
|
dynamicSecretLeaseCreateCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
||||||
|
dynamicSecretLeaseCreateCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.")
|
||||||
|
dynamicSecretLeaseCreateCmd.Flags().Bool("plain", false, "Print leased credentials without formatting, one per line")
|
||||||
|
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseCreateCmd)
|
||||||
|
|
||||||
|
dynamicSecretLeaseListCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
||||||
|
dynamicSecretLeaseListCmd.Flags().String("token", "", "Fetch dynamic secret leases machine identity access token")
|
||||||
|
dynamicSecretLeaseListCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
||||||
|
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseListCmd)
|
||||||
|
|
||||||
|
dynamicSecretLeaseRenewCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
||||||
|
dynamicSecretLeaseRenewCmd.Flags().String("token", "", "Renew dynamic secrets machine identity access token")
|
||||||
|
dynamicSecretLeaseRenewCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
||||||
|
dynamicSecretLeaseRenewCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.")
|
||||||
|
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRenewCmd)
|
||||||
|
|
||||||
|
dynamicSecretLeaseRevokeCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
||||||
|
dynamicSecretLeaseRevokeCmd.Flags().String("token", "", "Delete dynamic secrets using machine identity access token")
|
||||||
|
dynamicSecretLeaseRevokeCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
||||||
|
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRevokeCmd)
|
||||||
|
|
||||||
|
dynamicSecretCmd.AddCommand(dynamicSecretLeaseCmd)
|
||||||
|
|
||||||
|
dynamicSecretCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token")
|
||||||
|
dynamicSecretCmd.Flags().String("projectId", "", "Manually set the projectId to fetch dynamic-secret when using machine identity based auth")
|
||||||
|
dynamicSecretCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
|
||||||
|
dynamicSecretCmd.Flags().String("path", "/", "get dynamic secret within a folder path")
|
||||||
|
rootCmd.AddCommand(dynamicSecretCmd)
|
||||||
|
}
|
39
cli/packages/visualize/dynamic_secret_leases.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package visualize
|
||||||
|
|
||||||
|
import infisicalModels "github.com/infisical/go-sdk/packages/models"
|
||||||
|
|
||||||
|
func PrintAllDyamicSecretLeaseCredentials(leaseCredentials map[string]any) {
|
||||||
|
rows := [][]string{}
|
||||||
|
for key, value := range leaseCredentials {
|
||||||
|
if cred, ok := value.(string); ok {
|
||||||
|
rows = append(rows, []string{key, cred})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := []string{"Key", "Value"}
|
||||||
|
|
||||||
|
GenericTable(headers, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrintAllDynamicRootCredentials(dynamicRootCredentials []infisicalModels.DynamicSecret) {
|
||||||
|
rows := [][]string{}
|
||||||
|
for _, el := range dynamicRootCredentials {
|
||||||
|
rows = append(rows, []string{el.Name, el.Type, el.DefaultTTL, el.MaxTTL})
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := []string{"Name", "Provider", "Default TTL", "Max TTL"}
|
||||||
|
|
||||||
|
GenericTable(headers, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrintAllDynamicSecretLeases(dynamicSecretLeases []infisicalModels.DynamicSecretLease) {
|
||||||
|
rows := [][]string{}
|
||||||
|
const timeformat = "02-Jan-2006 03:04:05 PM"
|
||||||
|
for _, el := range dynamicSecretLeases {
|
||||||
|
rows = append(rows, []string{el.Id, el.ExpireAt.Local().Format(timeformat), el.CreatedAt.Local().Format(timeformat)})
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := []string{"ID", "Expire At", "Created At"}
|
||||||
|
|
||||||
|
GenericTable(headers, rows)
|
||||||
|
}
|
@@ -94,6 +94,33 @@ func getLongestValues(rows [][3]string) (longestSecretName, longestSecretType in
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenericTable(headers []string, rows [][]string) {
|
||||||
|
t := table.NewWriter()
|
||||||
|
t.SetOutputMirror(os.Stdout)
|
||||||
|
t.SetStyle(table.StyleLight)
|
||||||
|
|
||||||
|
// t.SetTitle(tableOptions.Title)
|
||||||
|
t.Style().Options.DrawBorder = true
|
||||||
|
t.Style().Options.SeparateHeader = true
|
||||||
|
t.Style().Options.SeparateColumns = true
|
||||||
|
|
||||||
|
tableHeaders := table.Row{}
|
||||||
|
for _, header := range headers {
|
||||||
|
tableHeaders = append(tableHeaders, header)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.AppendHeader(tableHeaders)
|
||||||
|
for _, row := range rows {
|
||||||
|
tableRow := table.Row{}
|
||||||
|
for _, val := range row {
|
||||||
|
tableRow = append(tableRow, val)
|
||||||
|
}
|
||||||
|
t.AppendRow(tableRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Render()
|
||||||
|
}
|
||||||
|
|
||||||
// stringWidth returns the width of a string.
|
// stringWidth returns the width of a string.
|
||||||
// ANSI escape sequences are ignored and double-width characters are handled correctly.
|
// ANSI escape sequences are ignored and double-width characters are handled correctly.
|
||||||
func stringWidth(str string) (width int) {
|
func stringWidth(str string) (width int) {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
error: CallGetRawSecretsV3: Unsuccessful response [GET https://app.infisical.com/api/v3/secrets/raw?environment=invalid-env&expandSecretReferences=true&include_imports=true&recursive=true&secretPath=%2F&workspaceId=bef697d4-849b-4a75-b284-0922f87f8ba2] [status-code=404] [response={"statusCode":404,"message":"Environment with slug 'invalid-env' in project with ID bef697d4-849b-4a75-b284-0922f87f8ba2 not found","error":"NotFound"}]
|
error: CallGetRawSecretsV3: Unsuccessful response [GET https://app.infisical.com/api/v3/secrets/raw?environment=invalid-env&expandSecretReferences=true&include_imports=true&recursive=true&secretPath=%2F&workspaceId=bef697d4-849b-4a75-b284-0922f87f8ba2] [status-code=404] [response={"error":"NotFound","message":"Environment with slug 'invalid-env' in project with ID bef697d4-849b-4a75-b284-0922f87f8ba2 not found","statusCode":404}]
|
||||||
|
|
||||||
|
|
||||||
If this issue continues, get support at https://infisical.com/slack
|
If this issue continues, get support at https://infisical.com/slack
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
Warning: Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug
|
Warning: Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug
|
||||||
┌───────────────┬──────────────┬─────────────┐
|
┌───────────────┬──────────────┬─────────────┐
|
||||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||||
├───────────────┼──────────────┼─────────────┤
|
├───────────────┼──────────────┼─────────────┤
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package tests
|
package tests
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@@ -41,11 +42,12 @@ var creds = Credentials{
|
|||||||
func ExecuteCliCommand(command string, args ...string) (string, error) {
|
func ExecuteCliCommand(command string, args ...string) (string, error) {
|
||||||
cmd := exec.Command(command, args...)
|
cmd := exec.Command(command, args...)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(fmt.Sprint(err) + ": " + string(output))
|
fmt.Println(fmt.Sprint(err) + ": " + FilterRequestID(strings.TrimSpace(string(output))))
|
||||||
return strings.TrimSpace(string(output)), err
|
return FilterRequestID(strings.TrimSpace(string(output))), err
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(string(output)), nil
|
return FilterRequestID(strings.TrimSpace(string(output))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupCli() {
|
func SetupCli() {
|
||||||
@@ -67,3 +69,34 @@ func SetupCli() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FilterRequestID(input string) string {
|
||||||
|
// Find the JSON part of the error message
|
||||||
|
start := strings.Index(input, "{")
|
||||||
|
end := strings.LastIndex(input, "}") + 1
|
||||||
|
|
||||||
|
if start == -1 || end == -1 {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonPart := input[:start] // Pre-JSON content
|
||||||
|
|
||||||
|
// Parse the JSON object
|
||||||
|
var errorObj map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(input[start:end]), &errorObj); err != nil {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove requestId field
|
||||||
|
delete(errorObj, "requestId")
|
||||||
|
delete(errorObj, "reqId")
|
||||||
|
|
||||||
|
// Convert back to JSON
|
||||||
|
filtered, err := json.Marshal(errorObj)
|
||||||
|
if err != nil {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct the full string
|
||||||
|
return jsonPart + string(filtered) + input[end:]
|
||||||
|
}
|
||||||
|
@@ -3,7 +3,6 @@ package tests
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Infisical/infisical-merge/packages/util"
|
|
||||||
"github.com/bradleyjkemp/cupaloy/v2"
|
"github.com/bradleyjkemp/cupaloy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,28 +95,29 @@ func TestUserAuth_SecretsGetAll(t *testing.T) {
|
|||||||
// testUserAuth_SecretsGetAllWithoutConnection(t)
|
// testUserAuth_SecretsGetAllWithoutConnection(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
|
// disabled for the time being
|
||||||
originalConfigFile, err := util.GetConfigFile()
|
// func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
|
||||||
if err != nil {
|
// originalConfigFile, err := util.GetConfigFile()
|
||||||
t.Fatalf("error getting config file")
|
// if err != nil {
|
||||||
}
|
// t.Fatalf("error getting config file")
|
||||||
newConfigFile := originalConfigFile
|
// }
|
||||||
|
// newConfigFile := originalConfigFile
|
||||||
|
|
||||||
// set it to a URL that will always be unreachable
|
// // set it to a URL that will always be unreachable
|
||||||
newConfigFile.LoggedInUserDomain = "http://localhost:4999"
|
// newConfigFile.LoggedInUserDomain = "http://localhost:4999"
|
||||||
util.WriteConfigFile(&newConfigFile)
|
// util.WriteConfigFile(&newConfigFile)
|
||||||
|
|
||||||
// restore config file
|
// // restore config file
|
||||||
defer util.WriteConfigFile(&originalConfigFile)
|
// defer util.WriteConfigFile(&originalConfigFile)
|
||||||
|
|
||||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent")
|
// output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent")
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Fatalf("error running CLI command: %v", err)
|
// t.Fatalf("error running CLI command: %v", err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Use cupaloy to snapshot test the output
|
// // Use cupaloy to snapshot test the output
|
||||||
err = cupaloy.Snapshot(output)
|
// err = cupaloy.Snapshot(output)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Fatalf("snapshot failed: %v", err)
|
// t.Fatalf("snapshot failed: %v", err)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
@@ -12,6 +12,18 @@ To request time off, just submit a request in Rippling and let Maidul know at le
|
|||||||
|
|
||||||
Since Infisical's team is globally distributed, it is hard for us to keep track of all the various national holidays across many different countries. Whether you'd like to celebrate Christmas or National Brisket Day (which, by the way, is on May 28th), you are welcome to take PTO on those days – just let Maidul know at least a week ahead so that we can adjust our planning.
|
Since Infisical's team is globally distributed, it is hard for us to keep track of all the various national holidays across many different countries. Whether you'd like to celebrate Christmas or National Brisket Day (which, by the way, is on May 28th), you are welcome to take PTO on those days – just let Maidul know at least a week ahead so that we can adjust our planning.
|
||||||
|
|
||||||
## Winter Break
|
## Winter break
|
||||||
|
|
||||||
Every year, Infisical team goes on a company-wide vacation during winter holidays. This year, the winter break period starts on December 21st, 2024 and ends on January 5th, 2025. You should expect to do no scheduled work during this period, but we will have a rotation process for [high and urgent service disruptions](https://infisical.com/sla).
|
Every year, Infisical team goes on a company-wide vacation during winter holidays. This year, the winter break period starts on December 21st, 2024 and ends on January 5th, 2025. You should expect to do no scheduled work during this period, but we will have a rotation process for [high and urgent service disruptions](https://infisical.com/sla).
|
||||||
|
|
||||||
|
## Parental leave
|
||||||
|
|
||||||
|
At Infisical, we recognize that parental leave is a special and important time, significantly different from a typical vacation. We’re proud to offer parental leave to everyone, regardless of gender, and whether you’ve become a parent through childbirth or adoption.
|
||||||
|
|
||||||
|
For team members who have been with Infisical for over a year by the time of your child’s birth or adoption, you are eligible for up to 12 weeks of paid parental leave. This leave will be provided in one continuous block to allow you uninterrupted time with your family. If you have been with Infisical for less than a year, we will follow the parental leave provisions required by your local jurisdiction.
|
||||||
|
|
||||||
|
While we trust your judgment, parental leave is intended to be a distinct benefit and is not designed to be combined with our unlimited PTO policy. To ensure fairness and balance, we generally discourage combining parental leave with an extended vacation.
|
||||||
|
|
||||||
|
When you’re ready, please notify Maidul about your plans for parental leave, ideally at least four months in advance. This allows us to support you fully and arrange any necessary logistics, including salary adjustments and statutory paperwork.
|
||||||
|
|
||||||
|
We’re here to support you as you embark on this exciting new chapter in your life!
|
||||||
|
295
docs/cli/commands/dynamic-secrets.mdx
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
---
|
||||||
|
title: "infisical dynamic-secrets"
|
||||||
|
description: "Perform dynamic secret operations directly with the CLI"
|
||||||
|
---
|
||||||
|
|
||||||
|
```
|
||||||
|
infisical dynamic-secrets
|
||||||
|
```
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Dynamic secrets are unique secrets generated on demand based on the provided configuration settings. For more details, refer to [dynamics secrets section](/documentation/platform/dynamic-secrets/overview).
|
||||||
|
|
||||||
|
This command enables you to perform list, lease, renew lease, and revoke lease operations on dynamic secrets within your Infisical project.
|
||||||
|
|
||||||
|
### Sub-commands
|
||||||
|
|
||||||
|
<Accordion title="infisical dynamic-secrets">
|
||||||
|
Use this command to print out all of the dynamic secrets in your project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ infisical dynamic-secrets
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
<Accordion title="INFISICAL_TOKEN">
|
||||||
|
Used to fetch dynamic secrets via a [machine identity](/documentation/platform/identities/machine-identities) instead of logged-in credentials. Simply, export this variable in the terminal before running this command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=<identity-client-id> --client-secret=<identity-client-secret> --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages.
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="INFISICAL_DISABLE_UPDATE_CHECK">
|
||||||
|
Used to disable the check for new CLI versions. This can improve the time it takes to run this command. Recommended for production environments.
|
||||||
|
|
||||||
|
To use, simply export this variable in the terminal before running this command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
export INFISICAL_DISABLE_UPDATE_CHECK=true
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
<Accordion title="--projectId">
|
||||||
|
The project ID to fetch dynamic secrets from. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets --projectId=<project-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--token">
|
||||||
|
The authenticated token to fetch dynamic secrets from. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets --token=<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--env">
|
||||||
|
Used to select the environment name on which actions should be taken. Default
|
||||||
|
value: `dev`
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--path">
|
||||||
|
Use to select the project folder on which dynamic secrets will be accessed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets --path="/" --env=dev
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="infisical dynamic-secrets lease create">
|
||||||
|
This command is used to create a new lease for a dynamic secret.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ infisical dynamic-secrets lease create <dynamic-secret-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
<Accordion title="--env">
|
||||||
|
Used to select the environment name on which actions should be taken. Default
|
||||||
|
value: `dev`
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--plain">
|
||||||
|
The `--plain` flag will output dynamic secret lease credentials values without formatting, one per line.
|
||||||
|
Default value: `false`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease create dynamic-secret-postgres --plain
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--path">
|
||||||
|
The `--path` flag indicates which project folder dynamic secrets will be injected from.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease create <dynamic-secret-name> --path="/" --env=dev
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--projectId">
|
||||||
|
The project ID of the dynamic secrets to lease from. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease create <dynamic-secret-name> --projectId=<project-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--token">
|
||||||
|
The authenticated token to create dynamic secret leases. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease create <dynamic-secret-name> --token=<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--ttl">
|
||||||
|
The lease lifetime. If not provided, the default TTL of the dynamic secret root credential will be used.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease create <dynamic-secret-name> --ttl=<ttl>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="infisical dynamic-secrets lease list">
|
||||||
|
This command is used to list leases for a dynamic secret.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ infisical dynamic-secrets lease list <dynamic-secret-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
<Accordion title="--env">
|
||||||
|
Used to select the environment name on which actions should be taken. Default
|
||||||
|
value: `dev`
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--path">
|
||||||
|
The `--path` flag indicates which project folder dynamic secrets will be injected from.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease list <dynamic-secret-name> --path="/" --env=dev
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--projectId">
|
||||||
|
The project ID of the dynamic secrets to list leases from. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease list <dynamic-secret-name> --projectId=<project-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--token">
|
||||||
|
The authenticated token to list dynamic secret leases. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease list <dynamic-secret-name> --token=<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="infisical dynamic-secrets lease renew">
|
||||||
|
This command is used to renew a lease before it expires.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ infisical dynamic-secrets lease renew <lease-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
<Accordion title="--env">
|
||||||
|
Used to select the environment name on which actions should be taken. Default
|
||||||
|
value: `dev`
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--path">
|
||||||
|
The `--path` flag indicates which project folder dynamic secrets will be renewed from.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease renew <lease-id> --path="/" --env=dev
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--projectId">
|
||||||
|
The project ID of the dynamic secret's lease from. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease renew <lease-id> --projectId=<project-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--token">
|
||||||
|
The authenticated token to create dynamic secret leases. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease renew <lease-id> --token=<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--ttl">
|
||||||
|
The lease lifetime. If not provided, the default TTL of the dynamic secret root credential will be used.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease renew <lease-id> --ttl=<ttl>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="infisical dynamic-secrets lease delete">
|
||||||
|
This command is used to delete a lease.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ infisical dynamic-secrets lease delete <lease-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
<Accordion title="--env">
|
||||||
|
Used to select the environment name on which actions should be taken. Default
|
||||||
|
value: `dev`
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--path">
|
||||||
|
The `--path` flag indicates which project folder dynamic secrets will be deleted from.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease delete <lease-id> --path="/" --env=dev
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--projectId">
|
||||||
|
The project ID of the dynamic secret's lease from. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease delete <lease-id> --projectId=<project-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--token">
|
||||||
|
The authenticated token to delete dynamic secret leases. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease delete <lease-id> --token=<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
</Accordion>
|
@@ -74,22 +74,22 @@ Next, you will need to follow the steps listed below to add AWS KMS for your org
|
|||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Navigate to the organization settings and select the 'Encryption' tab.">
|
<Step title="Navigate to the organization settings and select the 'Encryption' tab.">
|
||||||

|

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

|

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

|

|
||||||
Choose 'AWS KMS' from the list of encryption providers.
|
Choose 'AWS KMS' from the list of encryption providers.
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Provide the inputs for AWS KMS">
|
<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>
|
<ParamField path="Alias" type="string" required>
|
||||||
Name for referencing the AWS KMS key within the organization.
|
Name for referencing the AWS KMS key within the organization.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Description" type="string">
|
<ParamField path="Description" type="string">
|
||||||
Short description of the AWS KMS key.
|
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
|
## 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.
|
||||||
|
@@ -3,11 +3,15 @@ title: "SCIM Overview"
|
|||||||
description: "Learn how to provision users for Infisical via SCIM."
|
description: "Learn how to provision users for Infisical via SCIM."
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
SCIM provisioning can only be enabled when either SAML or OIDC is setup for
|
||||||
|
the organization.
|
||||||
|
</Note>
|
||||||
<Info>
|
<Info>
|
||||||
SCIM provisioning is a paid feature.
|
SCIM provisioning is a paid feature. If you're using Infisical Cloud, then it
|
||||||
|
is available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
||||||
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
then you should contact sales@infisical.com to purchase an enterprise license
|
||||||
then you should contact sales@infisical.com to purchase an enterprise license to use it.
|
to use it.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
You can configure your organization in Infisical to have users and user groups be provisioned/deprovisioned using [SCIM](https://scim.cloud/#Implementations2) via providers like Okta, Azure, JumpCloud, etc.
|
You can configure your organization in Infisical to have users and user groups be provisioned/deprovisioned using [SCIM](https://scim.cloud/#Implementations2) via providers like Okta, Azure, JumpCloud, etc.
|
||||||
@@ -20,13 +24,3 @@ SCIM providers:
|
|||||||
- [Okta SCIM](/documentation/platform/scim/okta)
|
- [Okta SCIM](/documentation/platform/scim/okta)
|
||||||
- [Azure SCIM](/documentation/platform/scim/azure)
|
- [Azure SCIM](/documentation/platform/scim/azure)
|
||||||
- [JumpCloud SCIM](/documentation/platform/scim/jumpcloud)
|
- [JumpCloud SCIM](/documentation/platform/scim/jumpcloud)
|
||||||
|
|
||||||
**FAQ**
|
|
||||||
|
|
||||||
<AccordionGroup>
|
|
||||||
<Accordion title="Why do SCIM-provisioned users have to finish setting up their account?">
|
|
||||||
Infisical's SCIM implementation accounts for retaining the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps in the platform.
|
|
||||||
|
|
||||||
For this reason, SCIM-provisioned users are initialized but must finish setting up their account when logging in the first time by creating a master encryption/decryption key. With this implementation, IdPs and SCIM providers cannot and will not have access to the decryption key needed to decrypt your secrets.
|
|
||||||
</Accordion>
|
|
||||||
</AccordionGroup>
|
|
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 |
281
docs/integrations/platforms/kubernetes-csi.mdx
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
---
|
||||||
|
title: "Kubernetes CSI"
|
||||||
|
description: "How to use Infisical to inject secrets directly into Kubernetes pods."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Infisical CSI provider allows you to use Infisical with the [Secrets Store CSI driver](https://secrets-store-csi-driver.sigs.k8s.io) to inject secrets directly into your Kubernetes pods through a volume mount.
|
||||||
|
In contrast to the [Infisical Kubernetes Operator](https://infisical.com/docs/integrations/platforms/kubernetes), the Infisical CSI provider will allow you to sync Infisical secrets directly to pods as files, removing the need for Kubernetes secret resources.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph Secrets Management
|
||||||
|
SS(Infisical) --> CSP(Infisical CSI Provider)
|
||||||
|
CSP --> CSD(Secrets Store CSI Driver)
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Application
|
||||||
|
CSD --> V(Volume)
|
||||||
|
V <--> P(Pod)
|
||||||
|
end
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
The following features are supported by the Infisical CSI Provider:
|
||||||
|
|
||||||
|
- Integration with Secrets Store CSI Driver for direct pod mounting
|
||||||
|
- Authentication using Kubernetes service accounts via machine identities
|
||||||
|
- Auto-syncing secrets when enabled via CSI Driver
|
||||||
|
- Configurable secret paths and file mounting locations
|
||||||
|
- Installation via Helm
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
The Infisical CSI provider is only supported for Kubernetes clusters with version >= 1.20.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
Currently, the Infisical CSI provider only supports static secrets.
|
||||||
|
|
||||||
|
## Deploy to Kubernetes cluster
|
||||||
|
|
||||||
|
### Install Secrets Store CSI Driver
|
||||||
|
|
||||||
|
In order to use the Infisical CSI provider, you will first have to install the [Secrets Store CSI driver](https://secrets-store-csi-driver.sigs.k8s.io/getting-started/installation) to your cluster. It is important that you define
|
||||||
|
the audience value for token requests as demonstrated below. The Infisical CSI provider will **NOT WORK** if this is not set.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm install csi secrets-store-csi-driver/secrets-store-csi-driver \
|
||||||
|
--namespace=kube-system \
|
||||||
|
--set "tokenRequests[0].audience=infisical" \
|
||||||
|
--set enableSecretRotation=true \
|
||||||
|
--set rotationPollInterval=2m \
|
||||||
|
--set "syncSecret.enabled=true" \
|
||||||
|
```
|
||||||
|
|
||||||
|
The flags configure the following:
|
||||||
|
|
||||||
|
- `tokenRequests[0].audience=infisical`: Sets the audience value for service account token authentication (required)
|
||||||
|
- `enableSecretRotation=true`: Enables automatic secret updates from Infisical
|
||||||
|
- `rotationPollInterval=2m`: Checks for secret updates every 2 minutes
|
||||||
|
- `syncSecret.enabled=true`: Enables syncing secrets to Kubernetes secrets
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
If you do not wish to use the auto-syncing feature of the secrets store CSI
|
||||||
|
driver, you can omit the `enableSecretRotation` and the `rotationPollInterval`
|
||||||
|
flags. Do note that by default, secrets from Infisical are only fetched and
|
||||||
|
mounted during pod creation. If there are any changes made to the secrets in
|
||||||
|
Infisical, they will not propagate to the pods unless auto-syncing is enabled
|
||||||
|
for the CSI driver.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
### Install Infisical CSI Provider
|
||||||
|
|
||||||
|
You would then have to install the Infisical CSI provider to your cluster.
|
||||||
|
|
||||||
|
**Install the latest Infisical Helm repository**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
|
||||||
|
|
||||||
|
helm repo update
|
||||||
|
```
|
||||||
|
|
||||||
|
**Install the Helm Chart**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm install infisical-csi-provider infisical-helm-charts/infisical-csi-provider
|
||||||
|
```
|
||||||
|
|
||||||
|
For a list of all supported arguments for the helm installation, you can run the following:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm show values infisical-helm-charts/infisical-csi-provider
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
In order for the Infisical CSI provider to pull secrets from your Infisical project, you will have to configure
|
||||||
|
a machine identity with [Kubernetes authentication](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth) configured with your cluster.
|
||||||
|
You can refer to the documentation for setting it up [here](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth#guide).
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
The allowed audience field of the Kubernetes authentication settings should
|
||||||
|
match the audience specified for the Secrets Store CSI driver during
|
||||||
|
installation.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
### Creating Secret Provider Class
|
||||||
|
|
||||||
|
With the Secrets Store CSI driver and the Infisical CSI provider installed, create a Kubernetes [SecretProviderClass](https://secrets-store-csi-driver.sigs.k8s.io/concepts.html#secretproviderclass) resource to establish
|
||||||
|
the connection between the CSI driver and the Infisical CSI provider for secret retrieval. You can create as many Secret Provider Classes as needed for your cluster.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: secrets-store.csi.x-k8s.io/v1
|
||||||
|
kind: SecretProviderClass
|
||||||
|
metadata:
|
||||||
|
name: my-infisical-app-csi-provider
|
||||||
|
spec:
|
||||||
|
provider: infisical
|
||||||
|
parameters:
|
||||||
|
infisicalUrl: "https://app.infisical.com"
|
||||||
|
authMethod: "kubernetes"
|
||||||
|
identityId: "ad2f8c67-cbe2-417a-b5eb-1339776ec0b3"
|
||||||
|
projectId: "09eda1f8-85a3-47a9-8a6f-e27f133b2a36"
|
||||||
|
envSlug: "prod"
|
||||||
|
secrets: |
|
||||||
|
- secretPath: "/"
|
||||||
|
fileName: "dbPassword"
|
||||||
|
secretKey: "DB_PASSWORD"
|
||||||
|
- secretPath: "/app"
|
||||||
|
fileName: "appSecret"
|
||||||
|
secretKey: "APP_SECRET"
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
The SecretProviderClass should be provisioned in the same namespace as the pod
|
||||||
|
you intend to mount secrets to.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
#### Supported Parameters
|
||||||
|
|
||||||
|
<Accordion title="infisicalUrl">
|
||||||
|
The base URL of your Infisical instance. If you're using Infisical Cloud US,
|
||||||
|
this should be set to `https://app.infisical.com`. If you're using Infisical
|
||||||
|
Cloud EU, then this should be set to `https://eu.infisical.com`.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="caCertificate">
|
||||||
|
The CA certificate of the Infisical instance in order to establish SSL/TLS
|
||||||
|
when the instance uses a private or self-signed certificate. Unless necessary,
|
||||||
|
this should be omitted.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="authMethod">
|
||||||
|
The auth method to use for authenticating the Infisical CSI provider with
|
||||||
|
Infisical. For now, the only supported method is `kubernetes`.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="identityId">
|
||||||
|
The ID of the machine identity to use for authenticating the Infisical CSI
|
||||||
|
provider with your Infisical organization. This should be the machine identity
|
||||||
|
configured with Kubernetes authentication.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="projectId">
|
||||||
|
The project ID of the Infisical project to pull secrets from.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="envSlug">
|
||||||
|
The slug of the project environment to pull secrets from.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="secrets">
|
||||||
|
An array that defines which secrets to retrieve and how to mount them. Each
|
||||||
|
entry requires three properties: `secretPath` and `secretKey` work together to
|
||||||
|
identify the source secret to fetch, while `fileName` specifies the path where
|
||||||
|
the secret's value will be mounted within the pod's filesystem.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="audience">
|
||||||
|
The custom audience value configured for the CSI driver. This defaults to
|
||||||
|
`infisical`.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
### Using Secret Provider Class
|
||||||
|
|
||||||
|
A pod can use the Secret Provider Class by mounting it as a CSI volume:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: nginx-secrets-store
|
||||||
|
labels:
|
||||||
|
app: nginx
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: nginx
|
||||||
|
image: nginx
|
||||||
|
volumeMounts:
|
||||||
|
- name: secrets-store-inline
|
||||||
|
mountPath: "/mnt/secrets-store"
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: secrets-store-inline
|
||||||
|
csi:
|
||||||
|
driver: secrets-store.csi.k8s.io
|
||||||
|
readOnly: true
|
||||||
|
volumeAttributes:
|
||||||
|
secretProviderClass: "my-infisical-app-csi-provider"
|
||||||
|
```
|
||||||
|
|
||||||
|
When the pod is created, the secrets are mounted as individual files in the /mnt/secrets-store directory.
|
||||||
|
|
||||||
|
### Verifying Secret Mounts
|
||||||
|
|
||||||
|
To verify your secrets are mounted correctly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check pod status
|
||||||
|
kubectl get pod nginx-secrets-store
|
||||||
|
|
||||||
|
# View mounted secrets
|
||||||
|
kubectl exec -it nginx-secrets-store -- ls -l /mnt/secrets-store
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
To troubleshoot issues with the Infisical CSI provider, refer to the logs of the Infisical CSI provider running on the same node as your pod.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl logs infisical-csi-provider-7x44t
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also refer to the logs of the secrets store CSI driver. Modify the command below with the appropriate pod and namespace of your secrets store CSI driver installation.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl logs csi-secrets-store-csi-driver-7h4jp -n=kube-system
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common issues include:**
|
||||||
|
|
||||||
|
- Mismatch in the audience value of the CSI driver with the machine identity's Kubernetes auth configuration
|
||||||
|
- SecretProviderClass in the wrong namespace
|
||||||
|
- Invalid machine identity configuration
|
||||||
|
- Incorrect secret paths or keys
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
For additional guidance on setting this up for your production cluster, you can refer to the Secrets Store CSI driver documentation [here](https://secrets-store-csi-driver.sigs.k8s.io/topics/best-practices).
|
||||||
|
|
||||||
|
## Frequently Asked Questions
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Is it possible to sync Infisical secrets as environment variables?">
|
||||||
|
Yes, but it requires an indirect approach:
|
||||||
|
|
||||||
|
1. First enable syncing to Kubernetes secrets by setting `syncSecret.enabled=true` in the CSI driver installation
|
||||||
|
2. Configure the Secret Provider Class to sync specific secrets to Kubernetes secrets
|
||||||
|
3. Use the resulting Kubernetes secrets in your pod's environment variables
|
||||||
|
|
||||||
|
This means secrets are first synced to Kubernetes secrets before they can be used as environment variables. You can find detailed examples in the [Secrets Store CSI driver documentation](https://secrets-store-csi-driver.sigs.k8s.io/topics/set-as-env-var).
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Do I have to list out every Infisical single secret that I want to sync?">
|
||||||
|
Yes, you will need to explicitly list each secret you want to sync in the
|
||||||
|
Secret Provider Class configuration. This is a common requirement across all
|
||||||
|
CSI providers as the Secrets Store CSI Driver architecture requires specific
|
||||||
|
mapping of secrets to their mounted file locations.
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
@@ -32,7 +32,10 @@
|
|||||||
"thumbsRating": true
|
"thumbsRating": true
|
||||||
},
|
},
|
||||||
"api": {
|
"api": {
|
||||||
"baseUrl": ["https://app.infisical.com", "http://localhost:8080"]
|
"baseUrl": [
|
||||||
|
"https://app.infisical.com",
|
||||||
|
"http://localhost:8080"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"topbarLinks": [
|
"topbarLinks": [
|
||||||
{
|
{
|
||||||
@@ -73,7 +76,9 @@
|
|||||||
"documentation/getting-started/introduction",
|
"documentation/getting-started/introduction",
|
||||||
{
|
{
|
||||||
"group": "Quickstart",
|
"group": "Quickstart",
|
||||||
"pages": ["documentation/guides/local-development"]
|
"pages": [
|
||||||
|
"documentation/guides/local-development"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Guides",
|
"group": "Guides",
|
||||||
@@ -127,7 +132,8 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"documentation/platform/kms-configuration/overview",
|
"documentation/platform/kms-configuration/overview",
|
||||||
"documentation/platform/kms-configuration/aws-kms",
|
"documentation/platform/kms-configuration/aws-kms",
|
||||||
"documentation/platform/kms-configuration/aws-hsm"
|
"documentation/platform/kms-configuration/aws-hsm",
|
||||||
|
"documentation/platform/kms-configuration/gcp-kms"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -316,6 +322,7 @@
|
|||||||
"cli/commands/init",
|
"cli/commands/init",
|
||||||
"cli/commands/run",
|
"cli/commands/run",
|
||||||
"cli/commands/secrets",
|
"cli/commands/secrets",
|
||||||
|
"cli/commands/dynamic-secrets",
|
||||||
"cli/commands/export",
|
"cli/commands/export",
|
||||||
"cli/commands/token",
|
"cli/commands/token",
|
||||||
"cli/commands/service-token",
|
"cli/commands/service-token",
|
||||||
@@ -344,6 +351,7 @@
|
|||||||
"group": "Container orchestrators",
|
"group": "Container orchestrators",
|
||||||
"pages": [
|
"pages": [
|
||||||
"integrations/platforms/kubernetes",
|
"integrations/platforms/kubernetes",
|
||||||
|
"integrations/platforms/kubernetes-csi",
|
||||||
"integrations/platforms/docker-swarm-with-agent",
|
"integrations/platforms/docker-swarm-with-agent",
|
||||||
"integrations/platforms/ecs-with-agent"
|
"integrations/platforms/ecs-with-agent"
|
||||||
]
|
]
|
||||||
@@ -459,20 +467,24 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Build Tool Integrations",
|
"group": "Build Tool Integrations",
|
||||||
"pages": ["integrations/build-tools/gradle"]
|
"pages": [
|
||||||
|
"integrations/build-tools/gradle"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "",
|
"group": "",
|
||||||
"pages": ["sdks/overview"]
|
"pages": [
|
||||||
|
"sdks/overview"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "SDK's",
|
"group": "SDK's",
|
||||||
"pages": [
|
"pages": [
|
||||||
"sdks/languages/node",
|
"sdks/languages/node",
|
||||||
"sdks/languages/python",
|
"sdks/languages/python",
|
||||||
|
"sdks/languages/java",
|
||||||
"sdks/languages/go",
|
"sdks/languages/go",
|
||||||
"sdks/languages/ruby",
|
"sdks/languages/ruby",
|
||||||
"sdks/languages/java",
|
|
||||||
"sdks/languages/csharp"
|
"sdks/languages/csharp"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -483,7 +495,9 @@
|
|||||||
"api-reference/overview/authentication",
|
"api-reference/overview/authentication",
|
||||||
{
|
{
|
||||||
"group": "Examples",
|
"group": "Examples",
|
||||||
"pages": ["api-reference/overview/examples/integration"]
|
"pages": [
|
||||||
|
"api-reference/overview/examples/integration"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -758,11 +772,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Service Tokens",
|
"group": "Service Tokens",
|
||||||
"pages": ["api-reference/endpoints/service-tokens/get"]
|
"pages": [
|
||||||
|
"api-reference/endpoints/service-tokens/get"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Audit Logs",
|
"group": "Audit Logs",
|
||||||
"pages": ["api-reference/endpoints/audit-logs/export-audit-log"]
|
"pages": [
|
||||||
|
"api-reference/endpoints/audit-logs/export-audit-log"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -861,7 +879,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "",
|
"group": "",
|
||||||
"pages": ["changelog/overview"]
|
"pages": [
|
||||||
|
"changelog/overview"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Contributing",
|
"group": "Contributing",
|
||||||
@@ -885,7 +905,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Contributing to SDK",
|
"group": "Contributing to SDK",
|
||||||
"pages": ["contributing/sdk/developing"]
|
"pages": [
|
||||||
|
"contributing/sdk/developing"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -909,13 +931,22 @@
|
|||||||
{
|
{
|
||||||
"title": "PRODUCT",
|
"title": "PRODUCT",
|
||||||
"links": [
|
"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",
|
"label": "Share Secrets",
|
||||||
"url": "https://app.infisical.com/share-secret"
|
"url": "https://app.infisical.com/share-secret"
|
||||||
},
|
},
|
||||||
{ "label": "Pricing", "url": "https://infisical.com/pricing" },
|
{
|
||||||
|
"label": "Pricing",
|
||||||
|
"url": "https://infisical.com/pricing"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Security",
|
"label": "Security",
|
||||||
"url": "https://infisical.com/docs/internals/security"
|
"url": "https://infisical.com/docs/internals/security"
|
||||||
@@ -1059,4 +1090,4 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,9 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: "Infisical Java SDK"
|
title: "Infisical Java SDK"
|
||||||
sidebarTitle: "Java"
|
sidebarTitle: "Java"
|
||||||
|
url: "https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-nodejs-sdk"
|
||||||
icon: "java"
|
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.
|
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)
|
- [Maven Package](https://github.com/Infisical/sdk/packages/2019741)
|
||||||
@@ -568,4 +571,5 @@ String decryptedString = client.decryptSymmetric(decryptOptions);
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
#### Returns (string)
|
#### 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">
|
<Card href="https://github.com/Infisical/python-sdk-official" title="Python" icon="python" color="#4c8abe">
|
||||||
Manage secrets for your Python application on demand
|
Manage secrets for your Python application on demand
|
||||||
</Card>
|
</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
|
Manage secrets for your Java application on demand
|
||||||
</Card>
|
</Card>
|
||||||
<Card href="/sdks/languages/go" title="Go" icon="golang" color="#367B99">
|
<Card href="/sdks/languages/go" title="Go" icon="golang" color="#367B99">
|
||||||
|
8
frontend/package-lock.json
generated
@@ -89,7 +89,7 @@
|
|||||||
"react-mailchimp-subscribe": "^2.1.3",
|
"react-mailchimp-subscribe": "^2.1.3",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-select": "^5.8.1",
|
"react-select": "^5.8.3",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^9.1.3",
|
||||||
"sanitize-html": "^2.12.1",
|
"sanitize-html": "^2.12.1",
|
||||||
@@ -21259,9 +21259,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-select": {
|
"node_modules/react-select": {
|
||||||
"version": "5.8.1",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.3.tgz",
|
||||||
"integrity": "sha512-RT1CJmuc+ejqm5MPgzyZujqDskdvB9a9ZqrdnVLsvAHjJ3Tj0hELnLeVPQlmYdVKCdCpxanepl6z7R5KhXhWzg==",
|
"integrity": "sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.0",
|
"@babel/runtime": "^7.12.0",
|
||||||
|
@@ -162,4 +162,4 @@
|
|||||||
"tailwindcss": "3.2",
|
"tailwindcss": "3.2",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,11 +1,17 @@
|
|||||||
|
import { ParsedUrlQuery } from "querystring";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { faAngleRight, faLock } from "@fortawesome/free-solid-svg-icons";
|
import { faAngleRight, faCheck, faCopy, faLock } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
import { Select, SelectItem, Tooltip } from "../v2";
|
import { createNotification } from "../notifications";
|
||||||
|
import { IconButton, Select, SelectItem, Tooltip } from "../v2";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pageName: string;
|
pageName: string;
|
||||||
@@ -50,6 +56,10 @@ export default function NavHeader({
|
|||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
|
const [isCopied, { timedToggle: toggleIsCopied }] = useToggle(false);
|
||||||
|
const [isHoveringCopyButton, setIsHoveringCopyButton] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const secretPathSegments = secretPath.split("/").filter(Boolean);
|
const secretPathSegments = secretPath.split("/").filter(Boolean);
|
||||||
@@ -132,8 +142,10 @@ export default function NavHeader({
|
|||||||
)}
|
)}
|
||||||
{isFolderMode &&
|
{isFolderMode &&
|
||||||
secretPathSegments?.map((folderName, index) => {
|
secretPathSegments?.map((folderName, index) => {
|
||||||
const query = { ...router.query };
|
const query: ParsedUrlQuery & { secretPath: string } = {
|
||||||
query.secretPath = `/${secretPathSegments.slice(0, index + 1).join("/")}`;
|
...router.query,
|
||||||
|
secretPath: `/${secretPathSegments.slice(0, index + 1).join("/")}`
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -142,14 +154,59 @@ export default function NavHeader({
|
|||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
|
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
|
||||||
{index + 1 === secretPathSegments?.length ? (
|
{index + 1 === secretPathSegments?.length ? (
|
||||||
<span className="text-sm font-semibold text-bunker-300">{folderName}</span>
|
<div className="flex items-center space-x-2">
|
||||||
|
<span
|
||||||
|
className={twMerge(
|
||||||
|
"text-sm font-semibold transition-all",
|
||||||
|
isHoveringCopyButton ? "text-bunker-200" : "text-bunker-300"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{folderName}
|
||||||
|
</span>
|
||||||
|
<Tooltip
|
||||||
|
className="relative right-2"
|
||||||
|
position="bottom"
|
||||||
|
content="Copy secret path"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="copy"
|
||||||
|
onMouseEnter={() => setIsHoveringCopyButton(true)}
|
||||||
|
onMouseLeave={() => setIsHoveringCopyButton(false)}
|
||||||
|
onClick={() => {
|
||||||
|
if (isCopied) return;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(query.secretPath);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Copied secret path to clipboard",
|
||||||
|
type: "info"
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleIsCopied(2000);
|
||||||
|
}}
|
||||||
|
className="hover:bg-bunker-100/10"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={!isCopied ? faCopy : faCheck}
|
||||||
|
size="sm"
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
passHref
|
passHref
|
||||||
legacyBehavior
|
legacyBehavior
|
||||||
href={{ pathname: "/project/[id]/secrets/[env]", query }}
|
href={{ pathname: "/project/[id]/secrets/[env]", query }}
|
||||||
>
|
>
|
||||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
|
<a
|
||||||
|
className={twMerge(
|
||||||
|
"text-sm font-semibold transition-all hover:text-primary",
|
||||||
|
isHoveringCopyButton ? "text-primary" : "text-primary/80"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{folderName}
|
{folderName}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
@@ -1,18 +1,60 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toastify";
|
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 = {
|
export type TNotification = {
|
||||||
title?: string;
|
title?: string;
|
||||||
text: ReactNode;
|
text: ReactNode;
|
||||||
children?: 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 (
|
return (
|
||||||
<div className="msg-container">
|
<div className="msg-container">
|
||||||
{title && <div className="text-md mb-1 font-medium">{title}</div>}
|
{title && <div className="text-md mb-1 font-medium">{title}</div>}
|
||||||
<div className={title ? "text-sm text-neutral-400" : "text-md"}>{text}</div>
|
<div className={title ? "text-sm text-neutral-400" : "text-md"}>{text}</div>
|
||||||
{children && <div className="mt-2">{children}</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>
|
</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";
|
@@ -0,0 +1,68 @@
|
|||||||
|
import { GroupBase } from "react-select";
|
||||||
|
import ReactSelectCreatable, { CreatableProps } from "react-select/creatable";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "../Select/components";
|
||||||
|
|
||||||
|
export const CreatableSelect = <T,>({
|
||||||
|
isMulti,
|
||||||
|
closeMenuOnSelect,
|
||||||
|
...props
|
||||||
|
}: CreatableProps<T, boolean, GroupBase<T>>) => {
|
||||||
|
return (
|
||||||
|
<ReactSelectCreatable
|
||||||
|
isMulti={isMulti}
|
||||||
|
closeMenuOnSelect={closeMenuOnSelect ?? !isMulti}
|
||||||
|
hideSelectedOptions={false}
|
||||||
|
unstyled
|
||||||
|
styles={{
|
||||||
|
input: (base) => ({
|
||||||
|
...base,
|
||||||
|
"input:focus": {
|
||||||
|
boxShadow: "none"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
multiValueLabel: (base) => ({
|
||||||
|
...base,
|
||||||
|
whiteSpace: "normal",
|
||||||
|
overflow: "visible"
|
||||||
|
}),
|
||||||
|
control: (base) => ({
|
||||||
|
...base,
|
||||||
|
transition: "none"
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
|
||||||
|
classNames={{
|
||||||
|
container: () => "w-full font-inter",
|
||||||
|
control: ({ isFocused }) =>
|
||||||
|
twMerge(
|
||||||
|
isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
|
||||||
|
"border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 hover:cursor-pointer"
|
||||||
|
),
|
||||||
|
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
|
||||||
|
input: () => "pl-1 py-0.5",
|
||||||
|
valueContainer: () => `p-1 max-h-[14rem] ${isMulti ? "!overflow-y-scroll" : ""} gap-1`,
|
||||||
|
singleValue: () => "leading-7 ml-1",
|
||||||
|
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
|
||||||
|
multiValueLabel: () => "leading-6 text-sm",
|
||||||
|
multiValueRemove: () => "hover:text-red text-bunker-400",
|
||||||
|
indicatorsContainer: () => "p-1 gap-1",
|
||||||
|
clearIndicator: () => "p-1 hover:text-red text-bunker-400",
|
||||||
|
indicatorSeparator: () => "bg-bunker-400",
|
||||||
|
dropdownIndicator: () => "text-bunker-200 p-1",
|
||||||
|
menu: () =>
|
||||||
|
"mt-2 border text-sm text-mineshaft-200 bg-mineshaft-900 border-mineshaft-600 rounded-md",
|
||||||
|
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
|
||||||
|
option: ({ isFocused, isSelected }) =>
|
||||||
|
twMerge(
|
||||||
|
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
|
||||||
|
isSelected && "text-mineshaft-200",
|
||||||
|
"hover:cursor-pointer text-xs px-3 py-2"
|
||||||
|
),
|
||||||
|
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
1
frontend/src/components/v2/CreatableSelect/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./CreatableSelect";
|
@@ -14,6 +14,7 @@ export type DatePickerProps = Omit<DayPickerProps, "selected"> & {
|
|||||||
onChange: (date?: Date) => void;
|
onChange: (date?: Date) => void;
|
||||||
popUpProps: PopoverProps;
|
popUpProps: PopoverProps;
|
||||||
popUpContentProps: PopoverContentProps;
|
popUpContentProps: PopoverContentProps;
|
||||||
|
dateFormat?: "PPP" | "PP" | "P"; // extend as needed
|
||||||
};
|
};
|
||||||
|
|
||||||
// Doc: https://react-day-picker.js.org/
|
// Doc: https://react-day-picker.js.org/
|
||||||
@@ -22,6 +23,7 @@ export const DatePicker = ({
|
|||||||
onChange,
|
onChange,
|
||||||
popUpProps,
|
popUpProps,
|
||||||
popUpContentProps,
|
popUpContentProps,
|
||||||
|
dateFormat = "PPP",
|
||||||
...props
|
...props
|
||||||
}: DatePickerProps) => {
|
}: DatePickerProps) => {
|
||||||
const [timeValue, setTimeValue] = useState<string>(value ? format(value, "HH:mm") : "00:00");
|
const [timeValue, setTimeValue] = useState<string>(value ? format(value, "HH:mm") : "00:00");
|
||||||
@@ -53,7 +55,7 @@ export const DatePicker = ({
|
|||||||
<Popover {...popUpProps}>
|
<Popover {...popUpProps}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="outline_bg" leftIcon={<FontAwesomeIcon icon={faCalendar} />}>
|
<Button variant="outline_bg" leftIcon={<FontAwesomeIcon icon={faCalendar} />}>
|
||||||
{value ? format(value, "PPP") : "Pick a date and time"}
|
{value ? format(value, dateFormat) : "Pick a date and time"}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-fit p-2" {...popUpContentProps}>
|
<PopoverContent className="w-fit p-2" {...popUpContentProps}>
|
||||||
|
@@ -1,52 +1,14 @@
|
|||||||
import Select, {
|
import Select, { Props } from "react-select";
|
||||||
ClearIndicatorProps,
|
|
||||||
components,
|
|
||||||
DropdownIndicatorProps,
|
|
||||||
MultiValueRemoveProps,
|
|
||||||
OptionProps,
|
|
||||||
Props
|
|
||||||
} from "react-select";
|
|
||||||
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
|
|
||||||
import { faChevronDown, faXmark } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
const DropdownIndicator = <T,>(props: DropdownIndicatorProps<T>) => {
|
import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "../Select/components";
|
||||||
return (
|
|
||||||
<components.DropdownIndicator {...props}>
|
|
||||||
<FontAwesomeIcon icon={faChevronDown} size="xs" />
|
|
||||||
</components.DropdownIndicator>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ClearIndicator = <T,>(props: ClearIndicatorProps<T>) => {
|
export const FilterableSelect = <T,>({
|
||||||
return (
|
isMulti,
|
||||||
<components.ClearIndicator {...props}>
|
closeMenuOnSelect,
|
||||||
<FontAwesomeIcon icon={faCircleXmark} />
|
tabSelectsValue = false,
|
||||||
</components.ClearIndicator>
|
...props
|
||||||
);
|
}: Props<T>) => (
|
||||||
};
|
|
||||||
|
|
||||||
const MultiValueRemove = (props: MultiValueRemoveProps) => {
|
|
||||||
return (
|
|
||||||
<components.MultiValueRemove {...props}>
|
|
||||||
<FontAwesomeIcon icon={faXmark} size="xs" />
|
|
||||||
</components.MultiValueRemove>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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" />
|
|
||||||
)}
|
|
||||||
</components.Option>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: Props<T>) => (
|
|
||||||
<Select
|
<Select
|
||||||
isMulti={isMulti}
|
isMulti={isMulti}
|
||||||
closeMenuOnSelect={closeMenuOnSelect ?? !isMulti}
|
closeMenuOnSelect={closeMenuOnSelect ?? !isMulti}
|
||||||
@@ -69,34 +31,48 @@ export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: P
|
|||||||
transition: "none"
|
transition: "none"
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
|
tabSelectsValue={tabSelectsValue}
|
||||||
|
components={{
|
||||||
|
DropdownIndicator,
|
||||||
|
ClearIndicator,
|
||||||
|
MultiValueRemove,
|
||||||
|
Option,
|
||||||
|
...props.components
|
||||||
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
container: () => "w-full font-inter",
|
container: ({ isDisabled }) =>
|
||||||
control: ({ isFocused }) =>
|
twMerge("w-full text-sm font-inter", isDisabled && "!pointer-events-auto opacity-50"),
|
||||||
|
control: ({ isFocused, isDisabled }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
|
isFocused ? "border-primary-400/50" : "border-mineshaft-600 ",
|
||||||
"border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 hover:cursor-pointer"
|
`border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 ${
|
||||||
|
isDisabled ? "!cursor-not-allowed" : "hover:border-gray-400 hover:cursor-pointer"
|
||||||
|
} `
|
||||||
),
|
),
|
||||||
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
|
placeholder: () =>
|
||||||
input: () => "pl-1 py-0.5",
|
`${isMulti ? "py-[0.22rem]" : "leading-7"} text-mineshaft-400 text-sm pl-1`,
|
||||||
|
input: () => "pl-1",
|
||||||
valueContainer: () =>
|
valueContainer: () =>
|
||||||
`p-1 max-h-[14rem] ${isMulti ? "!overflow-y-auto thin-scrollbar" : ""} gap-1`,
|
`px-1 max-h-[8.2rem] ${
|
||||||
|
isMulti ? "!overflow-y-auto thin-scrollbar py-1" : "py-[0.1rem]"
|
||||||
|
} gap-1`,
|
||||||
singleValue: () => "leading-7 ml-1",
|
singleValue: () => "leading-7 ml-1",
|
||||||
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
|
multiValue: () => "bg-mineshaft-600 text-sm rounded items-center py-0.5 px-2 gap-1.5",
|
||||||
multiValueLabel: () => "leading-6 text-sm",
|
multiValueLabel: () => "leading-6 text-sm",
|
||||||
multiValueRemove: () => "hover:text-red text-bunker-400",
|
multiValueRemove: () => "hover:text-red text-bunker-400",
|
||||||
indicatorsContainer: () => "p-1 gap-1",
|
indicatorsContainer: () => "p-1 gap-1",
|
||||||
clearIndicator: () => "p-1 hover:text-red text-bunker-400",
|
clearIndicator: () => "p-1 hover:text-red text-bunker-400",
|
||||||
indicatorSeparator: () => "bg-bunker-400",
|
indicatorSeparator: () => "bg-bunker-400",
|
||||||
dropdownIndicator: () => "text-bunker-200 p-1",
|
dropdownIndicator: () => "text-bunker-200 p-1",
|
||||||
|
menuList: () => "flex flex-col gap-1",
|
||||||
menu: () =>
|
menu: () =>
|
||||||
"mt-2 border text-sm text-mineshaft-200 bg-mineshaft-900 border-mineshaft-600 rounded-md",
|
"my-2 p-2 border text-sm text-mineshaft-200 thin-scrollbar bg-mineshaft-900 border-mineshaft-600 rounded-md",
|
||||||
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
|
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
|
||||||
option: ({ isFocused, isSelected }) =>
|
option: ({ isFocused, isSelected }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
|
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
|
||||||
isSelected && "text-mineshaft-200",
|
isSelected && "text-mineshaft-200",
|
||||||
"hover:cursor-pointer text-xs px-3 py-2"
|
"hover:cursor-pointer rounded text-xs px-3 py-2"
|
||||||
),
|
),
|
||||||
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
|
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
|
||||||
}}
|
}}
|
||||||
|
@@ -54,7 +54,7 @@ export const Pagination = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{startAdornment}
|
{startAdornment}
|
||||||
<div className="ml-auto mr-6 flex items-center space-x-2">
|
<div className={twMerge("mr-4 flex items-center space-x-2", startAdornment && "ml-auto")}>
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
{(page - 1) * perPage + 1} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
|
{(page - 1) * perPage + 1} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
|
||||||
</div>
|
</div>
|
||||||
|
47
frontend/src/components/v2/Select/components/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import {
|
||||||
|
ClearIndicatorProps,
|
||||||
|
components,
|
||||||
|
DropdownIndicatorProps,
|
||||||
|
MultiValueRemoveProps,
|
||||||
|
OptionProps
|
||||||
|
} from "react-select";
|
||||||
|
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { faChevronDown, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
export const DropdownIndicator = <T,>(props: DropdownIndicatorProps<T>) => {
|
||||||
|
return (
|
||||||
|
<components.DropdownIndicator {...props}>
|
||||||
|
<FontAwesomeIcon icon={faChevronDown} size="xs" />
|
||||||
|
</components.DropdownIndicator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClearIndicator = <T,>(props: ClearIndicatorProps<T>) => {
|
||||||
|
return (
|
||||||
|
<components.ClearIndicator {...props}>
|
||||||
|
<FontAwesomeIcon icon={faCircleXmark} />
|
||||||
|
</components.ClearIndicator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MultiValueRemove = (props: MultiValueRemoveProps) => {
|
||||||
|
return (
|
||||||
|
<components.MultiValueRemove {...props}>
|
||||||
|
<FontAwesomeIcon icon={faXmark} size="xs" />
|
||||||
|
</components.MultiValueRemove>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Option = <T,>({ isSelected, children, ...props }: OptionProps<T>) => {
|
||||||
|
return (
|
||||||
|
<components.Option isSelected={isSelected} {...props}>
|
||||||
|
<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";
|
position?: "top" | "bottom" | "left" | "right";
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
center?: boolean;
|
center?: boolean;
|
||||||
|
size?: "sm" | "md";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Tooltip = ({
|
export const Tooltip = ({
|
||||||
@@ -26,6 +27,7 @@ export const Tooltip = ({
|
|||||||
asChild = true,
|
asChild = true,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
position = "top",
|
position = "top",
|
||||||
|
size = "md",
|
||||||
...props
|
...props
|
||||||
}: TooltipProps) =>
|
}: TooltipProps) =>
|
||||||
// just render children if tooltip content is empty
|
// just render children if tooltip content is empty
|
||||||
@@ -43,7 +45,7 @@ export const Tooltip = ({
|
|||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
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=top]:animate-slideDownAndFade
|
||||||
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
|
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
|
||||||
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
|
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
|
||||||
@@ -51,6 +53,8 @@ export const Tooltip = ({
|
|||||||
`,
|
`,
|
||||||
isDisabled && "!hidden",
|
isDisabled && "!hidden",
|
||||||
center && "text-center",
|
center && "text-center",
|
||||||
|
size === "sm" && "rounded-sm py-1 px-2 text-xs",
|
||||||
|
size === "md" && "rounded-md py-2 px-4 text-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
12
frontend/src/helpers/members.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { TWorkspaceUser } from "@app/hooks/api/users/types";
|
||||||
|
|
||||||
|
export const getMemberLabel = (member: TWorkspaceUser) => {
|
||||||
|
const {
|
||||||
|
inviteEmail,
|
||||||
|
user: { firstName, lastName, username, email }
|
||||||
|
} = member;
|
||||||
|
|
||||||
|
return firstName || lastName
|
||||||
|
? `${firstName ?? ""} ${lastName ?? ""}`.trim()
|
||||||
|
: username || email || inviteEmail;
|
||||||
|
};
|
@@ -1,4 +1,4 @@
|
|||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
import { ProjectMembershipRole, TOrgRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
enum OrgMembershipRole {
|
enum OrgMembershipRole {
|
||||||
Admin = "admin",
|
Admin = "admin",
|
||||||
@@ -23,3 +23,8 @@ export const formatProjectRoleName = (name: string) => {
|
|||||||
|
|
||||||
export const isCustomProjectRole = (slug: string) =>
|
export const isCustomProjectRole = (slug: string) =>
|
||||||
!Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
|
!Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
|
||||||
|
|
||||||
|
export const findOrgMembershipRole = (roles: TOrgRole[], roleIdOrSlug: string) =>
|
||||||
|
isCustomOrgRole(roleIdOrSlug)
|
||||||
|
? roles.find((r) => r.id === roleIdOrSlug)
|
||||||
|
: roles.find((r) => r.slug === roleIdOrSlug);
|
||||||
|
@@ -177,11 +177,21 @@ export const useGetProjectSecretsOverview = (
|
|||||||
}),
|
}),
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (axios.isAxiosError(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({
|
createNotification({
|
||||||
title: "Error fetching secret details",
|
title: "Error fetching secret details",
|
||||||
type: "error",
|
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) => {
|
onError: (error) => {
|
||||||
if (axios.isAxiosError(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({
|
createNotification({
|
||||||
title: "Error fetching secret details",
|
title: "Error fetching secret details",
|
||||||
type: "error",
|
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) => {
|
onError: (error) => {
|
||||||
if (axios.isAxiosError(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({
|
createNotification({
|
||||||
title: "Error fetching secrets deep search",
|
title: "Error fetching secrets deep search",
|
||||||
type: "error",
|
type: "error",
|
||||||
text: serverResponse.message
|
text: message,
|
||||||
|
copyActions: [
|
||||||
|
{
|
||||||
|
value: requestId,
|
||||||
|
name: "Request ID",
|
||||||
|
label: `Request ID: ${requestId}`
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
export {
|
export {
|
||||||
useAddExternalKms,
|
useAddExternalKms,
|
||||||
|
useExternalKmsFetchGcpKeys,
|
||||||
useLoadProjectKmsBackup,
|
useLoadProjectKmsBackup,
|
||||||
useRemoveExternalKms,
|
useRemoveExternalKms,
|
||||||
useUpdateExternalKms,
|
useUpdateExternalKms,
|
||||||
|
@@ -3,7 +3,13 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { kmsKeys } from "./queries";
|
import { kmsKeys } from "./queries";
|
||||||
import { AddExternalKmsType, KmsType } from "./types";
|
import {
|
||||||
|
AddExternalKmsType,
|
||||||
|
ExternalKmsGcpSchemaType,
|
||||||
|
KmsGcpKeyFetchAuthType,
|
||||||
|
KmsType,
|
||||||
|
UpdateExternalKmsType
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const useAddExternalKms = (orgId: string) => {
|
export const useAddExternalKms = (orgId: string) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -33,7 +39,7 @@ export const useUpdateExternalKms = (orgId: string) => {
|
|||||||
provider
|
provider
|
||||||
}: {
|
}: {
|
||||||
kmsId: string;
|
kmsId: string;
|
||||||
} & AddExternalKmsType) => {
|
} & UpdateExternalKmsType) => {
|
||||||
const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, {
|
const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, {
|
||||||
name,
|
name,
|
||||||
description,
|
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 {
|
export enum ExternalKmsProvider {
|
||||||
AWS = "aws"
|
Aws = "aws",
|
||||||
|
Gcp = "gcp"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const INTERNAL_KMS_KEY_ID = "internal";
|
export const INTERNAL_KMS_KEY_ID = "internal";
|
||||||
@@ -44,6 +45,10 @@ export enum KmsAwsCredentialType {
|
|||||||
AssumeRole = "assume-role",
|
AssumeRole = "assume-role",
|
||||||
AccessKey = "access-key"
|
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({
|
export const ExternalKmsAwsSchema = z.object({
|
||||||
credential: z
|
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", [
|
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({
|
export const AddExternalKmsSchema = z.object({
|
||||||
@@ -100,3 +131,71 @@ export const AddExternalKmsSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type AddExternalKmsType = z.infer<typeof AddExternalKmsSchema>;
|
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"
|
||||||
|
}
|
||||||
|
@@ -52,7 +52,7 @@ export type Invoice = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PmtMethod = {
|
export type PmtMethod = {
|
||||||
id: string;
|
_id: string;
|
||||||
brand: string;
|
brand: string;
|
||||||
exp_month: number;
|
exp_month: number;
|
||||||
exp_year: number;
|
exp_year: number;
|
||||||
|
@@ -117,11 +117,21 @@ export const useGetProjectSecrets = ({
|
|||||||
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (axios.isAxiosError(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({
|
createNotification({
|
||||||
title: "Error fetching secrets",
|
title: "Error fetching secrets",
|
||||||
type: "error",
|
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),
|
enabled: Boolean(workspaceId && environment),
|
||||||
onError: (error: unknown) => {
|
onError: (error: unknown) => {
|
||||||
if (axios.isAxiosError(error) && !isErrorHandled) {
|
if (axios.isAxiosError(error) && !isErrorHandled) {
|
||||||
const serverResponse = error.response?.data as { message: string };
|
const { message, requestId } = error.response?.data as {
|
||||||
if (serverResponse.message !== ERROR_NOT_ALLOWED_READ_SECRETS) {
|
message: string;
|
||||||
|
requestId: string;
|
||||||
|
};
|
||||||
|
if (message !== ERROR_NOT_ALLOWED_READ_SECRETS) {
|
||||||
createNotification({
|
createNotification({
|
||||||
title: "Error fetching secrets",
|
title: "Error fetching secrets",
|
||||||
type: "error",
|
type: "error",
|
||||||
text: serverResponse.message
|
text: message,
|
||||||
|
copyActions: [
|
||||||
|
{
|
||||||
|
value: requestId,
|
||||||
|
name: "Request ID",
|
||||||
|
label: `Request ID: ${requestId}`
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsErrorHandled.on();
|
setIsErrorHandled.on();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -54,7 +54,7 @@ export type TApiErrors =
|
|||||||
requestId: string;
|
requestId: string;
|
||||||
error: ApiErrorTypes.ValidationError;
|
error: ApiErrorTypes.ValidationError;
|
||||||
message: ZodIssue[];
|
message: ZodIssue[];
|
||||||
statusCode: 401;
|
statusCode: 422;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
|
@@ -3,9 +3,16 @@ import { useState } from "react";
|
|||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { useDebounce } from "@app/hooks/useDebounce";
|
import { useDebounce } from "@app/hooks/useDebounce";
|
||||||
|
|
||||||
export const usePagination = <T extends string>(initialOrderBy: T) => {
|
export const usePagination = <T extends string>(
|
||||||
|
initialOrderBy: T,
|
||||||
|
{
|
||||||
|
initPerPage = 100
|
||||||
|
}: {
|
||||||
|
initPerPage?: number;
|
||||||
|
} = {}
|
||||||
|
) => {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [perPage, setPerPage] = useState(100);
|
const [perPage, setPerPage] = useState(initPerPage);
|
||||||
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
|
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
|
||||||
const [orderBy, setOrderBy] = useState<T>(initialOrderBy);
|
const [orderBy, setOrderBy] = useState<T>(initialOrderBy);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -26,6 +33,10 @@ export const usePagination = <T extends string>(initialOrderBy: T) => {
|
|||||||
search,
|
search,
|
||||||
setSearch,
|
setSearch,
|
||||||
orderBy,
|
orderBy,
|
||||||
setOrderBy
|
setOrderBy,
|
||||||
|
toggleOrderDirection: () =>
|
||||||
|
setOrderDirection((prev) =>
|
||||||
|
prev === OrderByDirection.DESC ? OrderByDirection.ASC : OrderByDirection.DESC
|
||||||
|
)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -10,7 +10,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faStar } from "@fortawesome/free-regular-svg-icons";
|
|
||||||
import {
|
import {
|
||||||
faAngleDown,
|
faAngleDown,
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
@@ -22,15 +21,11 @@ import {
|
|||||||
faInfo,
|
faInfo,
|
||||||
faMobile,
|
faMobile,
|
||||||
faPlus,
|
faPlus,
|
||||||
faQuestion,
|
faQuestion
|
||||||
faStar as faSolidStar
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
|
||||||
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import {
|
import {
|
||||||
@@ -39,20 +34,9 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
UpgradePlanModal
|
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { NewProjectModal } from "@app/components/v2/projects/NewProjectModal";
|
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
useOrganization,
|
|
||||||
useSubscription,
|
|
||||||
useUser,
|
|
||||||
useWorkspace
|
|
||||||
} from "@app/context";
|
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useGetAccessRequestsCount,
|
useGetAccessRequestsCount,
|
||||||
@@ -62,11 +46,9 @@ import {
|
|||||||
useSelectOrganization
|
useSelectOrganization
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { Workspace } from "@app/hooks/api/types";
|
|
||||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
|
||||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
|
||||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
import { InsecureConnectionBanner } from "@app/layouts/AppLayout/components/InsecureConnectionBanner";
|
import { InsecureConnectionBanner } from "@app/layouts/AppLayout/components/InsecureConnectionBanner";
|
||||||
|
import { ProjectSelect } from "@app/layouts/AppLayout/components/ProjectSelect";
|
||||||
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
||||||
import { Mfa } from "@app/views/Login/Mfa";
|
import { Mfa } from "@app/views/Login/Mfa";
|
||||||
import { CreateOrgModal } from "@app/views/Org/components";
|
import { CreateOrgModal } from "@app/views/Org/components";
|
||||||
@@ -108,23 +90,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
const { workspaces, currentWorkspace } = useWorkspace();
|
const { workspaces, currentWorkspace } = useWorkspace();
|
||||||
const { orgs, currentOrg } = useOrganization();
|
const { orgs, currentOrg } = useOrganization();
|
||||||
|
|
||||||
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
|
||||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
|
||||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||||
|
|
||||||
const workspacesWithFaveProp = useMemo(
|
|
||||||
() =>
|
|
||||||
workspaces
|
|
||||||
.map((w): Workspace & { isFavorite: boolean } => ({
|
|
||||||
...w,
|
|
||||||
isFavorite: Boolean(projectFavorites?.includes(w.id))
|
|
||||||
}))
|
|
||||||
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)),
|
|
||||||
[workspaces, projectFavorites]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
@@ -137,17 +106,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||||
}, [secretApprovalReqCount, accessApprovalRequestCount]);
|
}, [secretApprovalReqCount, accessApprovalRequestCount]);
|
||||||
|
|
||||||
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
|
||||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
|
||||||
: true;
|
|
||||||
|
|
||||||
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
|
||||||
"addNewWs",
|
|
||||||
"upgradePlan",
|
|
||||||
"createOrg"
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -230,38 +191,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
putUserInOrg();
|
putUserInOrg();
|
||||||
}, [router.query.id]);
|
}, [router.query.id]);
|
||||||
|
|
||||||
const addProjectToFavorites = async (projectId: string) => {
|
|
||||||
try {
|
|
||||||
if (currentOrg?.id) {
|
|
||||||
await updateUserProjectFavorites({
|
|
||||||
orgId: currentOrg?.id,
|
|
||||||
projectFavorites: [...(projectFavorites || []), projectId]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to add project to favorites.",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeProjectFromFavorites = async (projectId: string) => {
|
|
||||||
try {
|
|
||||||
if (currentOrg?.id) {
|
|
||||||
await updateUserProjectFavorites({
|
|
||||||
orgId: currentOrg?.id,
|
|
||||||
projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to remove project from favorites.",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (shouldShowMfa) {
|
if (shouldShowMfa) {
|
||||||
return (
|
return (
|
||||||
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
||||||
@@ -448,97 +377,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
)}
|
)}
|
||||||
{!router.asPath.includes("org") &&
|
{!router.asPath.includes("org") &&
|
||||||
(!router.asPath.includes("personal") && currentWorkspace ? (
|
(!router.asPath.includes("personal") && currentWorkspace ? (
|
||||||
<div className="mt-5 mb-4 w-full p-3">
|
<ProjectSelect />
|
||||||
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">
|
|
||||||
Project
|
|
||||||
</p>
|
|
||||||
<Select
|
|
||||||
defaultValue={currentWorkspace?.id}
|
|
||||||
value={currentWorkspace?.id}
|
|
||||||
className="w-full bg-mineshaft-600 py-2.5 font-medium [&>*:first-child]:truncate"
|
|
||||||
onValueChange={(value) => {
|
|
||||||
localStorage.setItem("projectData.id", value);
|
|
||||||
// this is not using react query because react query in overview is throwing error when envs are not exact same count
|
|
||||||
// to reproduce change this back to router.push and switch between two projects with different env count
|
|
||||||
// look into this on dashboard revamp
|
|
||||||
window.location.assign(`/project/${value}/secrets/overview`);
|
|
||||||
}}
|
|
||||||
position="popper"
|
|
||||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
|
|
||||||
>
|
|
||||||
<div className="no-scrollbar::-webkit-scrollbar h-full no-scrollbar">
|
|
||||||
{workspacesWithFaveProp
|
|
||||||
.filter((ws) => ws.orgId === currentOrg?.id)
|
|
||||||
.map(({ id, name, isFavorite }) => (
|
|
||||||
<div
|
|
||||||
className={twMerge(
|
|
||||||
"mb-1 grid grid-cols-7 rounded-md hover:bg-mineshaft-500",
|
|
||||||
id === currentWorkspace?.id && "bg-mineshaft-500"
|
|
||||||
)}
|
|
||||||
key={id}
|
|
||||||
>
|
|
||||||
<div className="col-span-6">
|
|
||||||
<SelectItem
|
|
||||||
key={`ws-layout-list-${id}`}
|
|
||||||
value={id}
|
|
||||||
className="transition-none data-[highlighted]:bg-mineshaft-500"
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</SelectItem>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-1 flex items-center">
|
|
||||||
{isFavorite ? (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faSolidStar}
|
|
||||||
className="text-sm text-mineshaft-300 hover:text-mineshaft-400"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
removeProjectFromFavorites(id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faStar}
|
|
||||||
className="text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
addProjectToFavorites(id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
|
||||||
<div className="w-full">
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Create}
|
|
||||||
a={OrgPermissionSubjects.Workspace}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
size="sm"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
onClick={() => {
|
|
||||||
if (isAddingProjectsAllowed) {
|
|
||||||
handlePopUpOpen("addNewWs");
|
|
||||||
} else {
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
>
|
|
||||||
Add Project
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
</div>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<Link href={`/org/${currentOrg?.id}/overview`}>
|
<Link href={`/org/${currentOrg?.id}/overview`}>
|
||||||
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
|
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
|
||||||
@@ -816,15 +655,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
<NewProjectModal
|
|
||||||
isOpen={popUp.addNewWs.isOpen}
|
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
|
||||||
/>
|
|
||||||
<UpgradePlanModal
|
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
|
||||||
text="You have exceeded the number of projects allowed on the free plan."
|
|
||||||
/>
|
|
||||||
<CreateOrgModal
|
<CreateOrgModal
|
||||||
isOpen={popUp?.createOrg?.isOpen}
|
isOpen={popUp?.createOrg?.isOpen}
|
||||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||||
|
@@ -0,0 +1,212 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { components, MenuProps, OptionProps } from "react-select";
|
||||||
|
import { faStar } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { faChevronRight, faPlus, faStar as faSolidStar } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, FilterableSelect, UpgradePlanModal } from "@app/components/v2";
|
||||||
|
import { NewProjectModal } from "@app/components/v2/projects";
|
||||||
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects,
|
||||||
|
useOrganization,
|
||||||
|
useSubscription,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||||
|
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||||
|
import { Workspace } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
type TWorkspaceWithFaveProp = Workspace & { isFavorite: boolean };
|
||||||
|
|
||||||
|
const ProjectsMenu = ({ children, ...props }: MenuProps<TWorkspaceWithFaveProp>) => {
|
||||||
|
return (
|
||||||
|
<components.Menu {...props}>
|
||||||
|
{children}
|
||||||
|
<hr className="mb-2 h-px border-0 bg-mineshaft-500" />
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Workspace}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
className="w-full bg-mineshaft-700 pt-2 text-bunker-200"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={() => props.clearValue()}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add Project
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</components.Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectOption = ({
|
||||||
|
isSelected,
|
||||||
|
children,
|
||||||
|
data,
|
||||||
|
...props
|
||||||
|
}: OptionProps<TWorkspaceWithFaveProp>) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||||
|
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
||||||
|
|
||||||
|
const removeProjectFromFavorites = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
await updateUserProjectFavorites({
|
||||||
|
orgId: currentOrg!.id,
|
||||||
|
projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)]
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to remove project from favorites.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addProjectToFavorites = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
await updateUserProjectFavorites({
|
||||||
|
orgId: currentOrg!.id,
|
||||||
|
projectFavorites: [...(projectFavorites || []), projectId]
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to add project to favorites.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<components.Option
|
||||||
|
isSelected={isSelected}
|
||||||
|
data={data}
|
||||||
|
{...props}
|
||||||
|
className={twMerge(props.className, isSelected && "bg-mineshaft-500")}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center">
|
||||||
|
{isSelected && (
|
||||||
|
<FontAwesomeIcon className="mr-2 text-primary" icon={faChevronRight} size="xs" />
|
||||||
|
)}
|
||||||
|
<p className="truncate">{children}</p>
|
||||||
|
{data.isFavorite ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSolidStar}
|
||||||
|
className="ml-auto text-sm text-yellow-600 hover:text-mineshaft-400"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await removeProjectFromFavorites(data.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faStar}
|
||||||
|
className="ml-auto text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await addProjectToFavorites(data.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</components.Option>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectSelect = () => {
|
||||||
|
const { workspaces, currentWorkspace } = useWorkspace();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
||||||
|
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
|
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||||
|
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||||
|
: true;
|
||||||
|
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
|
"addNewWs",
|
||||||
|
"upgradePlan"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { options, value } = useMemo(() => {
|
||||||
|
const projectOptions = workspaces
|
||||||
|
.map((w): Workspace & { isFavorite: boolean } => ({
|
||||||
|
...w,
|
||||||
|
isFavorite: Boolean(projectFavorites?.includes(w.id))
|
||||||
|
}))
|
||||||
|
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite));
|
||||||
|
|
||||||
|
const currentOption = projectOptions.find((option) => option.id === currentWorkspace?.id);
|
||||||
|
|
||||||
|
if (!currentOption) {
|
||||||
|
return {
|
||||||
|
options: projectOptions,
|
||||||
|
value: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
options: [
|
||||||
|
currentOption,
|
||||||
|
...projectOptions.filter((option) => option.id !== currentOption.id)
|
||||||
|
],
|
||||||
|
value: currentOption
|
||||||
|
};
|
||||||
|
}, [workspaces, projectFavorites, currentWorkspace]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-5 mb-4 w-full p-3">
|
||||||
|
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">Project</p>
|
||||||
|
<FilterableSelect
|
||||||
|
className="text-sm"
|
||||||
|
value={value}
|
||||||
|
filterOption={(option, inputValue) =>
|
||||||
|
option.data.name.toLowerCase().includes(inputValue.toLowerCase())
|
||||||
|
}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
// hacky use of null as indication to create project
|
||||||
|
if (!newValue) {
|
||||||
|
if (isAddingProjectsAllowed) {
|
||||||
|
handlePopUpOpen("addNewWs");
|
||||||
|
} else {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = newValue as TWorkspaceWithFaveProp;
|
||||||
|
localStorage.setItem("projectData.id", project.id);
|
||||||
|
// todo(akhi): this is not using react query because react query in overview is throwing error when envs are not exact same count
|
||||||
|
// to reproduce change this back to router.push and switch between two projects with different env count
|
||||||
|
// look into this on dashboard revamp
|
||||||
|
window.location.assign(`/project/${project.id}/secrets/overview`);
|
||||||
|
}}
|
||||||
|
options={options}
|
||||||
|
components={{
|
||||||
|
Option: ProjectOption,
|
||||||
|
Menu: ProjectsMenu
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text="You have exceeded the number of projects allowed on the free plan."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NewProjectModal
|
||||||
|
isOpen={popUp.addNewWs.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1 @@
|
|||||||
|
export * from "./ProjectSelect";
|
@@ -1,6 +1,6 @@
|
|||||||
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
|
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -9,20 +9,22 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
|||||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons";
|
import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons";
|
||||||
import {
|
import {
|
||||||
|
faArrowDownAZ,
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
faArrowUpRightFromSquare,
|
faArrowUpRightFromSquare,
|
||||||
|
faArrowUpZA,
|
||||||
faBorderAll,
|
faBorderAll,
|
||||||
faCheck,
|
faCheck,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faClipboard,
|
faClipboard,
|
||||||
faExclamationCircle,
|
faExclamationCircle,
|
||||||
faFileShield,
|
|
||||||
faHandPeace,
|
faHandPeace,
|
||||||
faList,
|
faList,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faNetworkWired,
|
faNetworkWired,
|
||||||
faPlug,
|
faPlug,
|
||||||
faPlus,
|
faPlus,
|
||||||
|
faSearch,
|
||||||
faStar as faSolidStar,
|
faStar as faSolidStar,
|
||||||
faUserPlus
|
faUserPlus
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -32,7 +34,15 @@ import * as Tabs from "@radix-ui/react-tabs";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||||
import { Button, IconButton, Input, Skeleton, UpgradePlanModal } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
|
Skeleton,
|
||||||
|
Tooltip,
|
||||||
|
UpgradePlanModal
|
||||||
|
} from "@app/components/v2";
|
||||||
import { NewProjectModal } from "@app/components/v2/projects";
|
import { NewProjectModal } from "@app/components/v2/projects";
|
||||||
import {
|
import {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
@@ -42,7 +52,9 @@ import {
|
|||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
|
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||||
import { useRegisterUserAction } from "@app/hooks/api";
|
import { useRegisterUserAction } from "@app/hooks/api";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
import { Workspace } from "@app/hooks/api/types";
|
import { Workspace } from "@app/hooks/api/types";
|
||||||
@@ -81,6 +93,10 @@ enum ProjectsViewMode {
|
|||||||
LIST = "list"
|
LIST = "list"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ProjectOrderBy {
|
||||||
|
Name = "name"
|
||||||
|
}
|
||||||
|
|
||||||
function copyToClipboard(id: string, setState: (value: boolean) => void) {
|
function copyToClipboard(id: string, setState: (value: boolean) => void) {
|
||||||
// Get the text field
|
// Get the text field
|
||||||
const copyText = document.getElementById(id) as HTMLInputElement;
|
const copyText = document.getElementById(id) as HTMLInputElement;
|
||||||
@@ -496,26 +512,48 @@ const OrganizationPage = () => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0;
|
const isWorkspaceEmpty = !isProjectViewLoading && orgWorkspaces?.length === 0;
|
||||||
const filteredWorkspaces = orgWorkspaces.filter((ws) =>
|
|
||||||
ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())
|
const {
|
||||||
|
setPage,
|
||||||
|
perPage,
|
||||||
|
setPerPage,
|
||||||
|
page,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
toggleOrderDirection,
|
||||||
|
orderDirection
|
||||||
|
} = usePagination(ProjectOrderBy.Name, { initPerPage: 24 });
|
||||||
|
|
||||||
|
const filteredWorkspaces = useMemo(
|
||||||
|
() =>
|
||||||
|
orgWorkspaces
|
||||||
|
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
|
||||||
|
.sort((a, b) =>
|
||||||
|
orderDirection === OrderByDirection.ASC
|
||||||
|
? a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||||
|
: b.name.toLowerCase().localeCompare(a.name.toLowerCase())
|
||||||
|
),
|
||||||
|
[searchFilter, page, perPage, orderDirection, offset, limit]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { workspacesWithFaveProp, favoriteWorkspaces, nonFavoriteWorkspaces } = useMemo(() => {
|
useResetPageHelper({
|
||||||
|
setPage,
|
||||||
|
offset,
|
||||||
|
totalCount: filteredWorkspaces.length
|
||||||
|
});
|
||||||
|
|
||||||
|
const { workspacesWithFaveProp } = useMemo(() => {
|
||||||
const workspacesWithFav = filteredWorkspaces
|
const workspacesWithFav = filteredWorkspaces
|
||||||
.map((w): Workspace & { isFavorite: boolean } => ({
|
.map((w): Workspace & { isFavorite: boolean } => ({
|
||||||
...w,
|
...w,
|
||||||
isFavorite: Boolean(projectFavorites?.includes(w.id))
|
isFavorite: Boolean(projectFavorites?.includes(w.id))
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite));
|
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite))
|
||||||
|
.slice(offset, limit * page);
|
||||||
const favWorkspaces = workspacesWithFav.filter((w) => w.isFavorite);
|
|
||||||
const nonFavWorkspaces = workspacesWithFav.filter((w) => !w.isFavorite);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workspacesWithFaveProp: workspacesWithFav,
|
workspacesWithFaveProp: workspacesWithFav
|
||||||
favoriteWorkspaces: favWorkspaces,
|
|
||||||
nonFavoriteWorkspaces: nonFavWorkspaces
|
|
||||||
};
|
};
|
||||||
}, [filteredWorkspaces, projectFavorites]);
|
}, [filteredWorkspaces, projectFavorites]);
|
||||||
|
|
||||||
@@ -566,7 +604,7 @@ const OrganizationPage = () => {
|
|||||||
{isFavorite ? (
|
{isFavorite ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faSolidStar}
|
icon={faSolidStar}
|
||||||
className="text-sm text-mineshaft-300 hover:text-mineshaft-400"
|
className="text-sm text-yellow-600 hover:text-mineshaft-400"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
removeProjectFromFavorites(workspace.id);
|
removeProjectFromFavorites(workspace.id);
|
||||||
@@ -623,11 +661,10 @@ const OrganizationPage = () => {
|
|||||||
key={workspace.id}
|
key={workspace.id}
|
||||||
className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
|
className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
|
||||||
index === 0 && "rounded-t-md"
|
index === 0 && "rounded-t-md"
|
||||||
} ${index === filteredWorkspaces.length - 1 && "rounded-b-md border-b"}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center sm:col-span-3 lg:col-span-4">
|
<div className="flex items-center sm:col-span-3 lg:col-span-4">
|
||||||
<FontAwesomeIcon icon={faFileShield} className="text-sm text-primary/70" />
|
<div className="truncate text-sm text-mineshaft-100">{workspace.name}</div>
|
||||||
<div className="ml-5 truncate text-sm text-mineshaft-100">{workspace.name}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end sm:col-span-3 lg:col-span-2">
|
<div className="flex items-center justify-end sm:col-span-3 lg:col-span-2">
|
||||||
<div className="text-center text-sm text-mineshaft-300">
|
<div className="text-center text-sm text-mineshaft-300">
|
||||||
@@ -636,7 +673,7 @@ const OrganizationPage = () => {
|
|||||||
{isFavorite ? (
|
{isFavorite ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faSolidStar}
|
icon={faSolidStar}
|
||||||
className="ml-6 text-sm text-mineshaft-300 hover:text-mineshaft-400"
|
className="ml-6 text-sm text-yellow-600 hover:text-mineshaft-400"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
removeProjectFromFavorites(workspace.id);
|
removeProjectFromFavorites(workspace.id);
|
||||||
@@ -656,63 +693,75 @@ const OrganizationPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const projectsGridView = (
|
let projectsComponents: ReactNode;
|
||||||
<>
|
|
||||||
{favoriteWorkspaces.length > 0 && (
|
|
||||||
<>
|
|
||||||
<p className="mt-6 text-xl font-semibold text-white">Favorites</p>
|
|
||||||
<div
|
|
||||||
className={`b grid w-full grid-cols-1 gap-4 ${
|
|
||||||
nonFavoriteWorkspaces.length > 0 && "border-b border-mineshaft-600"
|
|
||||||
} py-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`}
|
|
||||||
>
|
|
||||||
{favoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, true))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
|
||||||
{isProjectViewLoading &&
|
|
||||||
Array.apply(0, Array(3)).map((_x, i) => (
|
|
||||||
<div
|
|
||||||
key={`workspace-cards-loading-${i + 1}`}
|
|
||||||
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
|
||||||
>
|
|
||||||
<div className="mt-0 text-lg text-mineshaft-100">
|
|
||||||
<Skeleton className="w-3/4 bg-mineshaft-600" />
|
|
||||||
</div>
|
|
||||||
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
|
||||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{!isProjectViewLoading &&
|
|
||||||
nonFavoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, false))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectsListView = (
|
if (filteredWorkspaces.length || isProjectViewLoading) {
|
||||||
<div className="mt-4 w-full rounded-md">
|
switch (projectsViewMode) {
|
||||||
{isProjectViewLoading &&
|
case ProjectsViewMode.GRID:
|
||||||
Array.apply(0, Array(3)).map((_x, i) => (
|
projectsComponents = (
|
||||||
<div
|
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
key={`workspace-cards-loading-${i + 1}`}
|
{isProjectViewLoading &&
|
||||||
className={`min-w-72 group flex h-12 cursor-pointer flex-row items-center justify-between border border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
|
Array.apply(0, Array(3)).map((_x, i) => (
|
||||||
i === 0 && "rounded-t-md"
|
<div
|
||||||
} ${i === 2 && "rounded-b-md border-b"}`}
|
key={`workspace-cards-loading-${i + 1}`}
|
||||||
>
|
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||||
<Skeleton className="w-full bg-mineshaft-600" />
|
>
|
||||||
|
<div className="mt-0 text-lg text-mineshaft-100">
|
||||||
|
<Skeleton className="w-3/4 bg-mineshaft-600" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
||||||
|
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!isProjectViewLoading && (
|
||||||
|
<>
|
||||||
|
{workspacesWithFaveProp.map((workspace) =>
|
||||||
|
renderProjectGridItem(workspace, workspace.isFavorite)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
{!isProjectViewLoading &&
|
|
||||||
workspacesWithFaveProp.map((workspace, ind) =>
|
break;
|
||||||
renderProjectListItem(workspace, workspace.isFavorite, ind)
|
case ProjectsViewMode.LIST:
|
||||||
)}
|
default:
|
||||||
</div>
|
projectsComponents = (
|
||||||
);
|
<div className="mt-4 w-full rounded-md">
|
||||||
|
{isProjectViewLoading &&
|
||||||
|
Array.apply(0, Array(3)).map((_x, i) => (
|
||||||
|
<div
|
||||||
|
key={`workspace-cards-loading-${i + 1}`}
|
||||||
|
className={`min-w-72 group flex h-12 cursor-pointer flex-row items-center justify-between border border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
|
||||||
|
i === 0 && "rounded-t-md"
|
||||||
|
} ${i === 2 && "rounded-b-md border-b"}`}
|
||||||
|
>
|
||||||
|
<Skeleton className="w-full bg-mineshaft-600" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!isProjectViewLoading &&
|
||||||
|
workspacesWithFaveProp.map((workspace, ind) =>
|
||||||
|
renderProjectListItem(workspace, workspace.isFavorite, ind)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (orgWorkspaces.length) {
|
||||||
|
projectsComponents = (
|
||||||
|
<div className="mt-4 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSearch}
|
||||||
|
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
|
||||||
|
/>
|
||||||
|
<div className="text-center font-light">No projects match search...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen">
|
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen">
|
||||||
@@ -754,6 +803,24 @@ const OrganizationPage = () => {
|
|||||||
onChange={(e) => setSearchFilter(e.target.value)}
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
/>
|
/>
|
||||||
|
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||||
|
<Tooltip content="Toggle Sort Direction">
|
||||||
|
<IconButton
|
||||||
|
className="min-w-[2.4rem] border-none hover:bg-mineshaft-600"
|
||||||
|
ariaLabel={`Sort ${
|
||||||
|
orderDirection === OrderByDirection.ASC ? "descending" : "ascending"
|
||||||
|
}`}
|
||||||
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={toggleOrderDirection}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={orderDirection === OrderByDirection.ASC ? faArrowDownAZ : faArrowUpZA}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
@@ -804,9 +871,24 @@ const OrganizationPage = () => {
|
|||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
{projectsViewMode === ProjectsViewMode.LIST ? projectsListView : projectsGridView}
|
{projectsComponents}
|
||||||
|
{!isProjectViewLoading && Boolean(filteredWorkspaces.length) && (
|
||||||
|
<Pagination
|
||||||
|
className={
|
||||||
|
projectsViewMode === ProjectsViewMode.GRID
|
||||||
|
? "col-span-full !justify-start border-transparent bg-transparent pl-2"
|
||||||
|
: "rounded-b-md border border-mineshaft-600"
|
||||||
|
}
|
||||||
|
perPage={perPage}
|
||||||
|
perPageList={[12, 24, 48, 96]}
|
||||||
|
count={filteredWorkspaces.length}
|
||||||
|
page={page}
|
||||||
|
onChangePage={setPage}
|
||||||
|
onChangePerPage={setPerPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isWorkspaceEmpty && (
|
{isWorkspaceEmpty && (
|
||||||
<div className="w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
|
<div className="mt-4 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faFolderOpen}
|
icon={faFolderOpen}
|
||||||
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
|
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
|
||||||
|
@@ -32,13 +32,8 @@ export const queryClient = new QueryClient({
|
|||||||
{
|
{
|
||||||
title: "Validation Error",
|
title: "Validation Error",
|
||||||
type: "error",
|
type: "error",
|
||||||
text: (
|
text: "Please check the input and try again.",
|
||||||
<div>
|
callToAction: (
|
||||||
<p>Please check the input and try again.</p>
|
|
||||||
<p className="mt-2 text-xs">Request ID: {serverResponse.requestId}</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
children: (
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<ModalTrigger>
|
<ModalTrigger>
|
||||||
<Button variant="outline_bg" size="xs">
|
<Button variant="outline_bg" size="xs">
|
||||||
@@ -66,7 +61,14 @@ export const queryClient = new QueryClient({
|
|||||||
</TableContainer>
|
</TableContainer>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
),
|
||||||
|
copyActions: [
|
||||||
|
{
|
||||||
|
value: serverResponse.requestId,
|
||||||
|
name: "Request ID",
|
||||||
|
label: `Request ID: ${serverResponse.requestId}`
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{ closeOnClick: false }
|
{ closeOnClick: false }
|
||||||
);
|
);
|
||||||
@@ -77,9 +79,8 @@ export const queryClient = new QueryClient({
|
|||||||
{
|
{
|
||||||
title: "Forbidden Access",
|
title: "Forbidden Access",
|
||||||
type: "error",
|
type: "error",
|
||||||
|
text: `${serverResponse.message}.`,
|
||||||
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`,
|
callToAction: serverResponse?.details?.length ? (
|
||||||
children: serverResponse?.details?.length ? (
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<ModalTrigger>
|
<ModalTrigger>
|
||||||
<Button variant="outline_bg" size="xs">
|
<Button variant="outline_bg" size="xs">
|
||||||
@@ -165,7 +166,14 @@ export const queryClient = new QueryClient({
|
|||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
) : undefined
|
) : undefined,
|
||||||
|
copyActions: [
|
||||||
|
{
|
||||||
|
value: serverResponse.requestId,
|
||||||
|
name: "Request ID",
|
||||||
|
label: `Request ID: ${serverResponse.requestId}`
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{ closeOnClick: false }
|
{ closeOnClick: false }
|
||||||
);
|
);
|
||||||
@@ -174,7 +182,14 @@ export const queryClient = new QueryClient({
|
|||||||
createNotification({
|
createNotification({
|
||||||
title: "Bad Request",
|
title: "Bad Request",
|
||||||
type: "error",
|
type: "error",
|
||||||
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`
|
text: `${serverResponse.message}.`,
|
||||||
|
copyActions: [
|
||||||
|
{
|
||||||
|
value: serverResponse.requestId,
|
||||||
|
name: "Request ID",
|
||||||
|
label: `Request ID: ${serverResponse.requestId}`
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|