diff --git a/.github/workflows/nightly-tag-generation.yml b/.github/workflows/nightly-tag-generation.yml index 0a4eb3d97..b2704f96c 100644 --- a/.github/workflows/nightly-tag-generation.yml +++ b/.github/workflows/nightly-tag-generation.yml @@ -36,11 +36,23 @@ jobs: echo "Latest production tag: $LATEST_STABLE_TAG" + # Extract version numbers and increment minor version + VERSION_NUMBERS=$(echo "$LATEST_STABLE_TAG" | sed 's/^v//') + MAJOR=$(echo "$VERSION_NUMBERS" | cut -d'.' -f1) + MINOR=$(echo "$VERSION_NUMBERS" | cut -d'.' -f2) + PATCH=$(echo "$VERSION_NUMBERS" | cut -d'.' -f3) + + # Increment minor version, reset patch to 0 + NEXT_MINOR=$((MINOR + 1)) + NEXT_VERSION="v${MAJOR}.${NEXT_MINOR}.0" + + echo "Next version for nightly: $NEXT_VERSION" + # Get current date in YYYYMMDD format DATE=$(date +%Y%m%d) - # Base nightly tag name - BASE_TAG="${LATEST_STABLE_TAG}-nightly-${DATE}" + # Base nightly tag name using next version + BASE_TAG="${NEXT_VERSION}-nightly-${DATE}" # Check if this exact tag already exists if git tag --list | grep -q "^${BASE_TAG}$"; then @@ -65,7 +77,6 @@ jobs: echo "Generated nightly tag: $NIGHTLY_TAG" echo "NIGHTLY_TAG=$NIGHTLY_TAG" >> $GITHUB_ENV - echo "LATEST_PRODUCTION_TAG=$LATEST_STABLE_TAG" >> $GITHUB_ENV git tag "$NIGHTLY_TAG" git push origin "$NIGHTLY_TAG" diff --git a/backend/e2e-test/mocks/keystore.ts b/backend/e2e-test/mocks/keystore.ts index 91b64ff0d..0cebe6ef3 100644 --- a/backend/e2e-test/mocks/keystore.ts +++ b/backend/e2e-test/mocks/keystore.ts @@ -56,6 +56,15 @@ export const mockKeyStore = (): TKeyStoreFactory => { incrementBy: async () => { return 1; }, + pgGetIntItem: async (key) => { + const value = store[key]; + if (typeof value === "number") { + return Number(value); + } + }, + pgIncrementBy: async () => { + return 1; + }, getItems: async (keys) => { const values = keys.map((key) => { const value = store[key]; diff --git a/backend/e2e-test/routes/v2/secret-folder.spec.ts b/backend/e2e-test/routes/v2/secret-folder.spec.ts new file mode 100644 index 000000000..a2bed759a --- /dev/null +++ b/backend/e2e-test/routes/v2/secret-folder.spec.ts @@ -0,0 +1,165 @@ +import { seedData1 } from "@app/db/seed-data"; + +const createFolder = async (dto: { path: string; name: string }) => { + const res = await testServer.inject({ + method: "POST", + url: `/api/v2/folders`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + projectId: seedData1.project.id, + environment: seedData1.environment.slug, + name: dto.name, + path: dto.path + } + }); + expect(res.statusCode).toBe(200); + return res.json().folder; +}; + +const deleteFolder = async (dto: { path: string; id: string }) => { + const res = await testServer.inject({ + method: "DELETE", + url: `/api/v2/folders/${dto.id}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + projectId: seedData1.project.id, + environment: seedData1.environment.slug, + path: dto.path + } + }); + expect(res.statusCode).toBe(200); + return res.json().folder; +}; + +describe("Secret Folder Router", async () => { + test.each([ + { name: "folder1", path: "/" }, // one in root + { name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones + { name: "folder2", path: "/" }, + { name: "folder1", path: "/level1/level2" } // this should not create folder return same thing + ])("Create folder $name in $path", async ({ name, path }) => { + const createdFolder = await createFolder({ path, name }); + // check for default environments + expect(createdFolder).toEqual( + expect.objectContaining({ + name, + id: expect.any(String) + }) + ); + await deleteFolder({ path, id: createdFolder.id }); + }); + + test.each([ + { + path: "/", + expected: { + folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }], + length: 3 + } + }, + { path: "/level1/level2", expected: { folders: [{ name: "folder1" }], length: 1 } } + ])("Get folders $path", async ({ path, expected }) => { + const newFolders = await Promise.all(expected.folders.map(({ name }) => createFolder({ name, path }))); + + const res = await testServer.inject({ + method: "GET", + url: `/api/v2/folders`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + query: { + projectId: seedData1.project.id, + environment: seedData1.environment.slug, + path + } + }); + + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("folders"); + expect(payload.folders.length >= expected.folders.length).toBeTruthy(); + expect(payload).toEqual({ + folders: expect.arrayContaining(expected.folders.map((el) => expect.objectContaining(el))) + }); + + await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id }))); + }); + + test("Update a deep folder", async () => { + const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" }); + expect(newFolder).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: "folder-updated" + }) + ); + + const resUpdatedFolders = await testServer.inject({ + method: "GET", + url: `/api/v2/folders`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + query: { + projectId: seedData1.project.id, + environment: seedData1.environment.slug, + path: "/level1/level2" + } + }); + + expect(resUpdatedFolders.statusCode).toBe(200); + const updatedFolderList = JSON.parse(resUpdatedFolders.payload); + expect(updatedFolderList).toHaveProperty("folders"); + expect(updatedFolderList.folders[0].name).toEqual("folder-updated"); + + await deleteFolder({ path: "/level1/level2", id: newFolder.id }); + }); + + test("Delete a deep folder", async () => { + const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" }); + const res = await testServer.inject({ + method: "DELETE", + url: `/api/v2/folders/${newFolder.id}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + projectId: seedData1.project.id, + environment: seedData1.environment.slug, + path: "/level1/level2" + } + }); + + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("folder"); + expect(payload.folder).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: "folder-updated" + }) + ); + + const resUpdatedFolders = await testServer.inject({ + method: "GET", + url: `/api/v2/folders`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + query: { + projectId: seedData1.project.id, + environment: seedData1.environment.slug, + path: "/level1/level2" + } + }); + + expect(resUpdatedFolders.statusCode).toBe(200); + const updatedFolderList = JSON.parse(resUpdatedFolders.payload); + expect(updatedFolderList).toHaveProperty("folders"); + expect(updatedFolderList.folders.length).toEqual(0); + }); +}); diff --git a/backend/e2e-test/routes/v2/service-token.spec.ts b/backend/e2e-test/routes/v2/service-token.spec.ts index 025d9796f..4f72987cb 100644 --- a/backend/e2e-test/routes/v2/service-token.spec.ts +++ b/backend/e2e-test/routes/v2/service-token.spec.ts @@ -70,7 +70,7 @@ const createServiceToken = async ( const deleteServiceToken = async () => { const serviceTokenListRes = await testServer.inject({ method: "GET", - url: `/api/v1/workspace/${seedData1.project.id}/service-token-data`, + url: `/api/v1/projects/${seedData1.project.id}/service-token-data`, headers: { authorization: `Bearer ${jwtAuthToken}` } diff --git a/backend/e2e-test/routes/v3/secret-reference.spec.ts b/backend/e2e-test/routes/v3/secret-reference.spec.ts index 560565342..a5d8ba61d 100644 --- a/backend/e2e-test/routes/v3/secret-reference.spec.ts +++ b/backend/e2e-test/routes/v3/secret-reference.spec.ts @@ -314,8 +314,8 @@ describe("Secret expansion", () => { expect(listSecrets.imports).toEqual( expect.arrayContaining([ expect.objectContaining({ - secretPath: `/__reserve_replication_${secretImportFromProdToDev.id}`, - environment: seedData1.environment.slug, + secretPath: "/deep/nested", + environment: "prod", secrets: expect.arrayContaining([ expect.objectContaining({ secretKey: "NESTED_KEY_1", diff --git a/backend/e2e-test/routes/v4/secrets.spec.ts b/backend/e2e-test/routes/v4/secrets.spec.ts new file mode 100644 index 000000000..979adddf8 --- /dev/null +++ b/backend/e2e-test/routes/v4/secrets.spec.ts @@ -0,0 +1,678 @@ +import { SecretType } from "@app/db/schemas"; +import { seedData1 } from "@app/db/seed-data"; +import { AuthMode } from "@app/services/auth/auth-type"; + +type TRawSecret = { + secretKey: string; + secretValue: string; + secretComment?: string; + version: number; +}; + +const createSecret = async (dto: { path: string; key: string; value: string; comment: string; type?: SecretType }) => { + const createSecretReqBody = { + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + type: dto.type || SecretType.Shared, + secretPath: dto.path, + secretKey: dto.key, + secretValue: dto.value, + secretComment: dto.comment + }; + const createSecRes = await testServer.inject({ + method: "POST", + url: `/api/v4/secrets/${dto.key}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: createSecretReqBody + }); + expect(createSecRes.statusCode).toBe(200); + const createdSecretPayload = JSON.parse(createSecRes.payload); + expect(createdSecretPayload).toHaveProperty("secret"); + return createdSecretPayload.secret as TRawSecret; +}; + +const deleteSecret = async (dto: { path: string; key: string }) => { + const deleteSecRes = await testServer.inject({ + method: "DELETE", + url: `/api/v4/secrets/${dto.key}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + secretPath: dto.path + } + }); + expect(deleteSecRes.statusCode).toBe(200); + const updatedSecretPayload = JSON.parse(deleteSecRes.payload); + expect(updatedSecretPayload).toHaveProperty("secret"); + return updatedSecretPayload.secret as TRawSecret; +}; + +describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }])( + "Secret V4 - $auth mode", + async ({ auth }) => { + let folderId = ""; + let authToken = ""; + const secretTestCases = [ + { + path: "/", + secret: { + key: "SEC1", + value: "something-secret", + comment: "some comment" + } + }, + { + path: "/nested1/nested2/folder", + secret: { + key: "NESTED-SEC1", + value: "something-secret", + comment: "some comment" + } + }, + { + path: "/", + secret: { + key: "secret-key-2", + value: `-----BEGIN PRIVATE KEY----- + MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa6eeFk+cMVqFn + hoVQDYgn2Ptp5Azysr2UPq6P73pCL9BzUtOXKZROqDyGehzzfg3wE2KdYU1Jk5Uq + fP0ZOWDIlM2SaVCSI3FW32o5+ZiggjpqcVdLFc/PS0S/ZdSmpPd8h11iO2brtIAI + ugTW8fcKlGSNUwx9aFmE7A6JnTRliTxB1l6QaC+YAwTK39VgeVH2gDSWC407aS15 + QobAkaBKKmFkzB5D7i2ZJwt+uXJV/rbLmyDmtnw0lubciGn7NX9wbYef180fisqT + aPNAz0nPKk0fFH2Wd5MZixNGbrrpDA+FCYvI5doThZyT2hpj08qWP07oXXCAqw46 + IEupNSILAgMBAAECggEBAIJb5KzeaiZS3B3O8G4OBQ5rJB3WfyLYUHnoSWLsBbie + nc392/ovThLmtZAAQE6SO85Tsb93+t64Z2TKqv1H8G658UeMgfWIB78v4CcLJ2mi + TN/3opqXrzjkQOTDHzBgT7al/mpETHZ6fOdbCemK0fVALGFUioUZg4M8VXtuI4Jw + q28jAyoRKrCrzda4BeQ553NZ4G5RvwhX3O2I8B8upTbt5hLcisBKy8MPLYY5LUFj + YKAP+raf6QLliP6KYHuVxUlgzxjLTxVG41etcyqqZF+foyiKBO3PU3n8oh++tgQP + ExOxiR0JSkBG5b+oOBD0zxcvo3/SjBHn0dJOZCSU2SkCgYEAyCe676XnNyBZMRD7 + 6trsaoiCWBpA6M8H44+x3w4cQFtqV38RyLy60D+iMKjIaLqeBbnay61VMzo24Bz3 + EuF2n4+9k/MetLJ0NCw8HmN5k0WSMD2BFsJWG8glVbzaqzehP4tIclwDTYc1jQVt + IoV2/iL7HGT+x2daUwbU5kN5hK0CgYEAxiLB+fmjxJW7VY4SHDLqPdpIW0q/kv4K + d/yZBrCX799vjmFb9vLh7PkQUfJhMJ/ttJOd7EtT3xh4mfkBeLfHwVU0d/ahbmSH + UJu/E9ZGxAW3PP0kxHZtPrLKQwBnfq8AxBauIhR3rPSorQTIOKtwz1jMlHFSUpuL + 3KeK2YfDYJcCgYEAkQnJOlNcAuRb/WQzSHIvktssqK8NjiZHryy3Vc0hx7j2jES2 + HGI2dSVHYD9OSiXA0KFm3OTTsnViwm/60iGzFdjRJV6tR39xGUVcoyCuPnvRfUd0 + PYvBXgxgkYpyYlPDcwp5CvWGJy3tLi1acgOIwIuUr3S38sL//t4adGk8q1kCgYB8 + Jbs1Tl53BvrimKpwUNbE+sjrquJu0A7vL68SqgQJoQ7dP9PH4Ff/i+/V6PFM7mib + BQOm02wyFbs7fvKVGVJoqWK+6CIucX732x7W5yRgHtS5ukQXdbzt1Ek3wkEW98Cb + HTruz7RNAt/NyXlLSODeit1lBbx3Vk9EaxZtRsv88QKBgGn7JwXgez9NOyobsNIo + QVO80rpUeenSjuFi+R0VmbLKe/wgAQbYJ0xTAsQ0btqViMzB27D6mJyC+KUIwWNX + MN8a+m46v4kqvZkKL2c4gmDibyURNe/vCtCHFuanJS/1mo2tr4XDyEeiuK52eTd9 + omQDpP86RX/hIIQ+JyLSaWYa + -----END PRIVATE KEY-----`, + comment: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation" + } + }, + { + path: "/nested1/nested2/folder", + secret: { + key: "secret-key-3", + value: `-----BEGIN PRIVATE KEY----- + MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa6eeFk+cMVqFn + hoVQDYgn2Ptp5Azysr2UPq6P73pCL9BzUtOXKZROqDyGehzzfg3wE2KdYU1Jk5Uq + fP0ZOWDIlM2SaVCSI3FW32o5+ZiggjpqcVdLFc/PS0S/ZdSmpPd8h11iO2brtIAI + ugTW8fcKlGSNUwx9aFmE7A6JnTRliTxB1l6QaC+YAwTK39VgeVH2gDSWC407aS15 + QobAkaBKKmFkzB5D7i2ZJwt+uXJV/rbLmyDmtnw0lubciGn7NX9wbYef180fisqT + aPNAz0nPKk0fFH2Wd5MZixNGbrrpDA+FCYvI5doThZyT2hpj08qWP07oXXCAqw46 + IEupNSILAgMBAAECggEBAIJb5KzeaiZS3B3O8G4OBQ5rJB3WfyLYUHnoSWLsBbie + nc392/ovThLmtZAAQE6SO85Tsb93+t64Z2TKqv1H8G658UeMgfWIB78v4CcLJ2mi + TN/3opqXrzjkQOTDHzBgT7al/mpETHZ6fOdbCemK0fVALGFUioUZg4M8VXtuI4Jw + q28jAyoRKrCrzda4BeQ553NZ4G5RvwhX3O2I8B8upTbt5hLcisBKy8MPLYY5LUFj + YKAP+raf6QLliP6KYHuVxUlgzxjLTxVG41etcyqqZF+foyiKBO3PU3n8oh++tgQP + ExOxiR0JSkBG5b+oOBD0zxcvo3/SjBHn0dJOZCSU2SkCgYEAyCe676XnNyBZMRD7 + 6trsaoiCWBpA6M8H44+x3w4cQFtqV38RyLy60D+iMKjIaLqeBbnay61VMzo24Bz3 + EuF2n4+9k/MetLJ0NCw8HmN5k0WSMD2BFsJWG8glVbzaqzehP4tIclwDTYc1jQVt + IoV2/iL7HGT+x2daUwbU5kN5hK0CgYEAxiLB+fmjxJW7VY4SHDLqPdpIW0q/kv4K + d/yZBrCX799vjmFb9vLh7PkQUfJhMJ/ttJOd7EtT3xh4mfkBeLfHwVU0d/ahbmSH + UJu/E9ZGxAW3PP0kxHZtPrLKQwBnfq8AxBauIhR3rPSorQTIOKtwz1jMlHFSUpuL + 3KeK2YfDYJcCgYEAkQnJOlNcAuRb/WQzSHIvktssqK8NjiZHryy3Vc0hx7j2jES2 + HGI2dSVHYD9OSiXA0KFm3OTTsnViwm/60iGzFdjRJV6tR39xGUVcoyCuPnvRfUd0 + PYvBXgxgkYpyYlPDcwp5CvWGJy3tLi1acgOIwIuUr3S38sL//t4adGk8q1kCgYB8 + Jbs1Tl53BvrimKpwUNbE+sjrquJu0A7vL68SqgQJoQ7dP9PH4Ff/i+/V6PFM7mib + BQOm02wyFbs7fvKVGVJoqWK+6CIucX732x7W5yRgHtS5ukQXdbzt1Ek3wkEW98Cb + HTruz7RNAt/NyXlLSODeit1lBbx3Vk9EaxZtRsv88QKBgGn7JwXgez9NOyobsNIo + QVO80rpUeenSjuFi+R0VmbLKe/wgAQbYJ0xTAsQ0btqViMzB27D6mJyC+KUIwWNX + MN8a+m46v4kqvZkKL2c4gmDibyURNe/vCtCHFuanJS/1mo2tr4XDyEeiuK52eTd9 + omQDpP86RX/hIIQ+JyLSaWYa + -----END PRIVATE KEY-----`, + comment: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation" + } + }, + { + path: "/nested1/nested2/folder", + secret: { + key: "secret-key-3", + value: + "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gU2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uCg==", + comment: "" + } + } + ]; + + beforeAll(async () => { + if (auth === AuthMode.JWT) { + authToken = jwtAuthToken; + } else if (auth === AuthMode.IDENTITY_ACCESS_TOKEN) { + const identityLogin = await testServer.inject({ + method: "POST", + url: "/api/v1/auth/universal-auth/login", + body: { + clientSecret: seedData1.machineIdentity.clientCredentials.secret, + clientId: seedData1.machineIdentity.clientCredentials.id + } + }); + expect(identityLogin.statusCode).toBe(200); + authToken = identityLogin.json().accessToken; + } + // create a deep folder + const folderCreate = await testServer.inject({ + method: "POST", + url: `/api/v2/folders`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + name: "folder", + path: "/nested1/nested2" + } + }); + expect(folderCreate.statusCode).toBe(200); + folderId = folderCreate.json().folder.id; + }); + + afterAll(async () => { + const deleteFolder = await testServer.inject({ + method: "DELETE", + url: `/api/v2/folders/${folderId}`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: { + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + path: "/nested1/nested2" + } + }); + expect(deleteFolder.statusCode).toBe(200); + }); + + const getSecrets = async (environment: string, secretPath = "/") => { + const res = await testServer.inject({ + method: "GET", + url: `/api/v4/secrets`, + headers: { + authorization: `Bearer ${authToken}` + }, + query: { + secretPath, + environment, + projectId: seedData1.projectV3.id + } + }); + const secrets: TRawSecret[] = JSON.parse(res.payload).secrets || []; + return secrets; + }; + + test.each(secretTestCases)("Create secret in path $path", async ({ secret, path }) => { + const createdSecret = await createSecret({ path, ...secret }); + expect(createdSecret.secretKey).toEqual(secret.key); + expect(createdSecret.secretValue).toEqual(secret.value); + expect(createdSecret.secretComment || "").toEqual(secret.comment); + expect(createdSecret.version).toEqual(1); + + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secretKey: secret.key, + secretValue: secret.value, + type: SecretType.Shared + }) + ]) + ); + await deleteSecret({ path, key: secret.key }); + }); + + test.each(secretTestCases)("Get secret by name in path $path", async ({ secret, path }) => { + await createSecret({ path, ...secret }); + + const getSecByNameRes = await testServer.inject({ + method: "GET", + url: `/api/v4/secrets/${secret.key}`, + headers: { + authorization: `Bearer ${authToken}` + }, + query: { + secretPath: path, + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug + } + }); + expect(getSecByNameRes.statusCode).toBe(200); + const getSecretByNamePayload = JSON.parse(getSecByNameRes.payload); + expect(getSecretByNamePayload).toHaveProperty("secret"); + const decryptedSecret = getSecretByNamePayload.secret as TRawSecret; + expect(decryptedSecret.secretKey).toEqual(secret.key); + expect(decryptedSecret.secretValue).toEqual(secret.value); + expect(decryptedSecret.secretComment || "").toEqual(secret.comment); + + await deleteSecret({ path, key: secret.key }); + }); + + if (auth === AuthMode.JWT) { + test.each(secretTestCases)( + "Creating personal secret without shared throw error in path $path", + async ({ secret }) => { + const createSecretReqBody = { + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + type: SecretType.Personal, + secretKey: secret.key, + secretValue: secret.value, + secretComment: secret.comment + }; + const createSecRes = await testServer.inject({ + method: "POST", + url: `/api/v4/secrets/SEC2`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: createSecretReqBody + }); + const payload = JSON.parse(createSecRes.payload); + expect(createSecRes.statusCode).toBe(400); + expect(payload.error).toEqual("BadRequest"); + } + ); + + test.each(secretTestCases)("Creating personal secret in path $path", async ({ secret, path }) => { + await createSecret({ path, ...secret }); + + const createSecretReqBody = { + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + type: SecretType.Personal, + secretPath: path, + secretKey: secret.key, + secretValue: "personal-value", + secretComment: secret.comment + }; + const createSecRes = await testServer.inject({ + method: "POST", + url: `/api/v4/secrets/${secret.key}`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: createSecretReqBody + }); + expect(createSecRes.statusCode).toBe(200); + + // list secrets should contain personal one and shared one + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secretKey: secret.key, + secretValue: secret.value, + type: SecretType.Shared + }), + expect.objectContaining({ + secretKey: secret.key, + secretValue: "personal-value", + type: SecretType.Personal + }) + ]) + ); + + await deleteSecret({ path, key: secret.key }); + }); + + test.each(secretTestCases)( + "Deleting personal one should not delete shared secret in path $path", + async ({ secret, path }) => { + await createSecret({ path, ...secret }); // shared one + await createSecret({ path, ...secret, type: SecretType.Personal }); + + // shared secret deletion should delete personal ones also + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secretKey: secret.key, + type: SecretType.Shared + }), + expect.not.objectContaining({ + secretKey: secret.key, + type: SecretType.Personal + }) + ]) + ); + await deleteSecret({ path, key: secret.key }); + } + ); + } + + test.each(secretTestCases)("Update secret in path $path", async ({ path, secret }) => { + await createSecret({ path, ...secret }); + const updateSecretReqBody = { + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + type: SecretType.Shared, + secretPath: path, + secretKey: secret.key, + secretValue: "new-value", + secretComment: secret.comment + }; + const updateSecRes = await testServer.inject({ + method: "PATCH", + url: `/api/v4/secrets/${secret.key}`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: updateSecretReqBody + }); + expect(updateSecRes.statusCode).toBe(200); + const updatedSecretPayload = JSON.parse(updateSecRes.payload); + expect(updatedSecretPayload).toHaveProperty("secret"); + const decryptedSecret = updatedSecretPayload.secret; + expect(decryptedSecret.secretKey).toEqual(secret.key); + expect(decryptedSecret.secretValue).toEqual("new-value"); + expect(decryptedSecret.secretComment || "").toEqual(secret.comment); + + // list secret should have updated value + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secretKey: secret.key, + secretValue: "new-value", + type: SecretType.Shared + }) + ]) + ); + + await deleteSecret({ path, key: secret.key }); + }); + + test.each(secretTestCases)("Delete secret in path $path", async ({ secret, path }) => { + await createSecret({ path, ...secret }); + const deletedSecret = await deleteSecret({ path, key: secret.key }); + expect(deletedSecret.secretKey).toEqual(secret.key); + + // shared secret deletion should delete personal ones also + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.not.arrayContaining([ + expect.objectContaining({ + secretKey: secret.key, + type: SecretType.Shared + }), + expect.objectContaining({ + secretKey: secret.key, + type: SecretType.Personal + }) + ]) + ); + }); + + test.each(secretTestCases)("Bulk create secrets in path $path", async ({ secret, path }) => { + const createSharedSecRes = await testServer.inject({ + method: "POST", + url: `/api/v4/secrets/batch`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: { + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + secretPath: path, + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretKey: `BULK-${secret.key}-${i + 1}`, + secretValue: secret.value, + secretComment: secret.comment + })) + } + }); + expect(createSharedSecRes.statusCode).toBe(200); + const createSharedSecPayload = JSON.parse(createSharedSecRes.payload); + expect(createSharedSecPayload).toHaveProperty("secrets"); + + // bulk ones should exist + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining( + Array.from(Array(5)).map((_e, i) => + expect.objectContaining({ + secretKey: `BULK-${secret.key}-${i + 1}`, + secretValue: secret.value, + type: SecretType.Shared + }) + ) + ) + ); + + await Promise.all( + Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` })) + ); + }); + + test.each(secretTestCases)("Bulk create fail on existing secret in path $path", async ({ secret, path }) => { + await createSecret({ ...secret, key: `BULK-${secret.key}-1`, path }); + + const createSharedSecRes = await testServer.inject({ + method: "POST", + url: `/api/v4/secrets/batch`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: { + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + secretPath: path, + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretKey: `BULK-${secret.key}-${i + 1}`, + secretValue: secret.value, + secretComment: secret.comment + })) + } + }); + expect(createSharedSecRes.statusCode).toBe(400); + + await deleteSecret({ path, key: `BULK-${secret.key}-1` }); + }); + + test.each(secretTestCases)("Bulk update secrets in path $path", async ({ secret, path }) => { + await Promise.all( + Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path })) + ); + + const updateSharedSecRes = await testServer.inject({ + method: "PATCH", + url: `/api/v4/secrets/batch`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: { + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + secretPath: path, + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretKey: `BULK-${secret.key}-${i + 1}`, + secretValue: "update-value", + secretComment: secret.comment + })) + } + }); + expect(updateSharedSecRes.statusCode).toBe(200); + const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload); + expect(updateSharedSecPayload).toHaveProperty("secrets"); + + // bulk ones should exist + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining( + Array.from(Array(5)).map((_e, i) => + expect.objectContaining({ + secretKey: `BULK-${secret.key}-${i + 1}`, + secretValue: "update-value", + type: SecretType.Shared + }) + ) + ) + ); + await Promise.all( + Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` })) + ); + }); + + test.each(secretTestCases)("Bulk upsert secrets in path $path", async ({ secret, path }) => { + const updateSharedSecRes = await testServer.inject({ + method: "PATCH", + url: `/api/v4/secrets/batch`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: { + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + secretPath: path, + mode: "upsert", + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretKey: `BULK-${secret.key}-${i + 1}`, + secretValue: "update-value", + secretComment: secret.comment + })) + } + }); + expect(updateSharedSecRes.statusCode).toBe(200); + const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload); + expect(updateSharedSecPayload).toHaveProperty("secrets"); + + // bulk ones should exist + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining( + Array.from(Array(5)).map((_e, i) => + expect.objectContaining({ + secretKey: `BULK-${secret.key}-${i + 1}`, + secretValue: "update-value", + type: SecretType.Shared + }) + ) + ) + ); + await Promise.all( + Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` })) + ); + }); + + test("Bulk upsert secrets in path multiple paths", async () => { + const firstBatchSecrets = Array.from(Array(5)).map((_e, i) => ({ + secretKey: `BULK-KEY-${secretTestCases[0].secret.key}-${i + 1}`, + secretValue: "update-value", + secretComment: "comment", + secretPath: secretTestCases[0].path + })); + const secondBatchSecrets = Array.from(Array(5)).map((_e, i) => ({ + secretKey: `BULK-KEY-${secretTestCases[1].secret.key}-${i + 1}`, + secretValue: "update-value", + secretComment: "comment", + secretPath: secretTestCases[1].path + })); + const testSecrets = [...firstBatchSecrets, ...secondBatchSecrets]; + + const updateSharedSecRes = await testServer.inject({ + method: "PATCH", + url: `/api/v4/secrets/batch`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: { + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + mode: "upsert", + secrets: testSecrets + } + }); + expect(updateSharedSecRes.statusCode).toBe(200); + const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload); + expect(updateSharedSecPayload).toHaveProperty("secrets"); + + // bulk ones should exist + const firstBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[0].path); + expect(firstBatchSecretsOnInfisical).toEqual( + expect.arrayContaining( + firstBatchSecrets.map((el) => + expect.objectContaining({ + secretKey: el.secretKey, + secretValue: "update-value", + type: SecretType.Shared + }) + ) + ) + ); + const secondBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[1].path); + expect(secondBatchSecretsOnInfisical).toEqual( + expect.arrayContaining( + secondBatchSecrets.map((el) => + expect.objectContaining({ + secretKey: el.secretKey, + secretValue: "update-value", + type: SecretType.Shared + }) + ) + ) + ); + await Promise.all(testSecrets.map((el) => deleteSecret({ path: el.secretPath, key: el.secretKey }))); + }); + + test.each(secretTestCases)("Bulk delete secrets in path $path", async ({ secret, path }) => { + await Promise.all( + Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path })) + ); + + const deletedSharedSecRes = await testServer.inject({ + method: "DELETE", + url: `/api/v4/secrets/batch`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: { + projectId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + secretPath: path, + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretKey: `BULK-${secret.key}-${i + 1}` + })) + } + }); + + expect(deletedSharedSecRes.statusCode).toBe(200); + const deletedSecretPayload = JSON.parse(deletedSharedSecRes.payload); + expect(deletedSecretPayload).toHaveProperty("secrets"); + + // bulk ones should exist + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.not.arrayContaining( + Array.from(Array(5)).map((_e, i) => + expect.objectContaining({ + secretKey: `BULK-${secret.value}-${i + 1}`, + type: SecretType.Shared + }) + ) + ) + ); + }); + } +); diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index ff5f42286..085b8fe30 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -15,6 +15,7 @@ import { mockSmtpServer } from "./mocks/smtp"; import { initDbConnection } from "@app/db"; import { queueServiceFactory } from "@app/queue"; import { keyStoreFactory } from "@app/keystore/keystore"; +import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; import { buildRedisFromConfig } from "@app/lib/config/redis"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; @@ -62,7 +63,8 @@ export default { const smtp = mockSmtpServer(); const queue = queueServiceFactory(envCfg, { dbConnectionUrl: envCfg.DB_CONNECTION_URI }); - const keyStore = keyStoreFactory(envCfg); + const keyValueStoreDAL = keyValueStoreDALFactory(db); + const keyStore = keyStoreFactory(envCfg, keyValueStoreDAL); await queue.initialize(); diff --git a/backend/package-lock.json b/backend/package-lock.json index f22fd0b7b..c6ac0b147 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -25,6 +25,7 @@ "@fastify/multipart": "8.3.1", "@fastify/passport": "^2.4.0", "@fastify/rate-limit": "^9.0.0", + "@fastify/reply-from": "^9.8.0", "@fastify/request-context": "^5.1.0", "@fastify/session": "^10.7.0", "@fastify/static": "^7.0.4", @@ -8044,6 +8045,42 @@ "toad-cache": "^3.3.0" } }, + "node_modules/@fastify/reply-from": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@fastify/reply-from/-/reply-from-9.8.0.tgz", + "integrity": "sha512-bPNVaFhEeNI0Lyl6404YZaPFokudCplidE3QoOcr78yOy6H9sYw97p5KPYvY/NJNUHfFtvxOaSAHnK+YSiv/Mg==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^3.0.0", + "end-of-stream": "^1.4.4", + "fast-content-type-parse": "^1.1.0", + "fast-querystring": "^1.0.0", + "fastify-plugin": "^4.0.0", + "toad-cache": "^3.7.0", + "undici": "^5.19.1" + } + }, + "node_modules/@fastify/reply-from/node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@fastify/reply-from/node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/@fastify/request-context": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@fastify/request-context/-/request-context-5.1.0.tgz", @@ -29330,9 +29367,10 @@ } }, "node_modules/toad-cache": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.3.0.tgz", - "integrity": "sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", "engines": { "node": ">=12" } diff --git a/backend/package.json b/backend/package.json index aaef9f567..0c8464eaf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -145,6 +145,7 @@ "@fastify/multipart": "8.3.1", "@fastify/passport": "^2.4.0", "@fastify/rate-limit": "^9.0.0", + "@fastify/reply-from": "^9.8.0", "@fastify/request-context": "^5.1.0", "@fastify/session": "^10.7.0", "@fastify/static": "^7.0.4", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index c25d8d4d1..8ca4288b5 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -1,13 +1,13 @@ import "fastify"; -import { Redis } from "ioredis"; +import { Cluster, Redis } from "ioredis"; import { TUsers } from "@app/db/schemas"; import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-types"; import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-types"; import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-types"; import { TAuditLogServiceFactory, TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; -import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-types"; +import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-types"; import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service"; import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-types"; @@ -16,6 +16,7 @@ import { TEventBusService } from "@app/ee/services/event/event-bus-service"; import { TServerSentEventsService } from "@app/ee/services/event/event-sse-service"; import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TIdentityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template"; @@ -32,6 +33,7 @@ import { TPitServiceFactory } from "@app/ee/services/pit/pit-service"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-types"; import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { RateLimitConfiguration, TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-types"; +import { TRelayServiceFactory } from "@app/ee/services/relay/relay-service"; import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-types"; import { TScimServiceFactory } from "@app/ee/services/scim/scim-types"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; @@ -83,6 +85,8 @@ import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; +import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; +import { TOfflineUsageReportServiceFactory } from "@app/services/offline-usage-report/offline-usage-report-service"; import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service"; import { TOrgServiceFactory } from "@app/services/org/org-service"; import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; @@ -161,6 +165,7 @@ declare module "fastify" { }; // identity injection. depending on which kinda of token the information is filled in auth auth: TAuthMode; + shouldForwardWritesToPrimaryInstance: boolean; permission: { authMethod: ActorAuthMethod; type: ActorType; @@ -194,7 +199,7 @@ declare module "fastify" { } interface FastifyInstance { - redis: Redis; + redis: Redis | Cluster; services: { login: TAuthLoginFactory; password: TAuthPasswordFactory; @@ -293,6 +298,8 @@ declare module "fastify" { secretRotationV2: TSecretRotationV2ServiceFactory; microsoftTeams: TMicrosoftTeamsServiceFactory; assumePrivileges: TAssumePrivilegeServiceFactory; + relay: TRelayServiceFactory; + gatewayV2: TGatewayV2ServiceFactory; githubOrgSync: TGithubOrgSyncServiceFactory; folderCommit: TFolderCommitServiceFactory; pit: TPitServiceFactory; @@ -303,6 +310,8 @@ declare module "fastify" { bus: TEventBusService; sse: TServerSentEventsService; identityAuthTemplate: TIdentityAuthTemplateServiceFactory; + notification: TNotificationServiceFactory; + offlineUsageReport: TOfflineUsageReportServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index f645cb8f2..75a358341 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -101,6 +101,9 @@ import { TGateways, TGatewaysInsert, TGatewaysUpdate, + TGatewaysV2, + TGatewaysV2Insert, + TGatewaysV2Update, TGitAppInstallSessions, TGitAppInstallSessionsInsert, TGitAppInstallSessionsUpdate, @@ -179,6 +182,9 @@ import { TIncidentContacts, TIncidentContactsInsert, TIncidentContactsUpdate, + TInstanceRelayConfig, + TInstanceRelayConfigInsert, + TInstanceRelayConfigUpdate, TIntegrationAuths, TIntegrationAuthsInsert, TIntegrationAuthsUpdate, @@ -191,6 +197,9 @@ import { TInternalKms, TInternalKmsInsert, TInternalKmsUpdate, + TKeyValueStore, + TKeyValueStoreInsert, + TKeyValueStoreUpdate, TKmipClientCertificates, TKmipClientCertificatesInsert, TKmipClientCertificatesUpdate, @@ -230,9 +239,15 @@ import { TOrgGatewayConfig, TOrgGatewayConfigInsert, TOrgGatewayConfigUpdate, + TOrgGatewayConfigV2, + TOrgGatewayConfigV2Insert, + TOrgGatewayConfigV2Update, TOrgMemberships, TOrgMembershipsInsert, TOrgMembershipsUpdate, + TOrgRelayConfig, + TOrgRelayConfigInsert, + TOrgRelayConfigUpdate, TOrgRoles, TOrgRolesInsert, TOrgRolesUpdate, @@ -290,6 +305,9 @@ import { TRateLimit, TRateLimitInsert, TRateLimitUpdate, + TRelays, + TRelaysInsert, + TRelaysUpdate, TResourceMetadata, TResourceMetadataInsert, TResourceMetadataUpdate, @@ -530,6 +548,11 @@ import { TSecretReminderRecipientsInsert, TSecretReminderRecipientsUpdate } from "@app/db/schemas/secret-reminder-recipients"; +import { + TUserNotifications, + TUserNotificationsInsert, + TUserNotificationsUpdate +} from "@app/db/schemas/user-notifications"; declare module "knex" { namespace Knex { @@ -1233,6 +1256,17 @@ declare module "knex/types/tables" { TSecretScanningResourcesInsert, TSecretScanningResourcesUpdate >; + [TableName.InstanceRelayConfig]: KnexOriginal.CompositeTableType< + TInstanceRelayConfig, + TInstanceRelayConfigInsert, + TInstanceRelayConfigUpdate + >; + [TableName.OrgRelayConfig]: KnexOriginal.CompositeTableType< + TOrgRelayConfig, + TOrgRelayConfigInsert, + TOrgRelayConfigUpdate + >; + [TableName.Relay]: KnexOriginal.CompositeTableType; [TableName.SecretScanningScan]: KnexOriginal.CompositeTableType< TSecretScanningScans, TSecretScanningScansInsert, @@ -1254,5 +1288,21 @@ declare module "knex/types/tables" { TRemindersRecipientsInsert, TRemindersRecipientsUpdate >; + [TableName.OrgGatewayConfigV2]: KnexOriginal.CompositeTableType< + TOrgGatewayConfigV2, + TOrgGatewayConfigV2Insert, + TOrgGatewayConfigV2Update + >; + [TableName.GatewayV2]: KnexOriginal.CompositeTableType; + [TableName.UserNotifications]: KnexOriginal.CompositeTableType< + TUserNotifications, + TUserNotificationsInsert, + TUserNotificationsUpdate + >; + [TableName.KeyValueStore]: KnexOriginal.CompositeTableType< + TKeyValueStore, + TKeyValueStoreInsert, + TKeyValueStoreUpdate + >; } } diff --git a/backend/src/db/migrations/20250204025010_app-connections-and-secret-syncs-unique-constraint.ts b/backend/src/db/migrations/20250204025010_app-connections-and-secret-syncs-unique-constraint.ts index 348694ae7..f9b46559d 100644 --- a/backend/src/db/migrations/20250204025010_app-connections-and-secret-syncs-unique-constraint.ts +++ b/backend/src/db/migrations/20250204025010_app-connections-and-secret-syncs-unique-constraint.ts @@ -1,5 +1,6 @@ import { Knex } from "knex"; +import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists"; import { TableName } from "@app/db/schemas"; export async function up(knex: Knex): Promise { @@ -13,9 +14,7 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - await knex.schema.alterTable(TableName.AppConnection, (t) => { - t.dropUnique(["orgId", "name"]); - }); + await dropConstraintIfExists(TableName.AppConnection, "app_connections_orgid_name_unique", knex); await knex.schema.alterTable(TableName.SecretSync, (t) => { t.dropUnique(["projectId", "name"]); diff --git a/backend/src/db/migrations/20250819081226_identity-lockouts-ldap.ts b/backend/src/db/migrations/20250819081226_identity-lockouts-ldap.ts new file mode 100644 index 000000000..6cc851368 --- /dev/null +++ b/backend/src/db/migrations/20250819081226_identity-lockouts-ldap.ts @@ -0,0 +1,57 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.IdentityLdapAuth)) { + const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutEnabled"); + const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutThreshold"); + const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutDurationSeconds"); + const hasLockoutCounterReset = await knex.schema.hasColumn( + TableName.IdentityLdapAuth, + "lockoutCounterResetSeconds" + ); + + await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => { + if (!hasLockoutEnabled) { + t.boolean("lockoutEnabled").notNullable().defaultTo(true); + } + if (!hasLockoutThreshold) { + t.integer("lockoutThreshold").notNullable().defaultTo(3); + } + if (!hasLockoutDuration) { + t.integer("lockoutDurationSeconds").notNullable().defaultTo(300); // 5 minutes + } + if (!hasLockoutCounterReset) { + t.integer("lockoutCounterResetSeconds").notNullable().defaultTo(30); // 30 seconds + } + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.IdentityLdapAuth)) { + const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutEnabled"); + const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutThreshold"); + const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutDurationSeconds"); + const hasLockoutCounterReset = await knex.schema.hasColumn( + TableName.IdentityLdapAuth, + "lockoutCounterResetSeconds" + ); + + await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => { + if (hasLockoutEnabled) { + t.dropColumn("lockoutEnabled"); + } + if (hasLockoutThreshold) { + t.dropColumn("lockoutThreshold"); + } + if (hasLockoutDuration) { + t.dropColumn("lockoutDurationSeconds"); + } + if (hasLockoutCounterReset) { + t.dropColumn("lockoutCounterResetSeconds"); + } + }); + } +} diff --git a/backend/src/db/migrations/20250825131627_add-gateway-v2-pki-and-ssh-configs.ts b/backend/src/db/migrations/20250825131627_add-gateway-v2-pki-and-ssh-configs.ts new file mode 100644 index 000000000..812c4f48f --- /dev/null +++ b/backend/src/db/migrations/20250825131627_add-gateway-v2-pki-and-ssh-configs.ts @@ -0,0 +1,150 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.InstanceRelayConfig))) { + await knex.schema.createTable(TableName.InstanceRelayConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + + // Root CA for relay PKI + t.binary("encryptedRootRelayPkiCaPrivateKey").notNullable(); + t.binary("encryptedRootRelayPkiCaCertificate").notNullable(); + + // Instance CA for relay PKI + t.binary("encryptedInstanceRelayPkiCaPrivateKey").notNullable(); + t.binary("encryptedInstanceRelayPkiCaCertificate").notNullable(); + t.binary("encryptedInstanceRelayPkiCaCertificateChain").notNullable(); + + // Instance client/server intermediates for relay PKI + t.binary("encryptedInstanceRelayPkiClientCaPrivateKey").notNullable(); + t.binary("encryptedInstanceRelayPkiClientCaCertificate").notNullable(); + t.binary("encryptedInstanceRelayPkiClientCaCertificateChain").notNullable(); + t.binary("encryptedInstanceRelayPkiServerCaPrivateKey").notNullable(); + t.binary("encryptedInstanceRelayPkiServerCaCertificate").notNullable(); + t.binary("encryptedInstanceRelayPkiServerCaCertificateChain").notNullable(); + + // Org Parent CAs for relay + t.binary("encryptedOrgRelayPkiCaPrivateKey").notNullable(); + t.binary("encryptedOrgRelayPkiCaCertificate").notNullable(); + t.binary("encryptedOrgRelayPkiCaCertificateChain").notNullable(); + + // Instance SSH CAs for relay + t.binary("encryptedInstanceRelaySshClientCaPrivateKey").notNullable(); + t.binary("encryptedInstanceRelaySshClientCaPublicKey").notNullable(); + t.binary("encryptedInstanceRelaySshServerCaPrivateKey").notNullable(); + t.binary("encryptedInstanceRelaySshServerCaPublicKey").notNullable(); + }); + + await createOnUpdateTrigger(knex, TableName.InstanceRelayConfig); + } + + // Org-level relay configuration (one-to-one with organization) + if (!(await knex.schema.hasTable(TableName.OrgRelayConfig))) { + await knex.schema.createTable(TableName.OrgRelayConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + + t.uuid("orgId").notNullable().unique(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + // Org-scoped relay PKI (client + server) + t.binary("encryptedRelayPkiClientCaPrivateKey").notNullable(); + t.binary("encryptedRelayPkiClientCaCertificate").notNullable(); + t.binary("encryptedRelayPkiClientCaCertificateChain").notNullable(); + t.binary("encryptedRelayPkiServerCaPrivateKey").notNullable(); + t.binary("encryptedRelayPkiServerCaCertificate").notNullable(); + t.binary("encryptedRelayPkiServerCaCertificateChain").notNullable(); + + // Org-scoped relay SSH (client + server) + t.binary("encryptedRelaySshClientCaPrivateKey").notNullable(); + t.binary("encryptedRelaySshClientCaPublicKey").notNullable(); + t.binary("encryptedRelaySshServerCaPrivateKey").notNullable(); + t.binary("encryptedRelaySshServerCaPublicKey").notNullable(); + }); + + await createOnUpdateTrigger(knex, TableName.OrgRelayConfig); + } + + if (!(await knex.schema.hasTable(TableName.OrgGatewayConfigV2))) { + await knex.schema.createTable(TableName.OrgGatewayConfigV2, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("orgId").notNullable().unique(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.timestamps(true, true, true); + t.binary("encryptedRootGatewayCaPrivateKey").notNullable(); + t.binary("encryptedRootGatewayCaCertificate").notNullable(); + t.binary("encryptedGatewayServerCaPrivateKey").notNullable(); + t.binary("encryptedGatewayServerCaCertificate").notNullable(); + t.binary("encryptedGatewayServerCaCertificateChain").notNullable(); + t.binary("encryptedGatewayClientCaPrivateKey").notNullable(); + t.binary("encryptedGatewayClientCaCertificate").notNullable(); + t.binary("encryptedGatewayClientCaCertificateChain").notNullable(); + }); + + await createOnUpdateTrigger(knex, TableName.OrgGatewayConfigV2); + } + + if (!(await knex.schema.hasTable(TableName.Relay))) { + await knex.schema.createTable(TableName.Relay, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + + t.uuid("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + t.uuid("identityId"); + t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + + t.string("name").notNullable(); + t.string("host").notNullable(); + + t.unique(["orgId", "name"]); + }); + + await createOnUpdateTrigger(knex, TableName.Relay); + } + + if (!(await knex.schema.hasTable(TableName.GatewayV2))) { + await knex.schema.createTable(TableName.GatewayV2, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + t.uuid("identityId").notNullable().unique(); + t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + + t.uuid("relayId"); + t.foreign("relayId").references("id").inTable(TableName.Relay).onDelete("SET NULL"); + + t.string("name").notNullable(); + + t.unique(["orgId", "name"]); + + t.dateTime("heartbeat"); + }); + + await createOnUpdateTrigger(knex, TableName.GatewayV2); + } +} + +export async function down(knex: Knex): Promise { + await dropOnUpdateTrigger(knex, TableName.OrgRelayConfig); + await knex.schema.dropTableIfExists(TableName.OrgRelayConfig); + + await dropOnUpdateTrigger(knex, TableName.InstanceRelayConfig); + await knex.schema.dropTableIfExists(TableName.InstanceRelayConfig); + + await dropOnUpdateTrigger(knex, TableName.OrgGatewayConfigV2); + await knex.schema.dropTableIfExists(TableName.OrgGatewayConfigV2); + + await dropOnUpdateTrigger(knex, TableName.GatewayV2); + await knex.schema.dropTableIfExists(TableName.GatewayV2); + + await dropOnUpdateTrigger(knex, TableName.Relay); + await knex.schema.dropTableIfExists(TableName.Relay); +} diff --git a/backend/src/db/migrations/20250829203610_user-notifications.ts b/backend/src/db/migrations/20250829203610_user-notifications.ts new file mode 100644 index 000000000..6fabfa882 --- /dev/null +++ b/backend/src/db/migrations/20250829203610_user-notifications.ts @@ -0,0 +1,50 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.UserNotifications))) { + const createTableSql = knex.schema + .createTable(TableName.UserNotifications, (t) => { + t.uuid("id").defaultTo(knex.fn.uuid()); + t.uuid("userId").notNullable(); + t.uuid("orgId").nullable(); + + t.string("type").notNullable(); + t.string("title").notNullable(); // Markdown + t.text("body").nullable(); // Markdown + t.string("link").nullable(); + t.boolean("isRead").notNullable().defaultTo(false); + + t.timestamps(true, true, true); + + t.primary(["id", "createdAt"]); + }) + .toString(); + + await knex.schema.raw(` + ${createTableSql} PARTITION BY RANGE ("createdAt"); + `); + + await knex.schema.raw( + `CREATE TABLE ${TableName.UserNotifications}_default PARTITION OF ${TableName.UserNotifications} DEFAULT` + ); + + await knex.schema.alterTable(TableName.UserNotifications, (t) => { + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + t.index("type"); + t.index(["userId", "isRead"]); + t.index(["userId", "createdAt", "orgId"]); + }); + + await createOnUpdateTrigger(knex, TableName.UserNotifications); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.UserNotifications); + await dropOnUpdateTrigger(knex, TableName.UserNotifications); +} diff --git a/backend/src/db/migrations/20250901091637_add-gateway-v2-id-columns.ts b/backend/src/db/migrations/20250901091637_add-gateway-v2-id-columns.ts new file mode 100644 index 000000000..cb46b1261 --- /dev/null +++ b/backend/src/db/migrations/20250901091637_add-gateway-v2-id-columns.ts @@ -0,0 +1,33 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayV2Id"))) { + await knex.schema.alterTable(TableName.DynamicSecret, (table) => { + table.uuid("gatewayV2Id"); + table.foreign("gatewayV2Id").references("id").inTable(TableName.GatewayV2).onDelete("SET NULL"); + }); + } + + if (!(await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayV2Id"))) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => { + table.uuid("gatewayV2Id"); + table.foreign("gatewayV2Id").references("id").inTable(TableName.GatewayV2).onDelete("SET NULL"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayV2Id")) { + await knex.schema.alterTable(TableName.DynamicSecret, (table) => { + table.dropColumn("gatewayV2Id"); + }); + } + + if (await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayV2Id")) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => { + table.dropColumn("gatewayV2Id"); + }); + } +} diff --git a/backend/src/db/migrations/20250903191434_audit-log-stream-v2.ts b/backend/src/db/migrations/20250903191434_audit-log-stream-v2.ts new file mode 100644 index 000000000..a70dcb8b9 --- /dev/null +++ b/backend/src/db/migrations/20250903191434_audit-log-stream-v2.ts @@ -0,0 +1,221 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { crypto } from "@app/lib/crypto/cryptography"; +import { KmsDataKey } from "@app/services/kms/kms-types"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.AuditLogStream)) { + const hasProvider = await knex.schema.hasColumn(TableName.AuditLogStream, "provider"); + const hasEncryptedCredentials = await knex.schema.hasColumn(TableName.AuditLogStream, "encryptedCredentials"); + + await knex.schema.alterTable(TableName.AuditLogStream, (t) => { + if (!hasProvider) t.string("provider").notNullable().defaultTo("custom"); + if (!hasEncryptedCredentials) t.binary("encryptedCredentials"); + + // This column will no longer be used but we're not dropping it so that we can have a backup in case the migration goes wrong + t.string("url").nullable().alter(); + }); + + if (!hasEncryptedCredentials) { + const superAdminDAL = superAdminDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL); + const keyStore = inMemoryKeyStore(); + + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const logStreams = await knex(TableName.AuditLogStream).select( + "id", + "orgId", + + "url", + "encryptedHeadersAlgorithm", + "encryptedHeadersCiphertext", + "encryptedHeadersIV", + "encryptedHeadersKeyEncoding", + "encryptedHeadersTag" + ); + + const updatedLogStreams = await Promise.all( + logStreams.map(async (el) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId: el.orgId + }, + knex + ); + orgEncryptionRingBuffer.push(el.orgId, orgKmsService); + } + + const provider = "custom"; + let credentials; + + if ( + el.encryptedHeadersTag && + el.encryptedHeadersIV && + el.encryptedHeadersCiphertext && + el.encryptedHeadersKeyEncoding + ) { + const decryptedHeaders = crypto + .encryption() + .symmetric() + .decryptWithRootEncryptionKey({ + tag: el.encryptedHeadersTag, + iv: el.encryptedHeadersIV, + ciphertext: el.encryptedHeadersCiphertext, + keyEncoding: el.encryptedHeadersKeyEncoding as SecretKeyEncoding + }); + + credentials = { + url: el.url, + headers: JSON.parse(decryptedHeaders) + }; + } else { + credentials = { + url: el.url, + headers: [] + }; + } + + const encryptedCredentials = orgKmsService.encryptor({ + plainText: Buffer.from(JSON.stringify(credentials), "utf8") + }).cipherTextBlob; + + return { + id: el.id, + orgId: el.orgId, + url: el.url, + provider, + encryptedCredentials + }; + }) + ); + + for (let i = 0; i < updatedLogStreams.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.AuditLogStream) + .insert(updatedLogStreams.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + + await knex.schema.alterTable(TableName.AuditLogStream, (t) => { + t.binary("encryptedCredentials").notNullable().alter(); + }); + } + } +} + +// IMPORTANT: The down migration does not utilize the existing "url" and encrypted header columns +// because we're taking the latest data from the credentials column and re-encrypting it into relevant columns +// +// If this down migration was to fail, you can fall-back to the existing URL and encrypted header columns to retrieve +// data that was created prior to this migration + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.AuditLogStream)) { + const hasProvider = await knex.schema.hasColumn(TableName.AuditLogStream, "provider"); + const hasEncryptedCredentials = await knex.schema.hasColumn(TableName.AuditLogStream, "encryptedCredentials"); + + if (hasEncryptedCredentials) { + const superAdminDAL = superAdminDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL); + const keyStore = inMemoryKeyStore(); + + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const logStreamsToRevert = await knex(TableName.AuditLogStream) + .select("id", "orgId", "encryptedCredentials") + .where("provider", "custom") + .whereNotNull("encryptedCredentials"); + + const updatedLogStreams = await Promise.all( + logStreamsToRevert.map(async (el) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId: el.orgId + }, + knex + ); + orgEncryptionRingBuffer.push(el.orgId, orgKmsService); + } + + const decryptedCredentials = orgKmsService + .decryptor({ + cipherTextBlob: el.encryptedCredentials + }) + .toString(); + + const credentials: { url: string; headers: { key: string; value: string }[] } = + JSON.parse(decryptedCredentials); + + const originalUrl: string = credentials.url; + + const encryptedHeadersResult = crypto + .encryption() + .symmetric() + .encryptWithRootEncryptionKey(JSON.stringify(credentials.headers), envConfig); + + const encryptedHeadersAlgorithm: string = encryptedHeadersResult.algorithm; + const encryptedHeadersCiphertext: string = encryptedHeadersResult.ciphertext; + const encryptedHeadersIV: string = encryptedHeadersResult.iv; + const encryptedHeadersKeyEncoding: string = encryptedHeadersResult.encoding; + const encryptedHeadersTag: string = encryptedHeadersResult.tag; + + return { + id: el.id, + orgId: el.orgId, + encryptedCredentials: el.encryptedCredentials, + + url: originalUrl, + encryptedHeadersAlgorithm, + encryptedHeadersCiphertext, + encryptedHeadersIV, + encryptedHeadersKeyEncoding, + encryptedHeadersTag + }; + }) + ); + + for (let i = 0; i < updatedLogStreams.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.AuditLogStream) + .insert(updatedLogStreams.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + + await knex(TableName.AuditLogStream) + .where((qb) => { + void qb.whereNot("provider", "custom").orWhereNull("url"); + }) + .del(); + } + + await knex.schema.alterTable(TableName.AuditLogStream, (t) => { + t.string("url").notNullable().alter(); + + if (hasProvider) t.dropColumn("provider"); + if (hasEncryptedCredentials) t.dropColumn("encryptedCredentials"); + }); + } +} diff --git a/backend/src/db/migrations/20250908193226_sql-cache_int.ts b/backend/src/db/migrations/20250908193226_sql-cache_int.ts new file mode 100644 index 000000000..0e15c10d1 --- /dev/null +++ b/backend/src/db/migrations/20250908193226_sql-cache_int.ts @@ -0,0 +1,18 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.KeyValueStore))) { + await knex.schema.createTable(TableName.KeyValueStore, (t) => { + t.text("key").primary(); + t.bigint("integerValue"); + t.datetime("expiresAt"); + t.timestamps(true, true, true); + }); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.KeyValueStore); +} diff --git a/backend/src/db/migrations/20250911133926_add-auth-token-payload-column.ts b/backend/src/db/migrations/20250911133926_add-auth-token-payload-column.ts new file mode 100644 index 000000000..4a1e3f352 --- /dev/null +++ b/backend/src/db/migrations/20250911133926_add-auth-token-payload-column.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasPayloadCol = await knex.schema.hasColumn(TableName.AuthTokens, "payload"); + + if (!hasPayloadCol) { + await knex.schema.alterTable(TableName.AuthTokens, (t) => { + t.text("payload").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasPayloadCol = await knex.schema.hasColumn(TableName.AuthTokens, "payload"); + + if (hasPayloadCol) { + await knex.schema.alterTable(TableName.AuthTokens, (t) => { + t.dropColumn("payload"); + }); + } +} diff --git a/backend/src/db/migrations/20250912011133_app-connection-project-id-col.ts b/backend/src/db/migrations/20250912011133_app-connection-project-id-col.ts new file mode 100644 index 000000000..5c846a739 --- /dev/null +++ b/backend/src/db/migrations/20250912011133_app-connection-project-id-col.ts @@ -0,0 +1,41 @@ +import { Knex } from "knex"; + +import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists"; +import { TableName } from "@app/db/schemas"; + +const UNIQUE_NAME_ORG_CONNECTION_INDEX = "unique_name_org_app_connection"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.AppConnection)) { + // we can't add the constraint back after up since there may be conflicting names so we do if exists + await dropConstraintIfExists(TableName.AppConnection, "app_connections_orgid_name_unique", knex); + + if (!(await knex.schema.hasColumn(TableName.AppConnection, "projectId"))) { + await knex.schema.alterTable(TableName.AppConnection, (t) => { + t.string("projectId").nullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + // unique name for project-level connections + t.unique(["name", "projectId", "orgId"]); + }); + + // unique name for org-level connections + await knex.raw(` + CREATE UNIQUE INDEX ${UNIQUE_NAME_ORG_CONNECTION_INDEX} + ON ${TableName.AppConnection} ("name", "orgId") + WHERE "projectId" IS NULL + `); + } + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.AppConnection)) { + if (await knex.schema.hasColumn(TableName.AppConnection, "projectId")) { + await knex.schema.alterTable(TableName.AppConnection, (t) => { + t.dropUnique(["name", "projectId", "orgId"]); + t.dropColumn("projectId"); + }); + await dropConstraintIfExists(TableName.AppConnection, UNIQUE_NAME_ORG_CONNECTION_INDEX, knex); + } + } +} diff --git a/backend/src/db/schemas/app-connections.ts b/backend/src/db/schemas/app-connections.ts index 2218b75ce..41d1df17f 100644 --- a/backend/src/db/schemas/app-connections.ts +++ b/backend/src/db/schemas/app-connections.ts @@ -21,7 +21,8 @@ export const AppConnectionsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), isPlatformManagedCredentials: z.boolean().default(false).nullable().optional(), - gatewayId: z.string().uuid().nullable().optional() + gatewayId: z.string().uuid().nullable().optional(), + projectId: z.string().nullable().optional() }); export type TAppConnections = z.infer; diff --git a/backend/src/db/schemas/audit-log-streams.ts b/backend/src/db/schemas/audit-log-streams.ts index 901dd8d27..a3f6bafba 100644 --- a/backend/src/db/schemas/audit-log-streams.ts +++ b/backend/src/db/schemas/audit-log-streams.ts @@ -5,11 +5,13 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const AuditLogStreamsSchema = z.object({ id: z.string().uuid(), - url: z.string(), + url: z.string().nullable().optional(), encryptedHeadersCiphertext: z.string().nullable().optional(), encryptedHeadersIV: z.string().nullable().optional(), encryptedHeadersTag: z.string().nullable().optional(), @@ -17,7 +19,9 @@ export const AuditLogStreamsSchema = z.object({ encryptedHeadersKeyEncoding: z.string().nullable().optional(), orgId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + provider: z.string().default("custom"), + encryptedCredentials: zodBuffer }); export type TAuditLogStreams = z.infer; diff --git a/backend/src/db/schemas/auth-tokens.ts b/backend/src/db/schemas/auth-tokens.ts index 0d3e93219..396c06f13 100644 --- a/backend/src/db/schemas/auth-tokens.ts +++ b/backend/src/db/schemas/auth-tokens.ts @@ -18,7 +18,8 @@ export const AuthTokensSchema = z.object({ updatedAt: z.date(), userId: z.string().uuid().nullable().optional(), orgId: z.string().uuid().nullable().optional(), - aliasId: z.string().nullable().optional() + aliasId: z.string().nullable().optional(), + payload: z.string().nullable().optional() }); export type TAuthTokens = z.infer; diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts index 637d0c632..526239f1c 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -29,7 +29,8 @@ export const DynamicSecretsSchema = z.object({ encryptedInput: zodBuffer, projectGatewayId: z.string().uuid().nullable().optional(), gatewayId: z.string().uuid().nullable().optional(), - usernameTemplate: z.string().nullable().optional() + usernameTemplate: z.string().nullable().optional(), + gatewayV2Id: z.string().uuid().nullable().optional() }); export type TDynamicSecrets = z.infer; diff --git a/backend/src/db/schemas/gateways-v2.ts b/backend/src/db/schemas/gateways-v2.ts new file mode 100644 index 000000000..6aff8a168 --- /dev/null +++ b/backend/src/db/schemas/gateways-v2.ts @@ -0,0 +1,23 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const GatewaysV2Schema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + orgId: z.string().uuid(), + identityId: z.string().uuid(), + relayId: z.string().uuid().nullable().optional(), + name: z.string(), + heartbeat: z.date().nullable().optional() +}); + +export type TGatewaysV2 = z.infer; +export type TGatewaysV2Insert = Omit, TImmutableDBKeys>; +export type TGatewaysV2Update = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/identity-kubernetes-auths.ts b/backend/src/db/schemas/identity-kubernetes-auths.ts index deb78bf8a..4789ef365 100644 --- a/backend/src/db/schemas/identity-kubernetes-auths.ts +++ b/backend/src/db/schemas/identity-kubernetes-auths.ts @@ -32,7 +32,8 @@ export const IdentityKubernetesAuthsSchema = z.object({ encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(), gatewayId: z.string().uuid().nullable().optional(), accessTokenPeriod: z.coerce.number().default(0), - tokenReviewMode: z.string().default("api") + tokenReviewMode: z.string().default("api"), + gatewayV2Id: z.string().uuid().nullable().optional() }); export type TIdentityKubernetesAuths = z.infer; diff --git a/backend/src/db/schemas/identity-ldap-auths.ts b/backend/src/db/schemas/identity-ldap-auths.ts index 3e4d88649..87c7f1608 100644 --- a/backend/src/db/schemas/identity-ldap-auths.ts +++ b/backend/src/db/schemas/identity-ldap-auths.ts @@ -26,7 +26,11 @@ export const IdentityLdapAuthsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), accessTokenPeriod: z.coerce.number().default(0), - templateId: z.string().uuid().nullable().optional() + templateId: z.string().uuid().nullable().optional(), + lockoutEnabled: z.boolean().default(true), + lockoutThreshold: z.number().default(3), + lockoutDurationSeconds: z.number().default(300), + lockoutCounterResetSeconds: z.number().default(30) }); export type TIdentityLdapAuths = z.infer; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 1642c3555..f09e3c263 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -31,6 +31,7 @@ export * from "./folder-commits"; export * from "./folder-tree-checkpoint-resources"; export * from "./folder-tree-checkpoints"; export * from "./gateways"; +export * from "./gateways-v2"; export * from "./git-app-install-sessions"; export * from "./git-app-org"; export * from "./github-org-sync-configs"; @@ -57,10 +58,12 @@ export * from "./identity-token-auths"; export * from "./identity-ua-client-secrets"; export * from "./identity-universal-auths"; export * from "./incident-contacts"; +export * from "./instance-relay-config"; export * from "./integration-auths"; export * from "./integrations"; export * from "./internal-certificate-authorities"; export * from "./internal-kms"; +export * from "./key-value-store"; export * from "./kmip-client-certificates"; export * from "./kmip-clients"; export * from "./kmip-org-configs"; @@ -75,7 +78,9 @@ export * from "./models"; export * from "./oidc-configs"; export * from "./org-bots"; export * from "./org-gateway-config"; +export * from "./org-gateway-config-v2"; export * from "./org-memberships"; +export * from "./org-relay-config"; export * from "./org-roles"; export * from "./organizations"; export * from "./pki-alerts"; @@ -96,6 +101,7 @@ export * from "./project-user-additional-privilege"; export * from "./project-user-membership-roles"; export * from "./projects"; export * from "./rate-limit"; +export * from "./relays"; export * from "./resource-metadata"; export * from "./saml-configs"; export * from "./scim-tokens"; diff --git a/backend/src/db/schemas/instance-relay-config.ts b/backend/src/db/schemas/instance-relay-config.ts new file mode 100644 index 000000000..8b18ef0f5 --- /dev/null +++ b/backend/src/db/schemas/instance-relay-config.ts @@ -0,0 +1,38 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const InstanceRelayConfigSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + encryptedRootRelayPkiCaPrivateKey: zodBuffer, + encryptedRootRelayPkiCaCertificate: zodBuffer, + encryptedInstanceRelayPkiCaPrivateKey: zodBuffer, + encryptedInstanceRelayPkiCaCertificate: zodBuffer, + encryptedInstanceRelayPkiCaCertificateChain: zodBuffer, + encryptedInstanceRelayPkiClientCaPrivateKey: zodBuffer, + encryptedInstanceRelayPkiClientCaCertificate: zodBuffer, + encryptedInstanceRelayPkiClientCaCertificateChain: zodBuffer, + encryptedInstanceRelayPkiServerCaPrivateKey: zodBuffer, + encryptedInstanceRelayPkiServerCaCertificate: zodBuffer, + encryptedInstanceRelayPkiServerCaCertificateChain: zodBuffer, + encryptedOrgRelayPkiCaPrivateKey: zodBuffer, + encryptedOrgRelayPkiCaCertificate: zodBuffer, + encryptedOrgRelayPkiCaCertificateChain: zodBuffer, + encryptedInstanceRelaySshClientCaPrivateKey: zodBuffer, + encryptedInstanceRelaySshClientCaPublicKey: zodBuffer, + encryptedInstanceRelaySshServerCaPrivateKey: zodBuffer, + encryptedInstanceRelaySshServerCaPublicKey: zodBuffer +}); + +export type TInstanceRelayConfig = z.infer; +export type TInstanceRelayConfigInsert = Omit, TImmutableDBKeys>; +export type TInstanceRelayConfigUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/key-value-store.ts b/backend/src/db/schemas/key-value-store.ts new file mode 100644 index 000000000..448c78f24 --- /dev/null +++ b/backend/src/db/schemas/key-value-store.ts @@ -0,0 +1,20 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const KeyValueStoreSchema = z.object({ + key: z.string(), + integerValue: z.coerce.number().nullable().optional(), + expiresAt: z.date().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TKeyValueStore = z.infer; +export type TKeyValueStoreInsert = Omit, TImmutableDBKeys>; +export type TKeyValueStoreUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 855934b28..a4585972f 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -131,6 +131,7 @@ export enum TableName { SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2", SnapshotSecretV2 = "secret_snapshot_secrets_v2", ProjectSplitBackfillIds = "project_split_backfill_ids", + UserNotifications = "user_notifications", // Gateway OrgGatewayConfig = "org_gateway_config", Gateway = "gateways", @@ -178,7 +179,16 @@ export enum TableName { SecretScanningConfig = "secret_scanning_configs", // reminders Reminder = "reminders", - ReminderRecipient = "reminders_recipients" + ReminderRecipient = "reminders_recipients", + + // gateway v2 + InstanceRelayConfig = "instance_relay_config", + OrgRelayConfig = "org_relay_config", + OrgGatewayConfigV2 = "org_gateway_config_v2", + Relay = "relays", + GatewayV2 = "gateways_v2", + + KeyValueStore = "key_value_store" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId"; diff --git a/backend/src/db/schemas/org-gateway-config-v2.ts b/backend/src/db/schemas/org-gateway-config-v2.ts new file mode 100644 index 000000000..fab9a3182 --- /dev/null +++ b/backend/src/db/schemas/org-gateway-config-v2.ts @@ -0,0 +1,29 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const OrgGatewayConfigV2Schema = z.object({ + id: z.string().uuid(), + orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + encryptedRootGatewayCaPrivateKey: zodBuffer, + encryptedRootGatewayCaCertificate: zodBuffer, + encryptedGatewayServerCaPrivateKey: zodBuffer, + encryptedGatewayServerCaCertificate: zodBuffer, + encryptedGatewayServerCaCertificateChain: zodBuffer, + encryptedGatewayClientCaPrivateKey: zodBuffer, + encryptedGatewayClientCaCertificate: zodBuffer, + encryptedGatewayClientCaCertificateChain: zodBuffer +}); + +export type TOrgGatewayConfigV2 = z.infer; +export type TOrgGatewayConfigV2Insert = Omit, TImmutableDBKeys>; +export type TOrgGatewayConfigV2Update = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/org-relay-config.ts b/backend/src/db/schemas/org-relay-config.ts new file mode 100644 index 000000000..1752da76a --- /dev/null +++ b/backend/src/db/schemas/org-relay-config.ts @@ -0,0 +1,31 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const OrgRelayConfigSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + orgId: z.string().uuid(), + encryptedRelayPkiClientCaPrivateKey: zodBuffer, + encryptedRelayPkiClientCaCertificate: zodBuffer, + encryptedRelayPkiClientCaCertificateChain: zodBuffer, + encryptedRelayPkiServerCaPrivateKey: zodBuffer, + encryptedRelayPkiServerCaCertificate: zodBuffer, + encryptedRelayPkiServerCaCertificateChain: zodBuffer, + encryptedRelaySshClientCaPrivateKey: zodBuffer, + encryptedRelaySshClientCaPublicKey: zodBuffer, + encryptedRelaySshServerCaPrivateKey: zodBuffer, + encryptedRelaySshServerCaPublicKey: zodBuffer +}); + +export type TOrgRelayConfig = z.infer; +export type TOrgRelayConfigInsert = Omit, TImmutableDBKeys>; +export type TOrgRelayConfigUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/relays.ts b/backend/src/db/schemas/relays.ts new file mode 100644 index 000000000..4bb615e96 --- /dev/null +++ b/backend/src/db/schemas/relays.ts @@ -0,0 +1,22 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const RelaysSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + orgId: z.string().uuid().nullable().optional(), + identityId: z.string().uuid().nullable().optional(), + name: z.string(), + host: z.string() +}); + +export type TRelays = z.infer; +export type TRelaysInsert = Omit, TImmutableDBKeys>; +export type TRelaysUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/user-notifications.ts b/backend/src/db/schemas/user-notifications.ts new file mode 100644 index 000000000..146526600 --- /dev/null +++ b/backend/src/db/schemas/user-notifications.ts @@ -0,0 +1,25 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const UserNotificationsSchema = z.object({ + id: z.string().uuid(), + userId: z.string().uuid(), + orgId: z.string().uuid().nullable().optional(), + type: z.string(), + title: z.string(), + body: z.string().nullable().optional(), + link: z.string().nullable().optional(), + isRead: z.boolean().default(false), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TUserNotifications = z.infer; +export type TUserNotificationsInsert = Omit, TImmutableDBKeys>; +export type TUserNotificationsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/ee/routes/v1/audit-log-stream-router.ts b/backend/src/ee/routes/v1/audit-log-stream-router.ts deleted file mode 100644 index 17bd9e64b..000000000 --- a/backend/src/ee/routes/v1/audit-log-stream-router.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { z } from "zod"; - -import { AUDIT_LOG_STREAMS } from "@app/lib/api-docs"; -import { readLimit } from "@app/server/config/rateLimiter"; -import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedAuditLogStreamSchema } from "@app/server/routes/sanitizedSchemas"; -import { AuthMode } from "@app/services/auth/auth-type"; - -export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) => { - server.route({ - method: "POST", - url: "/", - config: { - rateLimit: readLimit - }, - schema: { - description: "Create an Audit Log Stream.", - security: [ - { - bearerAuth: [] - } - ], - body: z.object({ - url: z.string().min(1).describe(AUDIT_LOG_STREAMS.CREATE.url), - headers: z - .object({ - key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.key), - value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.value) - }) - .describe(AUDIT_LOG_STREAMS.CREATE.headers.desc) - .array() - .optional() - }), - response: { - 200: z.object({ - auditLogStream: SanitizedAuditLogStreamSchema - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const auditLogStream = await server.services.auditLogStream.create({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - url: req.body.url, - headers: req.body.headers - }); - - return { auditLogStream }; - } - }); - - server.route({ - method: "PATCH", - url: "/:id", - config: { - rateLimit: readLimit - }, - schema: { - description: "Update an Audit Log Stream by ID.", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - id: z.string().describe(AUDIT_LOG_STREAMS.UPDATE.id) - }), - body: z.object({ - url: z.string().optional().describe(AUDIT_LOG_STREAMS.UPDATE.url), - headers: z - .object({ - key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.key), - value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.value) - }) - .describe(AUDIT_LOG_STREAMS.UPDATE.headers.desc) - .array() - .optional() - }), - response: { - 200: z.object({ - auditLogStream: SanitizedAuditLogStreamSchema - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const auditLogStream = await server.services.auditLogStream.updateById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - id: req.params.id, - url: req.body.url, - headers: req.body.headers - }); - - return { auditLogStream }; - } - }); - - server.route({ - method: "DELETE", - url: "/:id", - config: { - rateLimit: readLimit - }, - schema: { - description: "Delete an Audit Log Stream by ID.", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - id: z.string().describe(AUDIT_LOG_STREAMS.DELETE.id) - }), - response: { - 200: z.object({ - auditLogStream: SanitizedAuditLogStreamSchema - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const auditLogStream = await server.services.auditLogStream.deleteById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - id: req.params.id - }); - - return { auditLogStream }; - } - }); - - server.route({ - method: "GET", - url: "/:id", - config: { - rateLimit: readLimit - }, - schema: { - description: "Get an Audit Log Stream by ID.", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - id: z.string().describe(AUDIT_LOG_STREAMS.GET_BY_ID.id) - }), - response: { - 200: z.object({ - auditLogStream: SanitizedAuditLogStreamSchema.extend({ - headers: z - .object({ - key: z.string(), - value: z.string() - }) - .array() - .optional() - }) - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const auditLogStream = await server.services.auditLogStream.getById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - id: req.params.id - }); - - return { auditLogStream }; - } - }); - - server.route({ - method: "GET", - url: "/", - config: { - rateLimit: readLimit - }, - schema: { - description: "List Audit Log Streams.", - security: [ - { - bearerAuth: [] - } - ], - response: { - 200: z.object({ - auditLogStreams: SanitizedAuditLogStreamSchema.array() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const auditLogStreams = await server.services.auditLogStream.list({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod - }); - - return { auditLogStreams }; - } - }); -}; diff --git a/backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-endpoints.ts b/backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-endpoints.ts new file mode 100644 index 000000000..816fc62fb --- /dev/null +++ b/backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-endpoints.ts @@ -0,0 +1,142 @@ +import { z } from "zod"; + +import { LogProvider } from "@app/ee/services/audit-log-stream/audit-log-stream-enums"; +import { TAuditLogStream } from "@app/ee/services/audit-log-stream/audit-log-stream-types"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerAuditLogStreamEndpoints = ({ + server, + provider, + createSchema, + updateSchema, + sanitizedResponseSchema +}: { + server: FastifyZodProvider; + provider: LogProvider; + createSchema: z.ZodType<{ + credentials: T["credentials"]; + }>; + updateSchema: z.ZodType<{ + credentials: T["credentials"]; + }>; + sanitizedResponseSchema: z.ZodTypeAny; +}) => { + server.route({ + method: "GET", + url: "/:logStreamId", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + logStreamId: z.string().uuid() + }), + response: { + 200: z.object({ + auditLogStream: sanitizedResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { logStreamId } = req.params; + + const auditLogStream = await server.services.auditLogStream.getById(logStreamId, provider, req.permission); + + return { auditLogStream }; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + body: createSchema, + response: { + 200: z.object({ + auditLogStream: sanitizedResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { credentials } = req.body; + + const auditLogStream = await server.services.auditLogStream.create( + { + provider, + credentials + }, + req.permission + ); + + return { auditLogStream }; + } + }); + + server.route({ + method: "PATCH", + url: "/:logStreamId", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + logStreamId: z.string().uuid() + }), + body: updateSchema, + response: { + 200: z.object({ + auditLogStream: sanitizedResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { logStreamId } = req.params; + const { credentials } = req.body; + + const auditLogStream = await server.services.auditLogStream.updateById( + { + logStreamId, + provider, + credentials + }, + req.permission + ); + + return { auditLogStream }; + } + }); + + server.route({ + method: "DELETE", + url: "/:logStreamId", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + logStreamId: z.string().uuid() + }), + response: { + 200: z.object({ + auditLogStream: sanitizedResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { logStreamId } = req.params; + + const auditLogStream = await server.services.auditLogStream.deleteById(logStreamId, provider, req.permission); + + return { auditLogStream }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-router.ts b/backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-router.ts new file mode 100644 index 000000000..48eed14c9 --- /dev/null +++ b/backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-router.ts @@ -0,0 +1,85 @@ +import { z } from "zod"; + +import { + AzureProviderListItemSchema, + SanitizedAzureProviderSchema +} from "@app/ee/services/audit-log-stream/azure/azure-provider-schemas"; +import { + CriblProviderListItemSchema, + SanitizedCriblProviderSchema +} from "@app/ee/services/audit-log-stream/cribl/cribl-provider-schemas"; +import { + CustomProviderListItemSchema, + SanitizedCustomProviderSchema +} from "@app/ee/services/audit-log-stream/custom/custom-provider-schemas"; +import { + DatadogProviderListItemSchema, + SanitizedDatadogProviderSchema +} from "@app/ee/services/audit-log-stream/datadog/datadog-provider-schemas"; +import { + SanitizedSplunkProviderSchema, + SplunkProviderListItemSchema +} from "@app/ee/services/audit-log-stream/splunk/splunk-provider-schemas"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const SanitizedAuditLogStreamSchema = z.union([ + SanitizedCustomProviderSchema, + SanitizedDatadogProviderSchema, + SanitizedSplunkProviderSchema, + SanitizedAzureProviderSchema, + SanitizedCriblProviderSchema +]); + +const ProviderOptionsSchema = z.discriminatedUnion("provider", [ + CustomProviderListItemSchema, + DatadogProviderListItemSchema, + SplunkProviderListItemSchema, + AzureProviderListItemSchema, + CriblProviderListItemSchema +]); + +export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/options", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + providerOptions: ProviderOptionsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: () => { + const providerOptions = server.services.auditLogStream.listProviderOptions(); + + return { providerOptions }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + auditLogStreams: SanitizedAuditLogStreamSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const auditLogStreams = await server.services.auditLogStream.list(req.permission); + + return { auditLogStreams }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/audit-log-stream-routers/index.ts b/backend/src/ee/routes/v1/audit-log-stream-routers/index.ts new file mode 100644 index 000000000..ad338c801 --- /dev/null +++ b/backend/src/ee/routes/v1/audit-log-stream-routers/index.ts @@ -0,0 +1,79 @@ +import { LogProvider } from "@app/ee/services/audit-log-stream/audit-log-stream-enums"; +import { + CreateAzureProviderLogStreamSchema, + SanitizedAzureProviderSchema, + UpdateAzureProviderLogStreamSchema +} from "@app/ee/services/audit-log-stream/azure/azure-provider-schemas"; +import { + CreateCriblProviderLogStreamSchema, + SanitizedCriblProviderSchema, + UpdateCriblProviderLogStreamSchema +} from "@app/ee/services/audit-log-stream/cribl/cribl-provider-schemas"; +import { + CreateCustomProviderLogStreamSchema, + SanitizedCustomProviderSchema, + UpdateCustomProviderLogStreamSchema +} from "@app/ee/services/audit-log-stream/custom/custom-provider-schemas"; +import { + CreateDatadogProviderLogStreamSchema, + SanitizedDatadogProviderSchema, + UpdateDatadogProviderLogStreamSchema +} from "@app/ee/services/audit-log-stream/datadog/datadog-provider-schemas"; +import { + CreateSplunkProviderLogStreamSchema, + SanitizedSplunkProviderSchema, + UpdateSplunkProviderLogStreamSchema +} from "@app/ee/services/audit-log-stream/splunk/splunk-provider-schemas"; + +import { registerAuditLogStreamEndpoints } from "./audit-log-stream-endpoints"; + +export * from "./audit-log-stream-router"; + +export const AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP: Record Promise> = + { + [LogProvider.Azure]: async (server: FastifyZodProvider) => { + registerAuditLogStreamEndpoints({ + server, + provider: LogProvider.Azure, + sanitizedResponseSchema: SanitizedAzureProviderSchema, + createSchema: CreateAzureProviderLogStreamSchema, + updateSchema: UpdateAzureProviderLogStreamSchema + }); + }, + [LogProvider.Custom]: async (server: FastifyZodProvider) => { + registerAuditLogStreamEndpoints({ + server, + provider: LogProvider.Custom, + sanitizedResponseSchema: SanitizedCustomProviderSchema, + createSchema: CreateCustomProviderLogStreamSchema, + updateSchema: UpdateCustomProviderLogStreamSchema + }); + }, + [LogProvider.Datadog]: async (server: FastifyZodProvider) => { + registerAuditLogStreamEndpoints({ + server, + provider: LogProvider.Datadog, + sanitizedResponseSchema: SanitizedDatadogProviderSchema, + createSchema: CreateDatadogProviderLogStreamSchema, + updateSchema: UpdateDatadogProviderLogStreamSchema + }); + }, + [LogProvider.Splunk]: async (server: FastifyZodProvider) => { + registerAuditLogStreamEndpoints({ + server, + provider: LogProvider.Splunk, + sanitizedResponseSchema: SanitizedSplunkProviderSchema, + createSchema: CreateSplunkProviderLogStreamSchema, + updateSchema: UpdateSplunkProviderLogStreamSchema + }); + }, + [LogProvider.Cribl]: async (server: FastifyZodProvider) => { + registerAuditLogStreamEndpoints({ + server, + provider: LogProvider.Cribl, + sanitizedResponseSchema: SanitizedCriblProviderSchema, + createSchema: CreateCriblProviderLogStreamSchema, + updateSchema: UpdateCriblProviderLogStreamSchema + }); + } + }; diff --git a/backend/src/ee/routes/v1/deprecated-project-role-router.ts b/backend/src/ee/routes/v1/deprecated-project-role-router.ts new file mode 100644 index 000000000..d0ffc0239 --- /dev/null +++ b/backend/src/ee/routes/v1/deprecated-project-role-router.ts @@ -0,0 +1,287 @@ +import { packRules } from "@casl/ability/extra"; +import { z } from "zod"; + +import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; +import { + backfillPermissionV1SchemaToV2Schema, + ProjectPermissionV1Schema +} from "@app/ee/services/permission/project-permission"; +import { PROJECT_ROLE } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types"; + +export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/:projectSlug/roles", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Create a project role", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug) + }), + body: z.object({ + slug: slugSchema({ max: 64 }) + .refine( + (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), + "Please choose a different slug, the slug you have entered is reserved" + ) + .describe(PROJECT_ROLE.CREATE.slug), + name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name), + description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description), + permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions) + }), + response: { + 200: z.object({ + role: SanitizedRoleSchemaV1 + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const role = await server.services.projectRole.createRole({ + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + filter: { + type: ProjectRoleServiceIdentifierType.SLUG, + projectSlug: req.params.projectSlug + }, + data: { + ...req.body, + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))) + } + }); + + return { role }; + } + }); + + server.route({ + method: "PATCH", + url: "/:projectSlug/roles/:roleId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Update a project role", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectSlug: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectSlug), + roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId) + }), + body: z.object({ + slug: slugSchema({ max: 64 }) + .refine( + (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), + "Please choose a different slug, the slug you have entered is reserved" + ) + .describe(PROJECT_ROLE.UPDATE.slug) + .optional(), + name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), + description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description), + permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() + }), + response: { + 200: z.object({ + role: SanitizedRoleSchemaV1 + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const role = await server.services.projectRole.updateRole({ + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + roleId: req.params.roleId, + data: { + ...req.body, + permissions: req.body.permissions + ? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))) + : undefined + } + }); + return { role }; + } + }); + + server.route({ + method: "DELETE", + url: "/:projectSlug/roles/:roleId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Delete a project role", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectSlug: z.string().trim().describe(PROJECT_ROLE.DELETE.projectSlug), + roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId) + }), + response: { + 200: z.object({ + role: SanitizedRoleSchemaV1 + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const role = await server.services.projectRole.deleteRole({ + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + roleId: req.params.roleId + }); + return { role }; + } + }); + + server.route({ + method: "GET", + url: "/:projectSlug/roles", + config: { + rateLimit: readLimit + }, + schema: { + description: "List project role", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectSlug: z.string().trim().describe(PROJECT_ROLE.LIST.projectSlug) + }), + response: { + 200: z.object({ + roles: ProjectRolesSchema.omit({ permissions: true, version: true }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const roles = await server.services.projectRole.listRoles({ + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + filter: { + type: ProjectRoleServiceIdentifierType.SLUG, + projectSlug: req.params.projectSlug + } + }); + return { roles }; + } + }); + + server.route({ + method: "GET", + url: "/:projectSlug/roles/slug/:slug", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectSlug), + slug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug) + }), + response: { + 200: z.object({ + role: SanitizedRoleSchemaV1.omit({ version: true }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const role = await server.services.projectRole.getRoleBySlug({ + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + filter: { + type: ProjectRoleServiceIdentifierType.SLUG, + projectSlug: req.params.projectSlug + }, + roleSlug: req.params.slug + }); + + return { role }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/permissions", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim() + }), + response: { + 200: z.object({ + data: z.object({ + membership: z.object({ + id: z.string(), + roles: z + .object({ + role: z.string() + }) + .array() + }), + assumedPrivilegeDetails: z + .object({ + actorId: z.string(), + actorType: z.string(), + actorName: z.string(), + actorEmail: z.string().optional() + }) + .optional(), + permissions: z.any().array() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { permissions, membership, assumedPrivilegeDetails } = await server.services.projectRole.getUserPermission( + req.permission.id, + req.params.projectId, + req.permission.authMethod, + req.permission.orgId + ); + + return { + data: { + permissions, + membership, + assumedPrivilegeDetails + } + }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/deprecated-project-router.ts b/backend/src/ee/routes/v1/deprecated-project-router.ts new file mode 100644 index 000000000..6c4175f84 --- /dev/null +++ b/backend/src/ee/routes/v1/deprecated-project-router.ts @@ -0,0 +1,195 @@ +import { z } from "zod"; + +import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas"; +import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs"; +import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/:workspaceId/secret-snapshots", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Projects], + description: "Return project secret snapshots ids", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.projectId) + }), + querystring: z.object({ + environment: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.environment), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(PROJECTS.GET_SNAPSHOTS.path), + offset: z.coerce.number().default(0).describe(PROJECTS.GET_SNAPSHOTS.offset), + limit: z.coerce.number().default(20).describe(PROJECTS.GET_SNAPSHOTS.limit) + }), + response: { + 200: z.object({ + secretSnapshots: SecretSnapshotsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretSnapshots = await server.services.snapshot.listSnapshots({ + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + ...req.query + }); + return { secretSnapshots }; + } + }); + + server.route({ + method: "GET", + url: "/:workspaceId/secret-snapshots/count", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + querystring: z.object({ + environment: z.string().trim(), + path: z.string().trim().default("/").transform(removeTrailingSlash) + }), + response: { + 200: z.object({ + count: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const count = await server.services.snapshot.projectSecretSnapshotCount({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + environment: req.query.environment, + path: req.query.path + }); + return { count }; + } + }); + + /* + * Daniel: This endpoint is no longer is use. + * We are keeping it for now because it has been exposed in our public api docs for a while, so by removing it we are likely to break users workflows. + * + * Please refer to the new endpoint, GET /api/v1/organization/audit-logs, for the same (and more) functionality. + */ + server.route({ + method: "GET", + url: "/:workspaceId/audit-logs", + config: { + rateLimit: readLimit + }, + schema: { + description: "Return audit logs", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId) + }), + querystring: z + .object({ + eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType), + userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType), + startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate), + endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate), + offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset), + limit: z.coerce.number().max(1000).default(20).describe(AUDIT_LOGS.EXPORT.limit), + actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor) + }) + .superRefine((el, ctx) => { + if (el.endDate && el.startDate) { + const startDate = new Date(el.startDate); + const endDate = new Date(el.endDate); + const maxAllowedDate = new Date(startDate); + maxAllowedDate.setMonth(maxAllowedDate.getMonth() + 3); + if (endDate < startDate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["endDate"], + message: "End date cannot be before start date" + }); + } + if (endDate > maxAllowedDate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["endDate"], + message: "Dates must be within 3 months" + }); + } + } + }), + response: { + 200: z.object({ + auditLogs: AuditLogsSchema.omit({ + eventMetadata: true, + eventType: true, + actor: true, + actorMetadata: true + }) + .merge( + z.object({ + project: z + .object({ + name: z.string(), + slug: z.string() + }) + .optional(), + event: z.object({ + type: z.string(), + metadata: z.any() + }), + actor: z.object({ + type: z.string(), + metadata: z.any() + }) + }) + ) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const auditLogs = await server.services.auditLog.listAuditLogs({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + + filter: { + ...req.query, + projectId: req.params.workspaceId, + endDate: req.query.endDate || new Date().toISOString(), + startDate: req.query.startDate || getLastMidnightDateISO(), + auditLogActorId: req.query.actor, + eventType: req.query.eventType ? [req.query.eventType] : undefined + } + }); + return { auditLogs }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/deprecated-secret-approval-policy-router.ts b/backend/src/ee/routes/v1/deprecated-secret-approval-policy-router.ts new file mode 100644 index 000000000..42f6f9c12 --- /dev/null +++ b/backend/src/ee/routes/v1/deprecated-secret-approval-policy-router.ts @@ -0,0 +1,293 @@ +import { nanoid } from "nanoid"; +import { z } from "zod"; + +import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { EnforcementLevel } from "@app/lib/types"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { sapPubSchema } from "@app/server/routes/sanitizedSchemas"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerDeprecatedSecretApprovalPolicyRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + config: { + rateLimit: writeLimit + }, + schema: { + body: z + .object({ + workspaceId: z.string(), + name: z.string().optional(), + environment: z.string().optional(), + environments: z.string().array().optional(), + secretPath: z + .string() + .min(1, { message: "Secret path cannot be empty" }) + .transform((val) => removeTrailingSlash(val)), + approvers: z + .discriminatedUnion("type", [ + z.object({ type: z.literal(ApproverType.Group), id: z.string() }), + z.object({ + type: z.literal(ApproverType.User), + id: z.string().optional(), + username: z.string().optional() + }) + ]) + .array() + .min(1, { message: "At least one approver should be provided" }) + .max(100, "Cannot have more than 100 approvers"), + bypassers: z + .discriminatedUnion("type", [ + z.object({ type: z.literal(BypasserType.Group), id: z.string() }), + z.object({ + type: z.literal(BypasserType.User), + id: z.string().optional(), + username: z.string().optional() + }) + ]) + .array() + .max(100, "Cannot have more than 100 bypassers") + .optional(), + approvals: z.number().min(1).default(1), + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), + allowedSelfApprovals: z.boolean().default(true) + }) + .refine((data) => data.environment || data.environments, "At least one environment should be provided"), + response: { + 200: z.object({ + approval: sapPubSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.body.workspaceId, + ...req.body, + name: req.body.name ?? `${req.body.environment || req.body.environments?.join(",")}-${nanoid(3)}`, + enforcementLevel: req.body.enforcementLevel + }); + return { approval }; + } + }); + + server.route({ + url: "/:sapId", + method: "PATCH", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + sapId: z.string() + }), + body: z.object({ + name: z.string().optional(), + approvers: z + .discriminatedUnion("type", [ + z.object({ type: z.literal(ApproverType.Group), id: z.string() }), + z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() }) + ]) + .array() + .min(1, { message: "At least one approver should be provided" }) + .max(100, "Cannot have more than 100 approvers"), + bypassers: z + .discriminatedUnion("type", [ + z.object({ type: z.literal(BypasserType.Group), id: z.string() }), + z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() }) + ]) + .array() + .max(100, "Cannot have more than 100 bypassers") + .optional(), + approvals: z.number().min(1).default(1), + secretPath: z + .string() + .trim() + .min(1, { message: "Secret path cannot be empty" }) + .optional() + .transform((val) => (val ? removeTrailingSlash(val) : undefined)), + enforcementLevel: z.nativeEnum(EnforcementLevel).optional(), + allowedSelfApprovals: z.boolean().default(true), + environments: z.array(z.string()).optional() + }), + response: { + 200: z.object({ + approval: sapPubSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + secretPolicyId: req.params.sapId + }); + return { approval }; + } + }); + + server.route({ + url: "/:sapId", + method: "DELETE", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + sapId: z.string() + }), + response: { + 200: z.object({ + approval: sapPubSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secretPolicyId: req.params.sapId + }); + return { approval }; + } + }); + + server.route({ + url: "/", + method: "GET", + config: { + rateLimit: readLimit + }, + schema: { + querystring: z.object({ + workspaceId: z.string().trim() + }), + response: { + 200: z.object({ + approvals: sapPubSchema + .extend({ + approvers: z + .object({ + id: z.string().nullable().optional(), + type: z.nativeEnum(ApproverType) + }) + .array(), + bypassers: z + .object({ + id: z.string().nullable().optional(), + type: z.nativeEnum(BypasserType) + }) + .array() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const approvals = await server.services.secretApprovalPolicy.getSecretApprovalPolicyByProjectId({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.query.workspaceId + }); + return { approvals }; + } + }); + + server.route({ + url: "/:sapId", + method: "GET", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + sapId: z.string() + }), + response: { + 200: z.object({ + approval: sapPubSchema.extend({ + approvers: z + .object({ + id: z.string().nullable().optional(), + type: z.nativeEnum(ApproverType), + username: z.string().nullable().optional() + }) + .array(), + bypassers: z + .object({ + id: z.string().nullable().optional(), + type: z.nativeEnum(BypasserType), + username: z.string().nullable().optional() + }) + .array() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const approval = await server.services.secretApprovalPolicy.getSecretApprovalPolicyById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params + }); + + return { approval }; + } + }); + + server.route({ + url: "/board", + method: "GET", + config: { + rateLimit: readLimit + }, + schema: { + querystring: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().transform(removeTrailingSlash) + }), + response: { + 200: z.object({ + policy: sapPubSchema + .extend({ + userApprovers: z.object({ userId: z.string().nullable().optional() }).array() + }) + .optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.query.workspaceId, + ...req.query + }); + return { policy }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/dynamic-secret-router.ts b/backend/src/ee/routes/v1/dynamic-secret-router.ts index b916bab67..b1b3cea8e 100644 --- a/backend/src/ee/routes/v1/dynamic-secret-router.ts +++ b/backend/src/ee/routes/v1/dynamic-secret-router.ts @@ -84,7 +84,9 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - dynamicSecret: SanitizedDynamicSecretSchema + dynamicSecret: SanitizedDynamicSecretSchema.extend({ + inputs: z.unknown() + }) }) } }, @@ -151,7 +153,9 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - dynamicSecret: SanitizedDynamicSecretSchema + dynamicSecret: SanitizedDynamicSecretSchema.extend({ + inputs: z.unknown() + }) }) } }, diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index ab9503f58..56d450df3 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -3,8 +3,11 @@ import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-templat import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router"; import { registerAccessApprovalRequestRouter } from "./access-approval-request-router"; import { registerAssumePrivilegeRouter } from "./assume-privilege-router"; -import { registerAuditLogStreamRouter } from "./audit-log-stream-router"; +import { AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP, registerAuditLogStreamRouter } from "./audit-log-stream-routers"; import { registerCaCrlRouter } from "./certificate-authority-crl-router"; +import { registerDeprecatedProjectRoleRouter } from "./deprecated-project-role-router"; +import { registerDeprecatedProjectRouter } from "./deprecated-project-router"; +import { registerDeprecatedSecretApprovalPolicyRouter } from "./deprecated-secret-approval-policy-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; @@ -24,9 +27,9 @@ import { registerPITRouter } from "./pit-router"; import { registerProjectRoleRouter } from "./project-role-router"; import { registerProjectRouter } from "./project-router"; import { registerRateLimitRouter } from "./rate-limit-router"; +import { registerRelayRouter } from "./relay-router"; import { registerSamlRouter } from "./saml-router"; import { registerScimRouter } from "./scim-router"; -import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router"; import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router"; import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router"; import { registerSecretRotationRouter } from "./secret-rotation-router"; @@ -46,18 +49,29 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { // org role starts with organization await server.register(registerOrgRoleRouter, { prefix: "/organization" }); await server.register(registerLicenseRouter, { prefix: "/organizations" }); + + // depreciated in favour of infisical workspace await server.register( async (projectRouter) => { - await projectRouter.register(registerProjectRoleRouter); - await projectRouter.register(registerProjectRouter); - await projectRouter.register(registerTrustedIpRouter); - await projectRouter.register(registerAssumePrivilegeRouter); + await projectRouter.register(registerDeprecatedProjectRoleRouter); + await projectRouter.register(registerDeprecatedProjectRouter); }, { prefix: "/workspace" } ); + + await server.register( + async (projectRouter) => { + await projectRouter.register(registerProjectRoleRouter); + await projectRouter.register(registerTrustedIpRouter); + await projectRouter.register(registerAssumePrivilegeRouter); + await projectRouter.register(registerProjectRouter); + }, + { prefix: "/projects" } + ); + await server.register(registerSnapshotRouter, { prefix: "/secret-snapshot" }); await server.register(registerPITRouter, { prefix: "/pit" }); - await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" }); + await server.register(registerDeprecatedSecretApprovalPolicyRouter, { prefix: "/secret-approvals" }); await server.register(registerSecretApprovalRequestRouter, { prefix: "/secret-approval-requests" }); @@ -79,6 +93,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { ); await server.register(registerGatewayRouter, { prefix: "/gateways" }); + await server.register(registerRelayRouter, { prefix: "/relays" }); await server.register(registerGithubOrgSyncRouter, { prefix: "/github-org-sync-config" }); await server.register( @@ -114,7 +129,21 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register(registerSecretRouter, { prefix: "/secrets" }); await server.register(registerSecretVersionRouter, { prefix: "/secret" }); await server.register(registerGroupRouter, { prefix: "/groups" }); - await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" }); + + await server.register( + async (auditLogStreamRouter) => { + await auditLogStreamRouter.register(registerAuditLogStreamRouter); + + // Provider-specific endpoints + await Promise.all( + Object.entries(AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP).map(([provider, router]) => + auditLogStreamRouter.register(router, { prefix: `/${provider}` }) + ) + ); + }, + { prefix: "/audit-log-streams" } + ); + await server.register(registerUserAdditionalPrivilegeRouter, { prefix: "/user-project-additional-privilege" }); await server.register( async (privilegeRouter) => { diff --git a/backend/src/ee/routes/v1/license-router.ts b/backend/src/ee/routes/v1/license-router.ts index 0a59fa7b5..17923975d 100644 --- a/backend/src/ee/routes/v1/license-router.ts +++ b/backend/src/ee/routes/v1/license-router.ts @@ -43,6 +43,12 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }, schema: { params: z.object({ organizationId: z.string().trim() }), + querystring: z.object({ + refreshCache: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + }), response: { 200: z.object({ plan: z.any() }) } @@ -54,7 +60,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, - orgId: req.params.organizationId + orgId: req.params.organizationId, + refreshCache: req.query.refreshCache }); return { plan }; } diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index 949d4cf7e..23633c8f5 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -2,26 +2,26 @@ import { packRules } from "@casl/ability/extra"; import { z } from "zod"; import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; -import { - backfillPermissionV1SchemaToV2Schema, - ProjectPermissionV1Schema -} from "@app/ee/services/permission/project-permission"; -import { PROJECT_ROLE } from "@app/lib/api-docs"; +import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; +import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; +import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas"; +import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types"; export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/:projectSlug/roles", + url: "/:projectId/roles", config: { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], description: "Create a project role", security: [ { @@ -29,10 +29,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug) + projectId: z.string().trim().describe(PROJECT_ROLE.CREATE.projectId) }), body: z.object({ - slug: slugSchema({ max: 64 }) + slug: slugSchema({ min: 1, max: 64 }) .refine( (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), "Please choose a different slug, the slug you have entered is reserved" @@ -40,11 +40,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { .describe(PROJECT_ROLE.CREATE.slug), name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name), description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description), - permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions) + permissions: ProjectPermissionV2Schema.array() + .describe(PROJECT_ROLE.CREATE.permissions) + .refine(checkForInvalidPermissionCombination) }), response: { 200: z.object({ - role: SanitizedRoleSchemaV1 + role: SanitizedRoleSchema }) } }, @@ -56,26 +58,27 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, actor: req.permission.type, filter: { - type: ProjectRoleServiceIdentifierType.SLUG, - projectSlug: req.params.projectSlug + type: ProjectRoleServiceIdentifierType.ID, + projectId: req.params.projectId }, data: { ...req.body, - permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))) + permissions: JSON.stringify(packRules(req.body.permissions)) } }); - return { role }; } }); server.route({ method: "PATCH", - url: "/:projectSlug/roles/:roleId", + url: "/:projectId/roles/:roleId", config: { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], description: "Update a project role", security: [ { @@ -83,24 +86,27 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - projectSlug: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectSlug), + projectId: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectId), roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId) }), body: z.object({ - slug: slugSchema({ max: 64 }) + slug: slugSchema({ min: 1, max: 64 }) .refine( (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), "Please choose a different slug, the slug you have entered is reserved" ) - .describe(PROJECT_ROLE.UPDATE.slug) - .optional(), + .optional() + .describe(PROJECT_ROLE.UPDATE.slug), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description), - permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() + permissions: ProjectPermissionV2Schema.array() + .describe(PROJECT_ROLE.UPDATE.permissions) + .optional() + .superRefine(checkForInvalidPermissionCombination) }), response: { 200: z.object({ - role: SanitizedRoleSchemaV1 + role: SanitizedRoleSchema }) } }, @@ -114,9 +120,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { roleId: req.params.roleId, data: { ...req.body, - permissions: req.body.permissions - ? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))) - : undefined + permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined } }); return { role }; @@ -125,11 +129,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", - url: "/:projectSlug/roles/:roleId", + url: "/:projectId/roles/:roleId", config: { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], description: "Delete a project role", security: [ { @@ -137,12 +143,12 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - projectSlug: z.string().trim().describe(PROJECT_ROLE.DELETE.projectSlug), + projectId: z.string().trim().describe(PROJECT_ROLE.DELETE.projectId), roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId) }), response: { 200: z.object({ - role: SanitizedRoleSchemaV1 + role: SanitizedRoleSchema }) } }, @@ -161,11 +167,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:projectSlug/roles", + url: "/:projectId/roles", config: { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], description: "List project role", security: [ { @@ -173,7 +181,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - projectSlug: z.string().trim().describe(PROJECT_ROLE.LIST.projectSlug) + projectId: z.string().trim().describe(PROJECT_ROLE.LIST.projectId) }), response: { 200: z.object({ @@ -189,8 +197,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, actor: req.permission.type, filter: { - type: ProjectRoleServiceIdentifierType.SLUG, - projectSlug: req.params.projectSlug + type: ProjectRoleServiceIdentifierType.ID, + projectId: req.params.projectId } }); return { roles }; @@ -199,18 +207,20 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:projectSlug/roles/slug/:slug", + url: "/:projectId/roles/slug/:roleSlug", config: { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], params: z.object({ - projectSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectSlug), - slug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug) + projectId: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectId), + roleSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug) }), response: { 200: z.object({ - role: SanitizedRoleSchemaV1.omit({ version: true }) + role: SanitizedRoleSchema.omit({ version: true }) }) } }, @@ -222,12 +232,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, actor: req.permission.type, filter: { - type: ProjectRoleServiceIdentifierType.SLUG, - projectSlug: req.params.projectSlug + type: ProjectRoleServiceIdentifierType.ID, + projectId: req.params.projectId }, - roleSlug: req.params.slug + roleSlug: req.params.roleSlug }); - return { role }; } }); diff --git a/backend/src/ee/routes/v1/project-router.ts b/backend/src/ee/routes/v1/project-router.ts index 8d8cc4817..4988565f8 100644 --- a/backend/src/ee/routes/v1/project-router.ts +++ b/backend/src/ee/routes/v1/project-router.ts @@ -1,9 +1,9 @@ import { z } from "zod"; -import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas"; -import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; -import { ApiDocsTags, AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs"; -import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn"; +import { SecretSnapshotsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; +import { removeTrailingSlash } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -12,7 +12,7 @@ import { KmsType } from "@app/services/kms/kms-types"; export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:workspaceId/secret-snapshots", + url: "/:projectId/secret-snapshots", config: { rateLimit: readLimit }, @@ -26,7 +26,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.workspaceId) + projectId: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.projectId) }), querystring: z.object({ environment: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.environment), @@ -47,7 +47,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, ...req.query }); return { secretSnapshots }; @@ -56,13 +56,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:workspaceId/secret-snapshots/count", + url: "/:projectId/secret-snapshots/count", config: { rateLimit: readLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), querystring: z.object({ environment: z.string().trim(), @@ -81,7 +81,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, environment: req.query.environment, path: req.query.path }); @@ -89,140 +89,15 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); - /* - * Daniel: This endpoint is no longer is use. - * We are keeping it for now because it has been exposed in our public api docs for a while, so by removing it we are likely to break users workflows. - * - * Please refer to the new endpoint, GET /api/v1/organization/audit-logs, for the same (and more) functionality. - */ server.route({ method: "GET", - url: "/:workspaceId/audit-logs", - config: { - rateLimit: readLimit - }, - schema: { - description: "Return audit logs", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId) - }), - querystring: z - .object({ - eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType), - userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType), - startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate), - endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate), - offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset), - limit: z.coerce.number().max(1000).default(20).describe(AUDIT_LOGS.EXPORT.limit), - actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor) - }) - .superRefine((el, ctx) => { - if (el.endDate && el.startDate) { - const startDate = new Date(el.startDate); - const endDate = new Date(el.endDate); - const maxAllowedDate = new Date(startDate); - maxAllowedDate.setMonth(maxAllowedDate.getMonth() + 3); - if (endDate < startDate) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["endDate"], - message: "End date cannot be before start date" - }); - } - if (endDate > maxAllowedDate) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["endDate"], - message: "Dates must be within 3 months" - }); - } - } - }), - response: { - 200: z.object({ - auditLogs: AuditLogsSchema.omit({ - eventMetadata: true, - eventType: true, - actor: true, - actorMetadata: true - }) - .merge( - z.object({ - project: z - .object({ - name: z.string(), - slug: z.string() - }) - .optional(), - event: z.object({ - type: z.string(), - metadata: z.any() - }), - actor: z.object({ - type: z.string(), - metadata: z.any() - }) - }) - ) - .array() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const auditLogs = await server.services.auditLog.listAuditLogs({ - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - actor: req.permission.type, - - filter: { - ...req.query, - projectId: req.params.workspaceId, - endDate: req.query.endDate || new Date().toISOString(), - startDate: req.query.startDate || getLastMidnightDateISO(), - auditLogActorId: req.query.actor, - eventType: req.query.eventType ? [req.query.eventType] : undefined - } - }); - return { auditLogs }; - } - }); - - server.route({ - method: "GET", - url: "/:workspaceId/audit-logs/filters/actors", + url: "/:projectId/kms", config: { rateLimit: readLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() - }), - response: { - 200: z.object({ - actors: z.string().array() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async () => ({ actors: [] }) - }); - - server.route({ - method: "GET", - url: "/:workspaceId/kms", - config: { - rateLimit: readLimit - }, - schema: { - params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), response: { 200: z.object({ @@ -241,7 +116,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + projectId: req.params.projectId }); return kmsKey; @@ -250,13 +125,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", - url: "/:workspaceId/kms", + url: "/:projectId/kms", config: { rateLimit: writeLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), body: z.object({ kms: z.discriminatedUnion("type", [ @@ -281,13 +156,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, ...req.body }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.params.workspaceId, + projectId: req.params.projectId, event: { type: EventType.UPDATE_PROJECT_KMS, metadata: { @@ -307,13 +182,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:workspaceId/kms/backup", + url: "/:projectId/kms/backup", config: { rateLimit: readLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), response: { 200: z.object({ @@ -328,12 +203,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + projectId: req.params.projectId }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.params.workspaceId, + projectId: req.params.projectId, event: { type: EventType.GET_PROJECT_KMS_BACKUP, metadata: {} @@ -346,13 +221,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/:workspaceId/kms/backup", + url: "/:projectId/kms/backup", config: { rateLimit: writeLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), body: z.object({ backup: z.string().min(1) @@ -374,13 +249,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, backup: req.body.backup }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.params.workspaceId, + projectId: req.params.projectId, event: { type: EventType.LOAD_PROJECT_KMS_BACKUP, metadata: {} @@ -393,13 +268,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/:workspaceId/migrate-v3", + url: "/:projectId/migrate-v3", config: { rateLimit: writeLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), response: { @@ -415,7 +290,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + projectId: req.params.projectId }); return migration; diff --git a/backend/src/ee/routes/v1/relay-router.ts b/backend/src/ee/routes/v1/relay-router.ts new file mode 100644 index 000000000..e20480088 --- /dev/null +++ b/backend/src/ee/routes/v1/relay-router.ts @@ -0,0 +1,103 @@ +import { z } from "zod"; + +import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto/cryptography"; +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerRelayRouter = async (server: FastifyZodProvider) => { + const appCfg = getConfig(); + + server.route({ + method: "POST", + url: "/register-instance-relay", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + host: z.string(), + name: slugSchema({ min: 1, max: 32, field: "name" }) + }), + response: { + 200: z.object({ + pki: z.object({ + serverCertificate: z.string(), + serverPrivateKey: z.string(), + clientCertificateChain: z.string() + }), + ssh: z.object({ + serverCertificate: z.string(), + serverPrivateKey: z.string(), + clientCAPublicKey: z.string() + }) + }) + } + }, + onRequest: (req, _, next) => { + const authHeader = req.headers.authorization; + + if (appCfg.RELAY_AUTH_SECRET && authHeader) { + const expectedHeader = `Bearer ${appCfg.RELAY_AUTH_SECRET}`; + if ( + authHeader.length === expectedHeader.length && + crypto.nativeCrypto.timingSafeEqual(Buffer.from(authHeader), Buffer.from(expectedHeader)) + ) { + return next(); + } + } + + throw new UnauthorizedError({ + message: "Invalid relay auth secret" + }); + }, + handler: async (req) => { + return server.services.relay.registerRelay({ + ...req.body + }); + } + }); + + server.route({ + method: "POST", + url: "/register-org-relay", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + host: z.string(), + name: slugSchema({ min: 1, max: 32, field: "name" }) + }), + response: { + 200: z.object({ + pki: z.object({ + serverCertificate: z.string(), + serverPrivateKey: z.string(), + clientCertificateChain: z.string() + }), + ssh: z.object({ + serverCertificate: z.string(), + serverPrivateKey: z.string(), + clientCAPublicKey: z.string() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + throw new BadRequestError({ + message: "Org relay registration is not yet supported" + }); + + return server.services.relay.registerRelay({ + ...req.body, + identityId: req.permission.id, + orgId: req.permission.orgId + }); + } + }); +}; diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index bdc9c2dcd..6a9b59f00 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -27,7 +27,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv }, schema: { querystring: z.object({ - workspaceId: z.string().trim(), + projectId: z.string().trim(), environment: z.string().trim().optional(), committer: z.string().trim().optional(), search: z.string().trim().optional(), @@ -80,7 +80,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.query, - projectId: req.query.workspaceId + projectId: req.query.projectId }); return { approvals, totalCount }; } @@ -94,7 +94,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv }, schema: { querystring: z.object({ - workspaceId: z.string().trim(), + projectId: z.string().trim(), policyId: z.string().trim().optional() }), response: { @@ -113,7 +113,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.query.workspaceId, + projectId: req.query.projectId, policyId: req.query.policyId }); return { approvals }; diff --git a/backend/src/ee/routes/v1/trusted-ip-router.ts b/backend/src/ee/routes/v1/trusted-ip-router.ts index b6fc3cc90..5416e8613 100644 --- a/backend/src/ee/routes/v1/trusted-ip-router.ts +++ b/backend/src/ee/routes/v1/trusted-ip-router.ts @@ -9,13 +9,13 @@ import { AuthMode } from "@app/services/auth/auth-type"; export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:workspaceId/trusted-ips", + url: "/:projectId/trusted-ips", config: { rateLimit: readLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), response: { 200: z.object({ @@ -27,7 +27,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const trustedIps = await server.services.trustedIp.listIpsByProjectId({ actorAuthMethod: req.permission.authMethod, - projectId: req.params.workspaceId, + projectId: req.params.projectId, actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId @@ -38,13 +38,13 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/:workspaceId/trusted-ips", + url: "/:projectId/trusted-ips", config: { rateLimit: writeLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), body: z.object({ ipAddress: z.string().trim(), @@ -61,7 +61,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const { trustedIp, project } = await server.services.trustedIp.addProjectIp({ actorAuthMethod: req.permission.authMethod, - projectId: req.params.workspaceId, + projectId: req.params.projectId, actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, @@ -86,13 +86,13 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", - url: "/:workspaceId/trusted-ips/:trustedIpId", + url: "/:projectId/trusted-ips/:trustedIpId", config: { rateLimit: writeLimit }, schema: { params: z.object({ - workspaceId: z.string().trim(), + projectId: z.string().trim(), trustedIpId: z.string().trim() }), body: z.object({ @@ -108,7 +108,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const { trustedIp, project } = await server.services.trustedIp.updateProjectIp({ - projectId: req.params.workspaceId, + projectId: req.params.projectId, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -135,13 +135,13 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", - url: "/:workspaceId/trusted-ips/:trustedIpId", + url: "/:projectId/trusted-ips/:trustedIpId", config: { rateLimit: writeLimit }, schema: { params: z.object({ - workspaceId: z.string().trim(), + projectId: z.string().trim(), trustedIpId: z.string().trim() }), response: { @@ -153,7 +153,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const { trustedIp, project } = await server.services.trustedIp.deleteProjectIp({ - projectId: req.params.workspaceId, + projectId: req.params.projectId, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, diff --git a/backend/src/ee/routes/v2/project-role-router.ts b/backend/src/ee/routes/v2/deprecated-project-role-router.ts similarity index 98% rename from backend/src/ee/routes/v2/project-role-router.ts rename to backend/src/ee/routes/v2/deprecated-project-role-router.ts index 538929316..41a04974d 100644 --- a/backend/src/ee/routes/v2/project-role-router.ts +++ b/backend/src/ee/routes/v2/deprecated-project-role-router.ts @@ -12,7 +12,7 @@ import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types"; -export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { +export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/:projectId/roles", diff --git a/backend/src/ee/routes/v2/gateway-router.ts b/backend/src/ee/routes/v2/gateway-router.ts new file mode 100644 index 000000000..a7e656a64 --- /dev/null +++ b/backend/src/ee/routes/v2/gateway-router.ts @@ -0,0 +1,133 @@ +import z from "zod"; + +import { GatewaysV2Schema } from "@app/db/schemas"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const SanitizedGatewayV2Schema = GatewaysV2Schema.pick({ + id: true, + identityId: true, + name: true, + createdAt: true, + updatedAt: true, + heartbeat: true +}); + +export const registerGatewayV2Router = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + schema: { + body: z.object({ + relayName: slugSchema({ min: 1, max: 32, field: "relayName" }), + name: slugSchema({ min: 1, max: 32, field: "name" }) + }), + response: { + 200: z.object({ + gatewayId: z.string(), + relayHost: z.string(), + pki: z.object({ + serverCertificate: z.string(), + serverPrivateKey: z.string(), + clientCertificateChain: z.string() + }), + ssh: z.object({ + clientCertificate: z.string(), + clientPrivateKey: z.string(), + serverCAPublicKey: z.string() + }) + }) + } + }, + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const gateway = await server.services.gatewayV2.registerGateway({ + orgId: req.permission.orgId, + relayName: req.body.relayName, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + name: req.body.name + }); + + return gateway; + } + }); + + server.route({ + method: "POST", + url: "/heartbeat", + config: { + rateLimit: writeLimit + }, + schema: { + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + await server.services.gatewayV2.heartbeat({ + orgPermission: req.permission + }); + + return { message: "Successfully triggered heartbeat" }; + } + }); + + server.route({ + method: "GET", + url: "/", + schema: { + response: { + 200: SanitizedGatewayV2Schema.extend({ + identity: z.object({ + name: z.string(), + id: z.string() + }) + }).array() + } + }, + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const gateways = await server.services.gatewayV2.listGateways({ + orgPermission: req.permission + }); + + return gateways; + } + }); + + server.route({ + method: "DELETE", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: SanitizedGatewayV2Schema + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), + handler: async (req) => { + const gateway = await server.services.gatewayV2.deleteGatewayById({ + orgPermission: req.permission, + id: req.params.id + }); + return gateway; + } + }); +}; diff --git a/backend/src/ee/routes/v2/index.ts b/backend/src/ee/routes/v2/index.ts index e364f4949..c402ab00a 100644 --- a/backend/src/ee/routes/v2/index.ts +++ b/backend/src/ee/routes/v2/index.ts @@ -7,14 +7,16 @@ import { SECRET_SCANNING_REGISTER_ROUTER_MAP } from "@app/ee/routes/v2/secret-scanning-v2-routers"; +import { registerDeprecatedProjectRoleRouter } from "./deprecated-project-role-router"; +import { registerGatewayV2Router } from "./gateway-router"; import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; -import { registerProjectRoleRouter } from "./project-role-router"; +import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router"; export const registerV2EERoutes = async (server: FastifyZodProvider) => { - // org role starts with organization await server.register( async (projectRouter) => { - await projectRouter.register(registerProjectRoleRouter); + // this has been depreciated and moved to /api/v1/projects + await projectRouter.register(registerDeprecatedProjectRoleRouter); }, { prefix: "/workspace" } ); @@ -23,6 +25,10 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => { prefix: "/identity-project-additional-privilege" }); + await server.register(registerGatewayV2Router, { prefix: "/gateways" }); + + await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" }); + await server.register( async (secretRotationV2Router) => { // register generic secret rotation endpoints diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v2/secret-approval-policy-router.ts similarity index 97% rename from backend/src/ee/routes/v1/secret-approval-policy-router.ts rename to backend/src/ee/routes/v2/secret-approval-policy-router.ts index dc87b83f2..f7f770197 100644 --- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts +++ b/backend/src/ee/routes/v2/secret-approval-policy-router.ts @@ -19,7 +19,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi schema: { body: z .object({ - workspaceId: z.string(), + projectId: z.string(), name: z.string().optional(), environment: z.string().optional(), environments: z.string().array().optional(), @@ -69,7 +69,6 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.body.workspaceId, ...req.body, name: req.body.name ?? `${req.body.environment || req.body.environments?.join(",")}-${nanoid(3)}`, enforcementLevel: req.body.enforcementLevel @@ -174,7 +173,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi }, schema: { querystring: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), response: { 200: z.object({ @@ -204,7 +203,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.query.workspaceId + projectId: req.query.projectId }); return { approvals }; } @@ -263,7 +262,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi }, schema: { querystring: z.object({ - workspaceId: z.string().trim(), + projectId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().transform(removeTrailingSlash) }), @@ -284,7 +283,6 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.query.workspaceId, ...req.query }); return { policy }; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 0f05bd5af..008b61919 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -20,6 +20,8 @@ import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; +import { TNotificationServiceFactory } from "../../../services/notification/notification-service"; +import { NotificationType } from "../../../services/notification/notification-types"; import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { TGroupDALFactory } from "../group/group-dal"; @@ -67,6 +69,7 @@ type TSecretApprovalRequestServiceFactoryDep = { projectSlackConfigDAL: Pick; microsoftTeamsService: Pick; projectMicrosoftTeamsConfigDAL: Pick; + notificationService: Pick; }; export const accessApprovalRequestServiceFactory = ({ @@ -84,7 +87,8 @@ export const accessApprovalRequestServiceFactory = ({ kmsService, microsoftTeamsService, projectMicrosoftTeamsConfigDAL, - projectSlackConfigDAL + projectSlackConfigDAL, + notificationService }: TSecretApprovalRequestServiceFactoryDep): TAccessApprovalRequestServiceFactory => { const $getEnvironmentFromPermissions = (permissions: unknown): string | null => { if (!Array.isArray(permissions) || permissions.length === 0) { @@ -245,7 +249,8 @@ export const accessApprovalRequestServiceFactory = ({ ); const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; - const approvalUrl = `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`; + const approvalPath = `/projects/secret-management/${project.id}/approval`; + const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; await triggerWorkflowIntegrationNotification({ input: { @@ -274,6 +279,17 @@ export const accessApprovalRequestServiceFactory = ({ } }); + await notificationService.createUserNotifications( + approverUsers.map((approver) => ({ + userId: approver.id, + orgId: actorOrgId, + type: NotificationType.ACCESS_APPROVAL_REQUEST, + title: "Access Approval Request", + body: `**${requesterFullName}** (${requestedByUser.email}) has requested ${isTemporary ? "temporary" : "permanent"} access to **${secretPath}** in the **${envSlug}** environment for project **${project.name}**.`, + link: approvalPath + })) + ); + await smtpService.sendMail({ recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!), subjectLine: "Access Approval Request", @@ -391,7 +407,8 @@ export const accessApprovalRequestServiceFactory = ({ const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; const editorFullName = `${editedByUser.firstName} ${editedByUser.lastName}`; - const approvalUrl = `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`; + const approvalPath = `/projects/secret-management/${project.id}/approval`; + const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; await triggerWorkflowIntegrationNotification({ input: { @@ -422,27 +439,44 @@ export const accessApprovalRequestServiceFactory = ({ } }); - await smtpService.sendMail({ - recipients: policy.approvers - .filter((approver) => Boolean(approver.email) && approver.userId !== editedByUser.id) - .map((approver) => approver.email!), - subjectLine: "Access Approval Request Updated", - substitutions: { - projectName: project.name, - requesterFullName, - requesterEmail: requestedByUser.email, - isTemporary: true, - expiresIn: msFn(ms(temporaryRange || ""), { long: true }), - secretPath, - environment: envSlug, - permissions: accessTypes, - approvalUrl, - editNote, - editorFullName, - editorEmail: editedByUser.email - }, - template: SmtpTemplates.AccessApprovalRequestUpdated - }); + await notificationService.createUserNotifications( + policy.approvers + .filter((approver) => Boolean(approver.userId) && approver.userId !== editedByUser.id) + .map((approver) => ({ + userId: approver.userId!, + orgId: actorOrgId, + type: NotificationType.ACCESS_APPROVAL_REQUEST_UPDATED, + title: "Access Approval Request Updated", + body: `**${editorFullName}** (${editedByUser.email}) has updated the access request submitted by **${requesterFullName}** (${requestedByUser.email}) for **${secretPath}** in the **${envSlug}** environment for project **${project.name}**.`, + link: approvalPath + })) + ); + + const recipients = policy.approvers + .filter((approver) => Boolean(approver.email) && approver.userId !== editedByUser.id) + .map((approver) => approver.email!); + + if (recipients.length > 0) { + await smtpService.sendMail({ + recipients, + subjectLine: "Access Approval Request Updated", + substitutions: { + projectName: project.name, + requesterFullName, + requesterEmail: requestedByUser.email, + isTemporary: true, + expiresIn: msFn(ms(temporaryRange || ""), { long: true }), + secretPath, + environment: envSlug, + permissions: accessTypes, + approvalUrl, + editNote, + editorFullName, + editorEmail: editedByUser.email + }, + template: SmtpTemplates.AccessApprovalRequestUpdated + }); + } return approvalRequest; }); diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-enums.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-enums.ts new file mode 100644 index 000000000..ebef18574 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-enums.ts @@ -0,0 +1,7 @@ +export enum LogProvider { + Azure = "azure", + Cribl = "cribl", + Custom = "custom", + Datadog = "datadog", + Splunk = "splunk" +} diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-factory.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-factory.ts new file mode 100644 index 000000000..8dde0e079 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-factory.ts @@ -0,0 +1,17 @@ +import { LogProvider } from "./audit-log-stream-enums"; +import { TAuditLogStreamCredentials, TLogStreamFactory } from "./audit-log-stream-types"; +import { AzureProviderFactory } from "./azure/azure-provider-factory"; +import { CriblProviderFactory } from "./cribl/cribl-provider-factory"; +import { CustomProviderFactory } from "./custom/custom-provider-factory"; +import { DatadogProviderFactory } from "./datadog/datadog-provider-factory"; +import { SplunkProviderFactory } from "./splunk/splunk-provider-factory"; + +type TLogStreamFactoryImplementation = TLogStreamFactory; + +export const LOG_STREAM_FACTORY_MAP: Record = { + [LogProvider.Azure]: AzureProviderFactory as TLogStreamFactoryImplementation, + [LogProvider.Datadog]: DatadogProviderFactory as TLogStreamFactoryImplementation, + [LogProvider.Splunk]: SplunkProviderFactory as TLogStreamFactoryImplementation, + [LogProvider.Custom]: CustomProviderFactory as TLogStreamFactoryImplementation, + [LogProvider.Cribl]: CriblProviderFactory as TLogStreamFactoryImplementation +}; diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-fns.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-fns.ts index dc93f238e..07d833030 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-fns.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-fns.ts @@ -1,21 +1,76 @@ -export function providerSpecificPayload(url: string) { - const { hostname } = new URL(url); +import { TAuditLogStreams } from "@app/db/schemas"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; - const payload: Record = {}; +import { TAuditLogStream, TAuditLogStreamCredentials } from "./audit-log-stream-types"; +import { getAzureProviderListItem } from "./azure/azure-provider-fns"; +import { getCriblProviderListItem } from "./cribl/cribl-provider-fns"; +import { getCustomProviderListItem } from "./custom/custom-provider-fns"; +import { getDatadogProviderListItem } from "./datadog/datadog-provider-fns"; +import { getSplunkProviderListItem } from "./splunk/splunk-provider-fns"; - switch (hostname) { - case "http-intake.logs.datadoghq.com": - case "http-intake.logs.us3.datadoghq.com": - case "http-intake.logs.us5.datadoghq.com": - case "http-intake.logs.datadoghq.eu": - case "http-intake.logs.ap1.datadoghq.com": - case "http-intake.logs.ddog-gov.com": - payload.ddsource = "infisical"; - payload.service = "audit-logs"; - break; - default: - break; - } +export const listProviderOptions = () => { + return [ + getDatadogProviderListItem(), + getSplunkProviderListItem(), + getCustomProviderListItem(), + getAzureProviderListItem(), + getCriblProviderListItem() + ].sort((a, b) => a.name.localeCompare(b.name)); +}; - return payload; -} +export const encryptLogStreamCredentials = async ({ + orgId, + credentials, + kmsService +}: { + orgId: string; + credentials: TAuditLogStreamCredentials; + kmsService: Pick; +}) => { + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({ + plainText: Buffer.from(JSON.stringify(credentials)) + }); + + return encryptedCredentialsBlob; +}; + +export const decryptLogStreamCredentials = async ({ + orgId, + encryptedCredentials, + kmsService +}: { + orgId: string; + encryptedCredentials: Buffer; + kmsService: Pick; +}) => { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const decryptedPlainTextBlob = decryptor({ + cipherTextBlob: encryptedCredentials + }); + + return JSON.parse(decryptedPlainTextBlob.toString()) as TAuditLogStreamCredentials; +}; + +export const decryptLogStream = async ( + logStream: TAuditLogStreams, + kmsService: Pick +) => { + return { + ...logStream, + credentials: await decryptLogStreamCredentials({ + encryptedCredentials: logStream.encryptedCredentials, + orgId: logStream.orgId, + kmsService + }) + } as TAuditLogStream; +}; diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-schemas.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-schemas.ts new file mode 100644 index 000000000..4fba79107 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-schemas.ts @@ -0,0 +1,14 @@ +import { AuditLogStreamsSchema } from "@app/db/schemas"; + +export const BaseProviderSchema = AuditLogStreamsSchema.omit({ + encryptedCredentials: true, + provider: true, + + // Old "archived" values + encryptedHeadersAlgorithm: true, + encryptedHeadersCiphertext: true, + encryptedHeadersIV: true, + encryptedHeadersKeyEncoding: true, + encryptedHeadersTag: true, + url: true +}); diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts index 46d2782b3..5dd0fd4ba 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts @@ -1,242 +1,252 @@ import { ForbiddenError } from "@casl/ability"; -import { RawAxiosRequestHeaders } from "axios"; +import { AxiosError } from "axios"; -import { SecretKeyEncoding } from "@app/db/schemas"; -import { getConfig } from "@app/lib/config/env"; -import { request } from "@app/lib/config/request"; -import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; -import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { TAuditLogs } from "@app/db/schemas"; +import { + decryptLogStream, + decryptLogStreamCredentials, + encryptLogStreamCredentials, + listProviderOptions +} from "@app/ee/services/audit-log-stream/audit-log-stream-fns"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal"; -import { providerSpecificPayload } from "./audit-log-stream-fns"; -import { LogStreamHeaders, TAuditLogStreamServiceFactory } from "./audit-log-stream-types"; +import { LogProvider } from "./audit-log-stream-enums"; +import { LOG_STREAM_FACTORY_MAP } from "./audit-log-stream-factory"; +import { TAuditLogStream, TCreateAuditLogStreamDTO, TUpdateAuditLogStreamDTO } from "./audit-log-stream-types"; +import { TCustomProviderCredentials } from "./custom/custom-provider-types"; -type TAuditLogStreamServiceFactoryDep = { +export type TAuditLogStreamServiceFactoryDep = { auditLogStreamDAL: TAuditLogStreamDALFactory; permissionService: Pick; licenseService: Pick; + kmsService: Pick; }; +export type TAuditLogStreamServiceFactory = ReturnType; + export const auditLogStreamServiceFactory = ({ auditLogStreamDAL, permissionService, - licenseService -}: TAuditLogStreamServiceFactoryDep): TAuditLogStreamServiceFactory => { - const create: TAuditLogStreamServiceFactory["create"] = async ({ - url, - actor, - headers = [], - actorId, - actorOrgId, - actorAuthMethod - }) => { - if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); - - const plan = await licenseService.getPlan(actorOrgId); + licenseService, + kmsService +}: TAuditLogStreamServiceFactoryDep) => { + const create = async ({ provider, credentials }: TCreateAuditLogStreamDTO, actor: OrgServiceActor) => { + const plan = await licenseService.getPlan(actor.orgId); if (!plan.auditLogStreams) { throw new BadRequestError({ - message: "Failed to create audit log streams due to plan restriction. Upgrade plan to create group." + message: "Failed to create Audit Log Stream: Plan restriction. Upgrade plan to continue." }); } const { permission } = await permissionService.getOrgPermission( - actor, - actorId, - actorOrgId, - actorAuthMethod, - actorOrgId + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + actor.orgId ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); - const appCfg = getConfig(); - if (appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url); - - const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId }); + const totalStreams = await auditLogStreamDAL.find({ orgId: actor.orgId }); if (totalStreams.length >= plan.auditLogStreamLimit) { throw new BadRequestError({ - message: - "Failed to create audit log streams due to plan limit reached. Kindly contact Infisical to add more streams." + message: "Failed to create Audit Log Stream: Plan limit reached. Contact Infisical to increase quota." }); } - // testing connection first - const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; - if (headers.length) - headers.forEach(({ key, value }) => { - streamHeaders[key] = value; - }); + const factory = LOG_STREAM_FACTORY_MAP[provider](); + const validatedCredentials = await factory.validateCredentials({ credentials }); - await request - .post( - url, - { ...providerSpecificPayload(url), ping: "ok" }, - { - headers: streamHeaders, - // request timeout - timeout: AUDIT_LOG_STREAM_TIMEOUT, - // connection timeout - signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) - } - ) - .catch((err) => { - throw new BadRequestError({ message: `Failed to connect with upstream source: ${(err as Error)?.message}` }); - }); + const encryptedCredentials = await encryptLogStreamCredentials({ + credentials: validatedCredentials, + orgId: actor.orgId, + kmsService + }); - const encryptedHeaders = headers - ? crypto.encryption().symmetric().encryptWithRootEncryptionKey(JSON.stringify(headers)) - : undefined; const logStream = await auditLogStreamDAL.create({ - orgId: actorOrgId, - url, - ...(encryptedHeaders - ? { - encryptedHeadersCiphertext: encryptedHeaders.ciphertext, - encryptedHeadersIV: encryptedHeaders.iv, - encryptedHeadersTag: encryptedHeaders.tag, - encryptedHeadersAlgorithm: encryptedHeaders.algorithm, - encryptedHeadersKeyEncoding: encryptedHeaders.encoding - } - : {}) + orgId: actor.orgId, + provider, + encryptedCredentials }); - return logStream; + + return { ...logStream, credentials: validatedCredentials } as TAuditLogStream; }; - const updateById: TAuditLogStreamServiceFactory["updateById"] = async ({ - id, - url, - actor, - headers = [], - actorId, - actorOrgId, - actorAuthMethod - }) => { - if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); - - const plan = await licenseService.getPlan(actorOrgId); - if (!plan.auditLogStreams) + const updateById = async ( + { logStreamId, provider, credentials }: TUpdateAuditLogStreamDTO, + actor: OrgServiceActor + ) => { + const plan = await licenseService.getPlan(actor.orgId); + if (!plan.auditLogStreams) { throw new BadRequestError({ - message: "Failed to update audit log streams due to plan restriction. Upgrade plan to create group." + message: "Failed to update Audit Log Stream: Plan restriction. Upgrade plan to continue." }); + } - const logStream = await auditLogStreamDAL.findById(id); - if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` }); + const logStream = await auditLogStreamDAL.findById(logStreamId); + if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` }); + + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + logStream.orgId + ); - const { orgId } = logStream; - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); - const appCfg = getConfig(); - if (url && appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url); - // testing connection first - const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; - if (headers.length) - headers.forEach(({ key, value }) => { - streamHeaders[key] = value; - }); + const finalCredentials = { ...credentials }; - await request - .post( - url || logStream.url, - { ...providerSpecificPayload(url || logStream.url), ping: "ok" }, - { - headers: streamHeaders, - // request timeout - timeout: AUDIT_LOG_STREAM_TIMEOUT, - // connection timeout - signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) - } - ) - .catch((err) => { - throw new Error(`Failed to connect with the source ${(err as Error)?.message}`); - }); + // For the "Custom" provider, we must handle masked header values ('******'). + // These are placeholders from the frontend for secrets that haven't been changed. + // We need to replace them with the original, unmasked values from the database. + if ( + provider === LogProvider.Custom && + "headers" in finalCredentials && + Array.isArray(finalCredentials.headers) && + finalCredentials.headers.some((header) => header.value === "******") + ) { + const decryptedOldCredentials = (await decryptLogStreamCredentials({ + encryptedCredentials: logStream.encryptedCredentials, + orgId: logStream.orgId, + kmsService + })) as TCustomProviderCredentials; - const encryptedHeaders = headers - ? crypto.encryption().symmetric().encryptWithRootEncryptionKey(JSON.stringify(headers)) - : undefined; - const updatedLogStream = await auditLogStreamDAL.updateById(id, { - url, - ...(encryptedHeaders - ? { - encryptedHeadersCiphertext: encryptedHeaders.ciphertext, - encryptedHeadersIV: encryptedHeaders.iv, - encryptedHeadersTag: encryptedHeaders.tag, - encryptedHeadersAlgorithm: encryptedHeaders.algorithm, - encryptedHeadersKeyEncoding: encryptedHeaders.encoding + const oldHeadersMap = decryptedOldCredentials.headers.reduce>((acc, header) => { + acc[header.key] = header.value; + return acc; + }, {}); + + const finalHeaders: { key: string; value: string }[] = []; + for (const header of finalCredentials.headers) { + if (header.value === "******") { + const oldValue = oldHeadersMap[header.key]; + if (oldValue) { + finalHeaders.push({ key: header.key, value: oldValue }); } - : {}) + } else { + finalHeaders.push(header); + } + } + finalCredentials.headers = finalHeaders; + } + + const factory = LOG_STREAM_FACTORY_MAP[provider](); + const validatedCredentials = await factory.validateCredentials({ credentials: finalCredentials }); + + const encryptedCredentials = await encryptLogStreamCredentials({ + credentials: validatedCredentials, + orgId: actor.orgId, + kmsService }); - return updatedLogStream; + + const updatedLogStream = await auditLogStreamDAL.updateById(logStreamId, { + encryptedCredentials + }); + + return { ...updatedLogStream, credentials: validatedCredentials } as TAuditLogStream; }; - const deleteById: TAuditLogStreamServiceFactory["deleteById"] = async ({ - id, - actor, - actorId, - actorOrgId, - actorAuthMethod - }) => { - if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); + const deleteById = async (logStreamId: string, provider: LogProvider, actor: OrgServiceActor) => { + const logStream = await auditLogStreamDAL.findById(logStreamId); + if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` }); - const logStream = await auditLogStreamDAL.findById(id); - if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` }); + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + logStream.orgId + ); - const { orgId } = logStream; - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); - const deletedLogStream = await auditLogStreamDAL.deleteById(id); - return deletedLogStream; + if (logStream.provider !== provider) { + throw new BadRequestError({ + message: `Audit Log Stream with ID '${logStreamId}' is not for provider '${provider}'` + }); + } + + const deletedLogStream = await auditLogStreamDAL.deleteById(logStreamId); + + return decryptLogStream(deletedLogStream, kmsService); }; - const getById: TAuditLogStreamServiceFactory["getById"] = async ({ - id, - actor, - actorId, - actorOrgId, - actorAuthMethod - }) => { - const logStream = await auditLogStreamDAL.findById(id); - if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` }); + const getById = async (logStreamId: string, provider: LogProvider, actor: OrgServiceActor) => { + const logStream = await auditLogStreamDAL.findById(logStreamId); - const { orgId } = logStream; - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); + if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${logStreamId}' not found` }); - const headers = - logStream?.encryptedHeadersCiphertext && logStream?.encryptedHeadersIV && logStream?.encryptedHeadersTag - ? (JSON.parse( - crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - tag: logStream.encryptedHeadersTag, - iv: logStream.encryptedHeadersIV, - ciphertext: logStream.encryptedHeadersCiphertext, - keyEncoding: logStream.encryptedHeadersKeyEncoding as SecretKeyEncoding - }) - ) as LogStreamHeaders[]) - : undefined; - - return { ...logStream, headers }; - }; - - const list: TAuditLogStreamServiceFactory["list"] = async ({ actor, actorId, actorOrgId, actorAuthMethod }) => { const { permission } = await permissionService.getOrgPermission( - actor, - actorId, - actorOrgId, - actorAuthMethod, - actorOrgId + actor.type, + actor.id, + logStream.orgId, + actor.authMethod, + actor.orgId ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); - const logStreams = await auditLogStreamDAL.find({ orgId: actorOrgId }); - return logStreams; + if (logStream.provider !== provider) { + throw new BadRequestError({ + message: `Audit Log Stream with ID '${logStreamId}' is not for provider '${provider}'` + }); + } + + return decryptLogStream(logStream, kmsService); + }; + + const list = async (actor: OrgServiceActor) => { + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); + + const logStreams = await auditLogStreamDAL.find({ orgId: actor.orgId }); + + return Promise.all(logStreams.map((stream) => decryptLogStream(stream, kmsService))); + }; + + const streamLog = async (orgId: string, auditLog: TAuditLogs) => { + const logStreams = await auditLogStreamDAL.find({ orgId }); + await Promise.allSettled( + logStreams.map(async ({ provider, encryptedCredentials }) => { + const credentials = await decryptLogStreamCredentials({ + encryptedCredentials, + orgId, + kmsService + }); + + const factory = LOG_STREAM_FACTORY_MAP[provider as LogProvider](); + + try { + await factory.streamLog({ + credentials, + auditLog + }); + } catch (error) { + logger.error( + error, + `Failed to stream audit log [auditLogId=${auditLog.id}] [provider=${provider}] [orgId=${orgId}]${error instanceof AxiosError ? `: ${error.message}` : ""}` + ); + throw error; + } + }) + ); }; return { @@ -244,6 +254,8 @@ export const auditLogStreamServiceFactory = ({ updateById, deleteById, getById, - list + list, + listProviderOptions, + streamLog }; }; diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts index 4c4a5609e..5983e50bf 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts @@ -1,48 +1,42 @@ -import { TAuditLogStreams } from "@app/db/schemas"; -import { TOrgPermission } from "@app/lib/types"; +import { TAuditLogs } from "@app/db/schemas"; -export type LogStreamHeaders = { - key: string; - value: string; +import { LogProvider } from "./audit-log-stream-enums"; +import { TAzureProvider, TAzureProviderCredentials } from "./azure/azure-provider-types"; +import { TCriblProvider, TCriblProviderCredentials } from "./cribl/cribl-provider-types"; +import { TCustomProvider, TCustomProviderCredentials } from "./custom/custom-provider-types"; +import { TDatadogProvider, TDatadogProviderCredentials } from "./datadog/datadog-provider-types"; +import { TSplunkProvider, TSplunkProviderCredentials } from "./splunk/splunk-provider-types"; + +export type TAuditLogStream = TDatadogProvider | TSplunkProvider | TCustomProvider | TAzureProvider | TCriblProvider; + +export type TAuditLogStreamCredentials = + | TDatadogProviderCredentials + | TSplunkProviderCredentials + | TCustomProviderCredentials + | TAzureProviderCredentials + | TCriblProviderCredentials; + +export type TCreateAuditLogStreamDTO = { + provider: LogProvider; + credentials: TAuditLogStreamCredentials; }; -export type TCreateAuditLogStreamDTO = Omit & { - url: string; - headers?: LogStreamHeaders[]; +export type TUpdateAuditLogStreamDTO = { + logStreamId: string; + provider: LogProvider; + credentials: TAuditLogStreamCredentials; }; -export type TUpdateAuditLogStreamDTO = Omit & { - id: string; - url?: string; - headers?: LogStreamHeaders[]; -}; +export type TLogStreamFactoryValidateCredentials = (input: { + credentials: C; +}) => Promise; -export type TDeleteAuditLogStreamDTO = Omit & { - id: string; -}; +export type TLogStreamFactoryStreamLog = (input: { + credentials: C; + auditLog: TAuditLogs; +}) => Promise; -export type TListAuditLogStreamDTO = Omit; - -export type TGetDetailsAuditLogStreamDTO = Omit & { - id: string; -}; - -export type TAuditLogStreamServiceFactory = { - create: (arg: TCreateAuditLogStreamDTO) => Promise; - updateById: (arg: TUpdateAuditLogStreamDTO) => Promise; - deleteById: (arg: TDeleteAuditLogStreamDTO) => Promise; - getById: (arg: TGetDetailsAuditLogStreamDTO) => Promise<{ - headers: LogStreamHeaders[] | undefined; - orgId: string; - url: string; - id: string; - createdAt: Date; - updatedAt: Date; - encryptedHeadersCiphertext?: string | null | undefined; - encryptedHeadersIV?: string | null | undefined; - encryptedHeadersTag?: string | null | undefined; - encryptedHeadersAlgorithm?: string | null | undefined; - encryptedHeadersKeyEncoding?: string | null | undefined; - }>; - list: (arg: TListAuditLogStreamDTO) => Promise; +export type TLogStreamFactory = () => { + validateCredentials: TLogStreamFactoryValidateCredentials; + streamLog: TLogStreamFactoryStreamLog; }; diff --git a/backend/src/ee/services/audit-log-stream/azure/azure-provider-factory.ts b/backend/src/ee/services/audit-log-stream/azure/azure-provider-factory.ts new file mode 100644 index 000000000..9a6157666 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/azure/azure-provider-factory.ts @@ -0,0 +1,98 @@ +import { RawAxiosRequestHeaders } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; + +import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue"; +import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types"; +import { TAzureProviderCredentials } from "./azure-provider-types"; + +function createPayload(event: { createdAt?: Date | string } & Record) { + return [ + { + ...event, + TimeGenerated: (event.createdAt ? new Date(event.createdAt) : new Date()).toISOString() + } + ]; +} + +async function getAzureToken(tenantId: string, clientId: string, clientSecret: string) { + const { data } = await request.post<{ access_token: string }>( + `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, + new URLSearchParams({ + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret, + scope: "https://monitor.azure.com/.default" + }), + { + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + } + ); + + return data.access_token; +} + +export const AzureProviderFactory = () => { + const validateCredentials: TLogStreamFactoryValidateCredentials = async ({ + credentials + }) => { + const { tenantId, clientId, clientSecret, dceUrl, dcrId, cltName } = credentials; + + await blockLocalAndPrivateIpAddresses(dceUrl); + + const token = await getAzureToken(tenantId, clientId, clientSecret); + + const streamHeaders: RawAxiosRequestHeaders = { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + }; + + await request + .post( + `${dceUrl}/dataCollectionRules/${dcrId}/streams/Custom-${cltName}_CL?api-version=2023-01-01`, + createPayload({ ping: "ok" }), + { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + } + ) + .catch((err) => { + throw new BadRequestError({ message: `Failed to connect with Azure: ${(err as Error)?.message}` }); + }); + + return credentials; + }; + + const streamLog: TLogStreamFactoryStreamLog = async ({ credentials, auditLog }) => { + const { tenantId, clientId, clientSecret, dceUrl, dcrId, cltName } = credentials; + + await blockLocalAndPrivateIpAddresses(dceUrl); + + const token = await getAzureToken(tenantId, clientId, clientSecret); + + const streamHeaders: RawAxiosRequestHeaders = { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + }; + + await request.post( + `${dceUrl}/dataCollectionRules/${dcrId}/streams/Custom-${cltName}_CL?api-version=2023-01-01`, + createPayload(auditLog), + { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + } + ); + }; + + return { + validateCredentials, + streamLog + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/azure/azure-provider-fns.ts b/backend/src/ee/services/audit-log-stream/azure/azure-provider-fns.ts new file mode 100644 index 000000000..e558b69e2 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/azure/azure-provider-fns.ts @@ -0,0 +1,8 @@ +import { LogProvider } from "../audit-log-stream-enums"; + +export const getAzureProviderListItem = () => { + return { + name: "Azure" as const, + provider: LogProvider.Azure as const + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/azure/azure-provider-schemas.ts b/backend/src/ee/services/audit-log-stream/azure/azure-provider-schemas.ts new file mode 100644 index 000000000..50def1d79 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/azure/azure-provider-schemas.ts @@ -0,0 +1,52 @@ +import RE2 from "re2"; +import { z } from "zod"; + +import { LogProvider } from "../audit-log-stream-enums"; +import { BaseProviderSchema } from "../audit-log-stream-schemas"; + +export const AzureProviderCredentialsSchema = z.object({ + tenantId: z.string().trim().uuid(), + clientId: z.string().trim().uuid(), + clientSecret: z.string().trim().length(40), + + // Data Collection Endpoint URL + dceUrl: z.string().trim().url().min(1).max(255), + + // Data Collection Rule Immutable ID + dcrId: z + .string() + .trim() + .refine((val) => new RE2(/^dcr-[0-9a-f]{32}$/).test(val), "DCR ID must be in dcr-*** format"), + + // Custom Log Table Name + cltName: z.string().trim().min(1).max(255) +}); + +const BaseAzureProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Azure) }); + +export const AzureProviderSchema = BaseAzureProviderSchema.extend({ + credentials: AzureProviderCredentialsSchema +}); + +export const SanitizedAzureProviderSchema = BaseAzureProviderSchema.extend({ + credentials: AzureProviderCredentialsSchema.pick({ + tenantId: true, + clientId: true, + dceUrl: true, + dcrId: true, + cltName: true + }) +}); + +export const AzureProviderListItemSchema = z.object({ + name: z.literal("Azure"), + provider: z.literal(LogProvider.Azure) +}); + +export const CreateAzureProviderLogStreamSchema = z.object({ + credentials: AzureProviderCredentialsSchema +}); + +export const UpdateAzureProviderLogStreamSchema = z.object({ + credentials: AzureProviderCredentialsSchema +}); diff --git a/backend/src/ee/services/audit-log-stream/azure/azure-provider-types.ts b/backend/src/ee/services/audit-log-stream/azure/azure-provider-types.ts new file mode 100644 index 000000000..0ba5f120d --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/azure/azure-provider-types.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +import { AzureProviderCredentialsSchema, AzureProviderSchema } from "./azure-provider-schemas"; + +export type TAzureProvider = z.infer; + +export type TAzureProviderCredentials = z.infer; diff --git a/backend/src/ee/services/audit-log-stream/cribl/cribl-provider-factory.ts b/backend/src/ee/services/audit-log-stream/cribl/cribl-provider-factory.ts new file mode 100644 index 000000000..2e4eef93b --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/cribl/cribl-provider-factory.ts @@ -0,0 +1,58 @@ +import { RawAxiosRequestHeaders } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; + +import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue"; +import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types"; +import { TCriblProviderCredentials } from "./cribl-provider-types"; + +export const CriblProviderFactory = () => { + const validateCredentials: TLogStreamFactoryValidateCredentials = async ({ + credentials + }) => { + const { url, token } = credentials; + + await blockLocalAndPrivateIpAddresses(url); + + const streamHeaders: RawAxiosRequestHeaders = { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + }; + + await request + .post(url, JSON.stringify({ ping: "ok" }), { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + }) + .catch((err) => { + throw new BadRequestError({ message: `Failed to connect with Cribl: ${(err as Error)?.message}` }); + }); + + return credentials; + }; + + const streamLog: TLogStreamFactoryStreamLog = async ({ credentials, auditLog }) => { + const { url, token } = credentials; + + await blockLocalAndPrivateIpAddresses(url); + + const streamHeaders: RawAxiosRequestHeaders = { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + }; + + await request.post(url, JSON.stringify(auditLog), { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + }); + }; + + return { + validateCredentials, + streamLog + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/cribl/cribl-provider-fns.ts b/backend/src/ee/services/audit-log-stream/cribl/cribl-provider-fns.ts new file mode 100644 index 000000000..f8b82509a --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/cribl/cribl-provider-fns.ts @@ -0,0 +1,8 @@ +import { LogProvider } from "../audit-log-stream-enums"; + +export const getCriblProviderListItem = () => { + return { + name: "Cribl" as const, + provider: LogProvider.Cribl as const + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/cribl/cribl-provider-schemas.ts b/backend/src/ee/services/audit-log-stream/cribl/cribl-provider-schemas.ts new file mode 100644 index 000000000..8c2a51f32 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/cribl/cribl-provider-schemas.ts @@ -0,0 +1,34 @@ +import { z } from "zod"; + +import { LogProvider } from "../audit-log-stream-enums"; +import { BaseProviderSchema } from "../audit-log-stream-schemas"; + +export const CriblProviderCredentialsSchema = z.object({ + url: z.string().url().trim().min(1).max(255), + token: z.string().trim().min(21).max(255) +}); + +const BaseCriblProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Cribl) }); + +export const CriblProviderSchema = BaseCriblProviderSchema.extend({ + credentials: CriblProviderCredentialsSchema +}); + +export const SanitizedCriblProviderSchema = BaseCriblProviderSchema.extend({ + credentials: CriblProviderCredentialsSchema.pick({ + url: true + }) +}); + +export const CriblProviderListItemSchema = z.object({ + name: z.literal("Cribl"), + provider: z.literal(LogProvider.Cribl) +}); + +export const CreateCriblProviderLogStreamSchema = z.object({ + credentials: CriblProviderCredentialsSchema +}); + +export const UpdateCriblProviderLogStreamSchema = z.object({ + credentials: CriblProviderCredentialsSchema +}); diff --git a/backend/src/ee/services/audit-log-stream/cribl/cribl-provider-types.ts b/backend/src/ee/services/audit-log-stream/cribl/cribl-provider-types.ts new file mode 100644 index 000000000..6577f1bff --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/cribl/cribl-provider-types.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +import { CriblProviderCredentialsSchema, CriblProviderSchema } from "./cribl-provider-schemas"; + +export type TCriblProvider = z.infer; + +export type TCriblProviderCredentials = z.infer; diff --git a/backend/src/ee/services/audit-log-stream/custom/custom-provider-factory.ts b/backend/src/ee/services/audit-log-stream/custom/custom-provider-factory.ts new file mode 100644 index 000000000..6e397638c --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/custom/custom-provider-factory.ts @@ -0,0 +1,67 @@ +import { RawAxiosRequestHeaders } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; + +import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue"; +import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types"; +import { TCustomProviderCredentials } from "./custom-provider-types"; + +export const CustomProviderFactory = () => { + const validateCredentials: TLogStreamFactoryValidateCredentials = async ({ + credentials + }) => { + const { url, headers } = credentials; + + await blockLocalAndPrivateIpAddresses(url); + + const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; + if (headers.length) { + headers.forEach(({ key, value }) => { + streamHeaders[key] = value; + }); + } + + await request + .post( + url, + { ping: "ok" }, + { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + } + ) + .catch((err) => { + throw new BadRequestError({ message: `Failed to connect with upstream source: ${(err as Error)?.message}` }); + }); + + return credentials; + }; + + const streamLog: TLogStreamFactoryStreamLog = async ({ credentials, auditLog }) => { + const { url, headers } = credentials; + + await blockLocalAndPrivateIpAddresses(url); + + const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; + + if (headers.length) { + headers.forEach(({ key, value }) => { + streamHeaders[key] = value; + }); + } + + await request.post(url, auditLog, { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + }); + }; + + return { + validateCredentials, + streamLog + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/custom/custom-provider-fns.ts b/backend/src/ee/services/audit-log-stream/custom/custom-provider-fns.ts new file mode 100644 index 000000000..27b8bbf72 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/custom/custom-provider-fns.ts @@ -0,0 +1,8 @@ +import { LogProvider } from "../audit-log-stream-enums"; + +export const getCustomProviderListItem = () => { + return { + name: "Custom" as const, + provider: LogProvider.Custom as const + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/custom/custom-provider-schemas.ts b/backend/src/ee/services/audit-log-stream/custom/custom-provider-schemas.ts new file mode 100644 index 000000000..d960d0fe9 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/custom/custom-provider-schemas.ts @@ -0,0 +1,50 @@ +import RE2 from "re2"; +import { z } from "zod"; + +import { LogProvider } from "../audit-log-stream-enums"; +import { BaseProviderSchema } from "../audit-log-stream-schemas"; + +export const CustomProviderCredentialsSchema = z.object({ + url: z.string().url().trim().min(1).max(255), + headers: z + .object({ + key: z + .string() + .min(1) + .refine((val) => new RE2(/^[^\n\r]+$/).test(val), "Header keys cannot contain newlines or carriage returns"), + value: z + .string() + .min(1) + .refine((val) => new RE2(/^[^\n\r]+$/).test(val), "Header values cannot contain newlines or carriage returns") + }) + .array() +}); + +const BaseCustomProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Custom) }); + +export const CustomProviderSchema = BaseCustomProviderSchema.extend({ + credentials: CustomProviderCredentialsSchema +}); + +export const SanitizedCustomProviderSchema = BaseCustomProviderSchema.extend({ + credentials: z.object({ + url: CustomProviderCredentialsSchema.shape.url, + // Return header keys and a redacted value + headers: CustomProviderCredentialsSchema.shape.headers.transform((headers) => + headers.map((header) => ({ ...header, value: "******" })) + ) + }) +}); + +export const CustomProviderListItemSchema = z.object({ + name: z.literal("Custom"), + provider: z.literal(LogProvider.Custom) +}); + +export const CreateCustomProviderLogStreamSchema = z.object({ + credentials: CustomProviderCredentialsSchema +}); + +export const UpdateCustomProviderLogStreamSchema = z.object({ + credentials: CustomProviderCredentialsSchema +}); diff --git a/backend/src/ee/services/audit-log-stream/custom/custom-provider-types.ts b/backend/src/ee/services/audit-log-stream/custom/custom-provider-types.ts new file mode 100644 index 000000000..9b2de8347 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/custom/custom-provider-types.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +import { CustomProviderCredentialsSchema, CustomProviderSchema } from "./custom-provider-schemas"; + +export type TCustomProvider = z.infer; + +export type TCustomProviderCredentials = z.infer; diff --git a/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-factory.ts b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-factory.ts new file mode 100644 index 000000000..ec55784da --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-factory.ts @@ -0,0 +1,67 @@ +import { RawAxiosRequestHeaders } from "axios"; + +import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; + +import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue"; +import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types"; +import { TDatadogProviderCredentials } from "./datadog-provider-types"; + +function createPayload(event: Record) { + const appCfg = getConfig(); + + const ddtags = [`env:${appCfg.NODE_ENV || "unknown"}`].join(","); + + return { + ...event, + hostname: new URL(appCfg.SITE_URL || "http://infisical").hostname, + ddsource: "infisical", + service: "infisical", + ddtags + }; +} + +export const DatadogProviderFactory = () => { + const validateCredentials: TLogStreamFactoryValidateCredentials = async ({ + credentials + }) => { + const { url, token } = credentials; + + await blockLocalAndPrivateIpAddresses(url); + + const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json", "DD-API-KEY": token }; + + await request + .post(url, createPayload({ ping: "ok" }), { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + }) + .catch((err) => { + throw new BadRequestError({ message: `Failed to connect with Datadog: ${(err as Error)?.message}` }); + }); + + return credentials; + }; + + const streamLog: TLogStreamFactoryStreamLog = async ({ credentials, auditLog }) => { + const { url, token } = credentials; + + await blockLocalAndPrivateIpAddresses(url); + + const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json", "DD-API-KEY": token }; + + await request.post(url, createPayload(auditLog), { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + }); + }; + + return { + validateCredentials, + streamLog + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-fns.ts b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-fns.ts new file mode 100644 index 000000000..ec68fdb39 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-fns.ts @@ -0,0 +1,8 @@ +import { LogProvider } from "../audit-log-stream-enums"; + +export const getDatadogProviderListItem = () => { + return { + name: "Datadog" as const, + provider: LogProvider.Datadog as const + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-schemas.ts b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-schemas.ts new file mode 100644 index 000000000..0445d79f2 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-schemas.ts @@ -0,0 +1,38 @@ +import RE2 from "re2"; +import { z } from "zod"; + +import { LogProvider } from "../audit-log-stream-enums"; +import { BaseProviderSchema } from "../audit-log-stream-schemas"; + +export const DatadogProviderCredentialsSchema = z.object({ + url: z.string().url().trim().min(1).max(255), + token: z + .string() + .trim() + .refine((val) => new RE2(/^[a-fA-F0-9]{32}$/).test(val), "Invalid Datadog API key format") +}); + +const BaseDatadogProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Datadog) }); + +export const DatadogProviderSchema = BaseDatadogProviderSchema.extend({ + credentials: DatadogProviderCredentialsSchema +}); + +export const SanitizedDatadogProviderSchema = BaseDatadogProviderSchema.extend({ + credentials: DatadogProviderCredentialsSchema.pick({ + url: true + }) +}); + +export const DatadogProviderListItemSchema = z.object({ + name: z.literal("Datadog"), + provider: z.literal(LogProvider.Datadog) +}); + +export const CreateDatadogProviderLogStreamSchema = z.object({ + credentials: DatadogProviderCredentialsSchema +}); + +export const UpdateDatadogProviderLogStreamSchema = z.object({ + credentials: DatadogProviderCredentialsSchema +}); diff --git a/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-types.ts b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-types.ts new file mode 100644 index 000000000..e7f6a1f28 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-types.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +import { DatadogProviderCredentialsSchema, DatadogProviderSchema } from "./datadog-provider-schemas"; + +export type TDatadogProvider = z.infer; + +export type TDatadogProviderCredentials = z.infer; diff --git a/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-factory.ts b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-factory.ts new file mode 100644 index 000000000..72fc90ce2 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-factory.ts @@ -0,0 +1,84 @@ +import { RawAxiosRequestHeaders } from "axios"; + +import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; + +import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue"; +import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types"; +import { TSplunkProviderCredentials } from "./splunk-provider-types"; + +function createPayload(event: Record) { + const appCfg = getConfig(); + + return { + time: Math.floor(Date.now() / 1000), + ...(appCfg.SITE_URL && { host: new URL(appCfg.SITE_URL).host }), + source: "infisical", + sourcetype: "_json", + event + }; +} + +async function createSplunkUrl(hostname: string) { + let parsedHostname: string; + try { + parsedHostname = new URL(`https://${hostname}`).hostname; + } catch (error) { + throw new BadRequestError({ message: `Invalid Splunk hostname provided: ${(error as Error).message}` }); + } + + await blockLocalAndPrivateIpAddresses(`https://${parsedHostname}`); + + return `https://${parsedHostname}:8088/services/collector/event`; +} + +export const SplunkProviderFactory = () => { + const validateCredentials: TLogStreamFactoryValidateCredentials = async ({ + credentials + }) => { + const { hostname, token } = credentials; + + const url = await createSplunkUrl(hostname); + + const streamHeaders: RawAxiosRequestHeaders = { + "Content-Type": "application/json", + Authorization: `Splunk ${token}` + }; + + await request + .post(url, createPayload({ ping: "ok" }), { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + }) + .catch((err) => { + throw new BadRequestError({ message: `Failed to connect with Splunk: ${(err as Error)?.message}` }); + }); + + return credentials; + }; + + const streamLog: TLogStreamFactoryStreamLog = async ({ credentials, auditLog }) => { + const { hostname, token } = credentials; + + const url = await createSplunkUrl(hostname); + + const streamHeaders: RawAxiosRequestHeaders = { + "Content-Type": "application/json", + Authorization: `Splunk ${token}` + }; + + await request.post(url, createPayload(auditLog), { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + }); + }; + + return { + validateCredentials, + streamLog + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-fns.ts b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-fns.ts new file mode 100644 index 000000000..e2ea2e316 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-fns.ts @@ -0,0 +1,8 @@ +import { LogProvider } from "../audit-log-stream-enums"; + +export const getSplunkProviderListItem = () => { + return { + name: "Splunk" as const, + provider: LogProvider.Splunk as const + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-schemas.ts b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-schemas.ts new file mode 100644 index 000000000..ab28db616 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-schemas.ts @@ -0,0 +1,59 @@ +import { z } from "zod"; + +import { LogProvider } from "../audit-log-stream-enums"; +import { BaseProviderSchema } from "../audit-log-stream-schemas"; + +export const SplunkProviderCredentialsSchema = z.object({ + hostname: z + .string() + .trim() + .min(1) + .max(255) + .superRefine((val, ctx) => { + if (val.includes("://")) { + ctx.addIssue({ + code: "custom", + message: "Hostname should not include protocol" + }); + return; + } + + try { + const url = new URL(`https://${val}`); + if (url.hostname !== val) { + ctx.addIssue({ + code: "custom", + message: "Must be a valid hostname without port or path" + }); + } + } catch { + ctx.addIssue({ code: "custom", message: "Invalid hostname" }); + } + }), + token: z.string().uuid().trim().min(1) +}); + +const BaseSplunkProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Splunk) }); + +export const SplunkProviderSchema = BaseSplunkProviderSchema.extend({ + credentials: SplunkProviderCredentialsSchema +}); + +export const SanitizedSplunkProviderSchema = BaseSplunkProviderSchema.extend({ + credentials: SplunkProviderCredentialsSchema.pick({ + hostname: true + }) +}); + +export const SplunkProviderListItemSchema = z.object({ + name: z.literal("Splunk"), + provider: z.literal(LogProvider.Splunk) +}); + +export const CreateSplunkProviderLogStreamSchema = z.object({ + credentials: SplunkProviderCredentialsSchema +}); + +export const UpdateSplunkProviderLogStreamSchema = z.object({ + credentials: SplunkProviderCredentialsSchema +}); diff --git a/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-types.ts b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-types.ts new file mode 100644 index 000000000..11f7c8fcb --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-types.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +import { SplunkProviderCredentialsSchema, SplunkProviderSchema } from "./splunk-provider-schemas"; + +export type TSplunkProvider = z.infer; + +export type TSplunkProviderCredentials = z.infer; diff --git a/backend/src/ee/services/audit-log/audit-log-queue.ts b/backend/src/ee/services/audit-log/audit-log-queue.ts index 0914b8f6b..6b286c2ec 100644 --- a/backend/src/ee/services/audit-log/audit-log-queue.ts +++ b/backend/src/ee/services/audit-log/audit-log-queue.ts @@ -1,22 +1,14 @@ -import { AxiosError, RawAxiosRequestHeaders } from "axios"; - -import { SecretKeyEncoding } from "@app/db/schemas"; -import { request } from "@app/lib/config/request"; -import { crypto } from "@app/lib/crypto/cryptography"; -import { logger } from "@app/lib/logger"; +import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TProjectDALFactory } from "@app/services/project/project-dal"; -import { TAuditLogStreamDALFactory } from "../audit-log-stream/audit-log-stream-dal"; -import { providerSpecificPayload } from "../audit-log-stream/audit-log-stream-fns"; -import { LogStreamHeaders } from "../audit-log-stream/audit-log-stream-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { TAuditLogDALFactory } from "./audit-log-dal"; import { TCreateAuditLogDTO } from "./audit-log-types"; type TAuditLogQueueServiceFactoryDep = { auditLogDAL: TAuditLogDALFactory; - auditLogStreamDAL: Pick; + auditLogStreamService: Pick; queueService: TQueueServiceFactory; projectDAL: Pick; licenseService: Pick; @@ -35,7 +27,7 @@ export const auditLogQueueServiceFactory = async ({ queueService, projectDAL, licenseService, - auditLogStreamDAL + auditLogStreamService }: TAuditLogQueueServiceFactoryDep): Promise => { const pushToLog = async (data: TCreateAuditLogDTO) => { await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, { @@ -86,60 +78,7 @@ export const auditLogQueueServiceFactory = async ({ 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( - crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - 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; - }); - - try { - const response = await request.post( - url, - { ...providerSpecificPayload(url), ...auditLog }, - { - headers, - // request timeout - timeout: AUDIT_LOG_STREAM_TIMEOUT, - // connection timeout - signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) - } - ); - return response; - } catch (error) { - logger.error( - `Failed to stream audit log [url=${url}] for org [orgId=${orgId}] [error=${(error as AxiosError).message}]` - ); - return error; - } - } - ) - ); + await auditLogStreamService.streamLog(orgId, auditLog); } }); diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 10c84cc4a..07773885a 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -146,7 +146,7 @@ export enum EventType { MOVE_SECRETS = "move-secrets", DELETE_SECRET = "delete-secret", DELETE_SECRETS = "delete-secrets", - GET_WORKSPACE_KEY = "get-workspace-key", + GET_PROJECT_KEY = "get-project-key", AUTHORIZE_INTEGRATION = "authorize-integration", UPDATE_INTEGRATION_AUTH = "update-integration-auth", UNAUTHORIZE_INTEGRATION = "unauthorize-integration", @@ -199,6 +199,7 @@ export enum EventType { CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS = "clear-identity-universal-auth-lockouts", + CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS = "clear-identity-ldap-auth-lockouts", GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id", @@ -249,9 +250,9 @@ export enum EventType { UPDATE_ENVIRONMENT = "update-environment", DELETE_ENVIRONMENT = "delete-environment", GET_ENVIRONMENT = "get-environment", - ADD_WORKSPACE_MEMBER = "add-workspace-member", - ADD_BATCH_WORKSPACE_MEMBER = "add-workspace-members", - REMOVE_WORKSPACE_MEMBER = "remove-workspace-member", + ADD_PROJECT_MEMBER = "add-project-member", + ADD_BATCH_PROJECT_MEMBER = "add-project-members", + REMOVE_PROJECT_MEMBER = "remove-project-member", CREATE_FOLDER = "create-folder", UPDATE_FOLDER = "update-folder", DELETE_FOLDER = "delete-folder", @@ -264,8 +265,8 @@ export enum EventType { CREATE_SECRET_IMPORT = "create-secret-import", UPDATE_SECRET_IMPORT = "update-secret-import", DELETE_SECRET_IMPORT = "delete-secret-import", - UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role", - UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions", + UPDATE_USER_PROJECT_ROLE = "update-user-project-role", + UPDATE_USER_PROJECT_DENIED_PERMISSIONS = "update-user-project-denied-permissions", SECRET_APPROVAL_MERGED = "secret-approval-merged", SECRET_APPROVAL_REQUEST = "secret-approval-request", SECRET_APPROVAL_CLOSED = "secret-approval-closed", @@ -392,6 +393,8 @@ export enum EventType { CREATE_APP_CONNECTION = "create-app-connection", UPDATE_APP_CONNECTION = "update-app-connection", DELETE_APP_CONNECTION = "delete-app-connection", + GET_APP_CONNECTION_USAGE = "get-app-connection-usage", + MIGRATE_APP_CONNECTION = "migrate-app-connection", CREATE_SHARED_SECRET = "create-shared-secret", CREATE_SECRET_REQUEST = "create-secret-request", DELETE_SHARED_SECRET = "delete-shared-secret", @@ -664,8 +667,8 @@ interface DeleteSecretBatchEvent { }; } -interface GetWorkspaceKeyEvent { - type: EventType.GET_WORKSPACE_KEY; +interface GetProjectKeyEvent { + type: EventType.GET_PROJECT_KEY; metadata: { keyId: string; }; @@ -1370,6 +1373,10 @@ interface AddIdentityLdapAuthEvent { allowedFields?: TAllowedFields[]; url: string; templateId?: string | null; + lockoutEnabled: boolean; + lockoutThreshold: number; + lockoutDurationSeconds: number; + lockoutCounterResetSeconds: number; }; } @@ -1384,6 +1391,10 @@ interface UpdateIdentityLdapAuthEvent { allowedFields?: TAllowedFields[]; url?: string; templateId?: string | null; + lockoutEnabled?: boolean; + lockoutThreshold?: number; + lockoutDurationSeconds?: number; + lockoutCounterResetSeconds?: number; }; } @@ -1401,6 +1412,13 @@ interface RevokeIdentityLdapAuthEvent { }; } +interface ClearIdentityLdapAuthLockoutsEvent { + type: EventType.CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS; + metadata: { + identityId: string; + }; +} + interface LoginIdentityOidcAuthEvent { type: EventType.LOGIN_IDENTITY_OIDC_AUTH; metadata: { @@ -1557,24 +1575,24 @@ interface DeleteEnvironmentEvent { }; } -interface AddWorkspaceMemberEvent { - type: EventType.ADD_WORKSPACE_MEMBER; +interface AddProjectMemberEvent { + type: EventType.ADD_PROJECT_MEMBER; metadata: { userId: string; email: string; }; } -interface AddBatchWorkspaceMemberEvent { - type: EventType.ADD_BATCH_WORKSPACE_MEMBER; +interface AddBatchProjectMemberEvent { + type: EventType.ADD_BATCH_PROJECT_MEMBER; metadata: Array<{ userId: string; email: string; }>; } -interface RemoveWorkspaceMemberEvent { - type: EventType.REMOVE_WORKSPACE_MEMBER; +interface RemoveProjectMemberEvent { + type: EventType.REMOVE_PROJECT_MEMBER; metadata: { userId: string; email: string; @@ -1713,7 +1731,7 @@ interface DeleteSecretImportEvent { } interface UpdateUserRole { - type: EventType.UPDATE_USER_WORKSPACE_ROLE; + type: EventType.UPDATE_USER_PROJECT_ROLE; metadata: { userId: string; email: string; @@ -1723,7 +1741,7 @@ interface UpdateUserRole { } interface UpdateUserDeniedPermissions { - type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS; + type: EventType.UPDATE_USER_PROJECT_DENIED_PERMISSIONS; metadata: { userId: string; email: string; @@ -2781,14 +2799,31 @@ interface GetAppConnectionEvent { }; } +interface GetAppConnectionUsageEvent { + type: EventType.GET_APP_CONNECTION_USAGE; + metadata: { + connectionId: string; + }; +} + +interface MigrateAppConnectionEvent { + type: EventType.MIGRATE_APP_CONNECTION; + metadata: { + connectionId: string; + }; +} + interface CreateAppConnectionEvent { type: EventType.CREATE_APP_CONNECTION; - metadata: Omit & { connectionId: string }; + metadata: Omit & { connectionId: string }; } interface UpdateAppConnectionEvent { type: EventType.UPDATE_APP_CONNECTION; - metadata: Omit & { connectionId: string; credentialsUpdated: boolean }; + metadata: Omit & { + connectionId: string; + credentialsUpdated: boolean; + }; } interface DeleteAppConnectionEvent { @@ -3477,7 +3512,7 @@ export type Event = | MoveSecretsEvent | DeleteSecretEvent | DeleteSecretBatchEvent - | GetWorkspaceKeyEvent + | GetProjectKeyEvent | AuthorizeIntegrationEvent | UpdateIntegrationAuthEvent | UnauthorizeIntegrationEvent @@ -3562,13 +3597,14 @@ export type Event = | UpdateIdentityLdapAuthEvent | GetIdentityLdapAuthEvent | RevokeIdentityLdapAuthEvent + | ClearIdentityLdapAuthLockoutsEvent | CreateEnvironmentEvent | GetEnvironmentEvent | UpdateEnvironmentEvent | DeleteEnvironmentEvent - | AddWorkspaceMemberEvent - | AddBatchWorkspaceMemberEvent - | RemoveWorkspaceMemberEvent + | AddProjectMemberEvent + | AddBatchProjectMemberEvent + | RemoveProjectMemberEvent | CreateFolderEvent | UpdateFolderEvent | DeleteFolderEvent @@ -3697,6 +3733,8 @@ export type Event = | CreateAppConnectionEvent | UpdateAppConnectionEvent | DeleteAppConnectionEvent + | GetAppConnectionUsageEvent + | MigrateAppConnectionEvent | GetSshHostGroupEvent | CreateSshHostGroupEvent | UpdateSshHostGroupEvent diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts index 525de9efd..974e20061 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts @@ -46,7 +46,10 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => { try { - const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first(); + const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease) + .count("*") + .where({ dynamicSecretId }) + .first(); return parseInt(doc || "0", 10); } catch (error) { throw new DatabaseError({ error, name: "DynamicSecretCountLeases" }); @@ -55,7 +58,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const doc = await (tx || db)(TableName.DynamicSecretLease) + const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease) .where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id }) .first() .join( diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 73dcbe6e3..659e07bca 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -19,6 +19,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal"; import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue"; import { TGatewayDALFactory } from "../gateway/gateway-dal"; +import { TGatewayV2DALFactory } from "../gateway-v2/gateway-v2-dal"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TDynamicSecretDALFactory } from "./dynamic-secret-dal"; import { DynamicSecretStatus, TDynamicSecretServiceFactory } from "./dynamic-secret-types"; @@ -39,6 +40,7 @@ type TDynamicSecretServiceFactoryDep = { permissionService: Pick; kmsService: Pick; gatewayDAL: Pick; + gatewayV2DAL: Pick; resourceMetadataDAL: Pick; }; @@ -53,6 +55,7 @@ export const dynamicSecretServiceFactory = ({ projectDAL, kmsService, gatewayDAL, + gatewayV2DAL, resourceMetadataDAL }: TDynamicSecretServiceFactoryDep): TDynamicSecretServiceFactory => { const create: TDynamicSecretServiceFactory["create"] = async ({ @@ -70,6 +73,7 @@ export const dynamicSecretServiceFactory = ({ metadata, usernameTemplate }) => { + let isGatewayV1 = true; const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); @@ -118,17 +122,22 @@ export const dynamicSecretServiceFactory = ({ const gatewayId = inputs.gatewayId as string; const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId }); + const [gatewayv2] = await gatewayV2DAL.find({ id: gatewayId, orgId: actorOrgId }); - if (!gateway) { + if (!gateway && !gatewayv2) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found` }); } + if (!gateway) { + isGatewayV1 = false; + } + const { permission: orgPermission } = await permissionService.getOrgPermission( actor, actorId, - gateway.orgId, + gateway?.orgId ?? gatewayv2?.orgId, actorAuthMethod, actorOrgId ); @@ -138,7 +147,7 @@ export const dynamicSecretServiceFactory = ({ OrgPermissionSubjects.Gateway ); - selectedGatewayId = gateway.id; + selectedGatewayId = gateway?.id ?? gatewayv2?.id; } const isConnected = await selectedProvider.validateConnection(provider.inputs, { projectId }); @@ -159,7 +168,8 @@ export const dynamicSecretServiceFactory = ({ defaultTTL, folderId: folder.id, name, - gatewayId: selectedGatewayId, + gatewayId: isGatewayV1 ? selectedGatewayId : undefined, + gatewayV2Id: isGatewayV1 ? undefined : selectedGatewayId, usernameTemplate }, tx @@ -180,7 +190,7 @@ export const dynamicSecretServiceFactory = ({ return cfg; }); - return dynamicSecretCfg; + return { ...dynamicSecretCfg, inputs }; }; const updateByName: TDynamicSecretServiceFactory["updateByName"] = async ({ @@ -270,20 +280,27 @@ export const dynamicSecretServiceFactory = ({ const updatedInput = await selectedProvider.validateProviderInputs(newInput, { projectId }); let selectedGatewayId: string | null = null; + let isGatewayV1 = true; if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) { const gatewayId = updatedInput.gatewayId as string; const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId }); - if (!gateway) { + const [gatewayv2] = await gatewayV2DAL.find({ id: gatewayId, orgId: actorOrgId }); + + if (!gateway && !gatewayv2) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found` }); } + if (!gateway) { + isGatewayV1 = false; + } + const { permission: orgPermission } = await permissionService.getOrgPermission( actor, actorId, - gateway.orgId, + actorOrgId, actorAuthMethod, actorOrgId ); @@ -293,7 +310,7 @@ export const dynamicSecretServiceFactory = ({ OrgPermissionSubjects.Gateway ); - selectedGatewayId = gateway.id; + selectedGatewayId = gateway?.id ?? gatewayv2?.id; } const isConnected = await selectedProvider.validateConnection(newInput, { projectId }); @@ -309,7 +326,8 @@ export const dynamicSecretServiceFactory = ({ defaultTTL, name: newName ?? name, status: null, - gatewayId: selectedGatewayId, + gatewayId: isGatewayV1 ? selectedGatewayId : null, + gatewayV2Id: isGatewayV1 ? null : selectedGatewayId, usernameTemplate }, tx @@ -337,7 +355,7 @@ export const dynamicSecretServiceFactory = ({ return cfg; }); - return updatedDynamicCfg; + return { ...updatedDynamicCfg, inputs: updatedInput }; }; const deleteByName: TDynamicSecretServiceFactory["deleteByName"] = async ({ diff --git a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts index 294a9a723..32efd9848 100644 --- a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts +++ b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts @@ -30,7 +30,7 @@ const generateUsername = (usernameTemplate?: string | null, identity?: { name: s export const CassandraProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs); - const hostIps = await Promise.all( + await Promise.all( providerInputs.host .split(",") .filter(Boolean) @@ -48,10 +48,10 @@ export const CassandraProvider = (): TDynamicProviderFns => { allowedExpressions: (val) => ["username"].includes(val) }); - return { ...providerInputs, hostIps }; + return { ...providerInputs }; }; - const $getClient = async (providerInputs: z.infer & { hostIps: string[] }) => { + const $getClient = async (providerInputs: z.infer) => { const sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined; const client = new cassandra.Client({ sslOptions, @@ -64,7 +64,7 @@ export const CassandraProvider = (): TDynamicProviderFns => { }, keyspace: providerInputs.keyspace, localDataCenter: providerInputs?.localDataCenter, - contactPoints: providerInputs.hostIps + contactPoints: providerInputs.host.split(",") }); return client; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts index bde62fc61..f4d43f23f 100644 --- a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -28,14 +28,14 @@ const generateUsername = (usernameTemplate?: string | null, identity?: { name: s export const ElasticSearchProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs); - const [hostIp] = await verifyHostInputValidity(providerInputs.host); - return { ...providerInputs, hostIp }; + await verifyHostInputValidity(providerInputs.host); + return { ...providerInputs }; }; - const $getClient = async (providerInputs: z.infer & { hostIp: string }) => { + const $getClient = async (providerInputs: z.infer) => { const connection = new ElasticSearchClient({ node: { - url: new URL(`${providerInputs.hostIp}:${providerInputs.port}`), + url: new URL(`${providerInputs.host}:${providerInputs.port}`), ...(providerInputs.ca && { ssl: { rejectUnauthorized: false, diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index 184b9fc89..3ec0f795e 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -1,6 +1,7 @@ import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake"; import { TGatewayServiceFactory } from "../../gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service"; import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache"; import { AwsIamProvider } from "./aws-iam"; import { AzureEntraIDProvider } from "./azure-entra-id"; @@ -24,12 +25,14 @@ import { VerticaProvider } from "./vertica"; type TBuildDynamicSecretProviderDTO = { gatewayService: Pick; + gatewayV2Service: Pick; }; export const buildDynamicSecretProviders = ({ - gatewayService + gatewayService, + gatewayV2Service }: TBuildDynamicSecretProviderDTO): Record => ({ - [DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }), + [DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService, gatewayV2Service }), [DynamicSecretProviders.Cassandra]: CassandraProvider(), [DynamicSecretProviders.AwsIam]: AwsIamProvider(), [DynamicSecretProviders.Redis]: RedisDatabaseProvider(), @@ -44,7 +47,7 @@ export const buildDynamicSecretProviders = ({ [DynamicSecretProviders.Snowflake]: SnowflakeProvider(), [DynamicSecretProviders.Totp]: TotpProvider(), [DynamicSecretProviders.SapAse]: SapAseProvider(), - [DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }), + [DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService, gatewayV2Service }), [DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }), [DynamicSecretProviders.GcpIam]: GcpIamProvider(), [DynamicSecretProviders.Github]: GithubProvider(), diff --git a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts index 3d69c3282..3c924458d 100644 --- a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts +++ b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts @@ -5,12 +5,14 @@ import https from "https"; import { BadRequestError } from "@app/lib/errors"; import { sanitizeString } from "@app/lib/fn"; import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types"; import { TDynamicSecretKubernetesLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types"; import { TGatewayServiceFactory } from "../../gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service"; import { DynamicSecretKubernetesSchema, KubernetesAuthMethod, @@ -26,6 +28,7 @@ const GATEWAY_AUTH_DEFAULT_URL = "https://kubernetes.default.svc.cluster.local"; type TKubernetesProviderDTO = { gatewayService: Pick; + gatewayV2Service: Pick; }; const generateUsername = (usernameTemplate?: string | null) => { @@ -38,7 +41,10 @@ const generateUsername = (usernameTemplate?: string | null) => { }); }; -export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => { +export const KubernetesProvider = ({ + gatewayService, + gatewayV2Service +}: TKubernetesProviderDTO): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs); if (!providerInputs.gatewayId && providerInputs.url) { @@ -58,6 +64,32 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): }, gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise ): Promise => { + const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ + gatewayId: inputs.gatewayId, + targetHost: inputs.targetHost, + targetPort: inputs.targetPort + }); + if (gatewayV2ConnectionDetails) { + const callbackResult = await withGatewayV2Proxy( + async (port) => { + return gatewayCallback( + inputs.reviewTokenThroughGateway ? "http://localhost" : "https://localhost", + port, + inputs.httpsAgent + ); + }, + { + relayHost: gatewayV2ConnectionDetails.relayHost, + gateway: gatewayV2ConnectionDetails.gateway, + relay: gatewayV2ConnectionDetails.relay, + protocol: inputs.reviewTokenThroughGateway ? GatewayProxyProtocol.Http : GatewayProxyProtocol.Tcp, + httpsAgent: inputs.httpsAgent + } + ); + + return callbackResult; + } + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); @@ -353,8 +385,18 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): return true; } catch (error) { let errorMessage = error instanceof Error ? error.message : "Unknown error"; - if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) { - errorMessage = (error.response?.data as { message: string }).message; + if (axios.isAxiosError(error)) { + if (error.response) { + let { message } = error?.response?.data as unknown as { message?: string }; + + if (!message && typeof error.response.data === "string") { + message = error.response.data; + } + + if (message) { + errorMessage = message; + } + } } const sanitizedErrorMessage = sanitizeString({ @@ -603,8 +645,18 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): }; } catch (error) { let errorMessage = error instanceof Error ? error.message : "Unknown error"; - if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) { - errorMessage = (error.response?.data as { message: string }).message; + if (axios.isAxiosError(error)) { + if (error.response) { + let { message } = error?.response?.data as unknown as { message?: string }; + + if (!message && typeof error.response.data === "string") { + message = error.response.data; + } + + if (message) { + errorMessage = message; + } + } } const sanitizedErrorMessage = sanitizeString({ @@ -740,8 +792,18 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): } } catch (error) { let errorMessage = error instanceof Error ? error.message : "Unknown error"; - if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) { - errorMessage = (error.response?.data as { message: string }).message; + if (axios.isAxiosError(error)) { + if (error.response) { + let { message } = error?.response?.data as unknown as { message?: string }; + + if (!message && typeof error.response.data === "string") { + message = error.response.data; + } + + if (message) { + errorMessage = message; + } + } } const sanitizedErrorMessage = sanitizeString({ diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index b8782efe0..3586fa0d9 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -170,6 +170,7 @@ export const DynamicSecretSqlDBSchema = z.object({ revocationStatement: z.string().trim(), renewStatement: z.string().trim().optional(), ca: z.string().optional(), + sslEnabled: z.boolean().optional(), gatewayId: z.string().nullable().optional() }); @@ -283,11 +284,11 @@ export const DynamicSecretMongoAtlasSchema = z.object({ export const DynamicSecretMongoDBSchema = z.object({ host: z.string().min(1).trim().toLowerCase(), - port: z.number().optional(), + port: z.number().optional().nullable(), username: z.string().min(1).trim(), password: z.string().min(1).trim(), database: z.string().min(1).trim(), - ca: z.string().min(1).optional(), + ca: z.string().trim().optional().nullable(), roles: z .string() .array() diff --git a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts index dfae417f6..8154f3e13 100644 --- a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts +++ b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts @@ -28,15 +28,15 @@ const generateUsername = (usernameTemplate?: string | null, identity?: { name: s export const MongoDBProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs); - const [hostIp] = await verifyHostInputValidity(providerInputs.host); - return { ...providerInputs, hostIp }; + await verifyHostInputValidity(providerInputs.host); + return { ...providerInputs }; }; - const $getClient = async (providerInputs: z.infer & { hostIp: string }) => { + const $getClient = async (providerInputs: z.infer) => { const isSrv = !providerInputs.port; const uri = isSrv - ? `mongodb+srv://${providerInputs.hostIp}` - : `mongodb://${providerInputs.hostIp}:${providerInputs.port}`; + ? `mongodb+srv://${providerInputs.host}` + : `mongodb://${providerInputs.host}:${providerInputs.port}`; const client = new MongoClient(uri, { auth: { @@ -44,7 +44,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => { password: providerInputs.password }, directConnection: !isSrv, - ca: providerInputs.ca + ca: providerInputs.ca || undefined }); return client; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts index 2660d9d8a..f3a0470b0 100644 --- a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts +++ b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts @@ -87,13 +87,13 @@ async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRa export const RabbitMqProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs); - const [hostIp] = await verifyHostInputValidity(providerInputs.host); - return { ...providerInputs, hostIp }; + await verifyHostInputValidity(providerInputs.host); + return { ...providerInputs }; }; - const $getClient = async (providerInputs: z.infer & { hostIp: string }) => { + const $getClient = async (providerInputs: z.infer) => { const axiosInstance = axios.create({ - baseURL: `${providerInputs.hostIp}:${providerInputs.port}/api`, + baseURL: `${providerInputs.host}:${providerInputs.port}/api`, auth: { username: providerInputs.username, password: providerInputs.password diff --git a/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts index b84859484..34f77d465 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts @@ -36,7 +36,7 @@ export const SapAseProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs); - const [hostIp] = await verifyHostInputValidity(providerInputs.host); + await verifyHostInputValidity(providerInputs.host); validateHandlebarTemplate("SAP ASE creation", providerInputs.creationStatement, { allowedExpressions: (val) => ["username", "password"].includes(val) }); @@ -45,16 +45,13 @@ export const SapAseProvider = (): TDynamicProviderFns => { allowedExpressions: (val) => ["username"].includes(val) }); } - return { ...providerInputs, hostIp }; + return { ...providerInputs }; }; - const $getClient = async ( - providerInputs: z.infer & { hostIp: string }, - useMaster?: boolean - ) => { + const $getClient = async (providerInputs: z.infer, useMaster?: boolean) => { const connectionString = `DRIVER={FreeTDS};` + - `SERVER=${providerInputs.hostIp};` + + `SERVER=${providerInputs.host};` + `PORT=${providerInputs.port};` + `DATABASE=${useMaster ? "master" : providerInputs.database};` + `UID=${providerInputs.username};` + diff --git a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts index 53b88a192..bc7400a36 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts @@ -37,7 +37,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs); - const [hostIp] = await verifyHostInputValidity(providerInputs.host); + await verifyHostInputValidity(providerInputs.host); validateHandlebarTemplate("SAP Hana creation", providerInputs.creationStatement, { allowedExpressions: (val) => ["username", "password", "expiration"].includes(val) }); @@ -49,12 +49,12 @@ export const SapHanaProvider = (): TDynamicProviderFns => { validateHandlebarTemplate("SAP Hana revoke", providerInputs.revocationStatement, { allowedExpressions: (val) => ["username"].includes(val) }); - return { ...providerInputs, hostIp }; + return { ...providerInputs }; }; - const $getClient = async (providerInputs: z.infer & { hostIp: string }) => { + const $getClient = async (providerInputs: z.infer) => { const client = hdb.createClient({ - host: providerInputs.hostIp, + host: providerInputs.host, port: providerInputs.port, user: providerInputs.username, password: providerInputs.password, diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index c8d036ce3..a9011b993 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -1,15 +1,18 @@ import handlebars from "handlebars"; import knex from "knex"; +import RE2 from "re2"; import { z } from "zod"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; import { sanitizeString } from "@app/lib/fn"; import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { TGatewayServiceFactory } from "../../gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models"; import { compileUsernameTemplate } from "./templateUtils"; @@ -128,9 +131,13 @@ const generateUsername = (provider: SqlProviders, usernameTemplate?: string | nu type TSqlDatabaseProviderDTO = { gatewayService: Pick; + gatewayV2Service: Pick; }; -export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => { +export const SqlDatabaseProvider = ({ + gatewayService, + gatewayV2Service +}: TSqlDatabaseProviderDTO): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs); @@ -150,17 +157,40 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) return { ...providerInputs, hostIp }; }; - const $getClient = async (providerInputs: z.infer) => { - const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined; + const $getClient = async ( + providerInputs: z.infer & { hostIp: string; originalHost: string } + ) => { + const ssl = providerInputs.ca + ? { rejectUnauthorized: false, ca: providerInputs.ca, servername: providerInputs.host } + : undefined; + const isMsSQLClient = providerInputs.client === SqlProviders.MsSQL; + /* + We route through the gateway by setting connection.host = "localhost". + Azure SQL identifies the logical server from the TDS login name when the host + isn’t the Azure FQDN. Therefore, when using the gateway, ensure username is + "user@" so Azure opens the correct logical server. + Direct connections to the Azure FQDN usually don’t require this suffix. + */ + const isAzureSql = isMsSQLClient && new RE2(/\.database\.windows\.net$/i).test(providerInputs.originalHost); + const azureServerLabel = + isAzureSql && providerInputs.gatewayId ? providerInputs.originalHost?.split(".")[0] : undefined; + const effectiveUser = + isAzureSql && !providerInputs.username.includes("@") && azureServerLabel + ? `${providerInputs.username}@${azureServerLabel}` + : providerInputs.username; + const db = knex({ client: providerInputs.client, connection: { database: providerInputs.database, port: providerInputs.port, - host: providerInputs.host, - user: providerInputs.username, + host: + providerInputs.client === SqlProviders.Postgres && !providerInputs.gatewayId + ? providerInputs.hostIp + : providerInputs.host, + user: effectiveUser, password: providerInputs.password, ssl, // @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver @@ -168,6 +198,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) // https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19 options: isMsSQLClient ? { + ...(providerInputs.sslEnabled !== undefined ? { encrypt: providerInputs.sslEnabled } : {}), trustServerCertificate: !providerInputs.ca, cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {} } @@ -183,6 +214,26 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) providerInputs: z.infer, gatewayCallback: (host: string, port: number) => Promise ) => { + const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ + gatewayId: providerInputs.gatewayId as string, + targetHost: providerInputs.host, + targetPort: providerInputs.port + }); + + if (gatewayV2ConnectionDetails) { + return withGatewayV2Proxy( + async (port) => { + await gatewayCallback("localhost", port); + }, + { + relayHost: gatewayV2ConnectionDetails.relayHost, + gateway: gatewayV2ConnectionDetails.gateway, + relay: gatewayV2ConnectionDetails.relay, + protocol: GatewayProxyProtocol.Tcp + } + ); + } + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); await withGatewayProxy( @@ -209,8 +260,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); let isConnected = false; - const gatewayCallback = async (host = providerInputs.hostIp, port = providerInputs.port) => { - const db = await $getClient({ ...providerInputs, port, host }); + const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { + const db = await $getClient({ + ...providerInputs, + port, + host, + hostIp: providerInputs.hostIp, + originalHost: providerInputs.host + }); // oracle needs from keyword const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1"; @@ -251,7 +308,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements); const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { - const db = await $getClient({ ...providerInputs, port, host }); + const db = await $getClient({ + ...providerInputs, + port, + host, + originalHost: providerInputs.host + }); try { const expiration = new Date(expireAt).toISOString(); @@ -294,7 +356,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const username = entityId; const { database } = providerInputs; const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { - const db = await $getClient({ ...providerInputs, port, host }); + const db = await $getClient({ + ...providerInputs, + port, + host, + originalHost: providerInputs.host + }); try { const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database }); const queries = revokeStatement.toString().split(";").filter(Boolean); @@ -329,7 +396,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) if (!providerInputs.renewStatement) return { entityId }; const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { - const db = await $getClient({ ...providerInputs, port, host }); + const db = await $getClient({ + ...providerInputs, + port, + host, + originalHost: providerInputs.host + }); const expiration = new Date(expireAt).toISOString(); const { database } = providerInputs; diff --git a/backend/src/ee/services/event/event-bus-service.ts b/backend/src/ee/services/event/event-bus-service.ts index bb102c721..00fd14e9f 100644 --- a/backend/src/ee/services/event/event-bus-service.ts +++ b/backend/src/ee/services/event/event-bus-service.ts @@ -1,11 +1,11 @@ -import Redis from "ioredis"; +import { Cluster, Redis } from "ioredis"; import { z } from "zod"; import { logger } from "@app/lib/logger"; import { BusEventSchema, TopicName } from "./types"; -export const eventBusFactory = (redis: Redis) => { +export const eventBusFactory = (redis: Redis | Cluster) => { const publisher = redis.duplicate(); // Duplicate the publisher to create a subscriber. // This is necessary because Redis does not allow a single connection to both publish and subscribe. diff --git a/backend/src/ee/services/event/event-sse-service.ts b/backend/src/ee/services/event/event-sse-service.ts index dc52cc14c..147af8e3c 100644 --- a/backend/src/ee/services/event/event-sse-service.ts +++ b/backend/src/ee/services/event/event-sse-service.ts @@ -1,6 +1,6 @@ /* eslint-disable no-continue */ import { subject } from "@casl/ability"; -import Redis from "ioredis"; +import { Cluster, Redis } from "ioredis"; import { KeyStorePrefixes } from "@app/keystore/keystore"; import { logger } from "@app/lib/logger"; @@ -12,7 +12,7 @@ import { BusEvent, RegisteredEvent } from "./types"; const AUTH_REFRESH_INTERVAL = 60 * 1000; const HEART_BEAT_INTERVAL = 15 * 1000; -export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => { +export const sseServiceFactory = (bus: TEventBusService, redis: Redis | Cluster) => { const clients = new Set(); const heartbeatInterval = setInterval(() => { diff --git a/backend/src/ee/services/event/event-sse-stream.ts b/backend/src/ee/services/event/event-sse-stream.ts index 13e18374f..9ed5e677c 100644 --- a/backend/src/ee/services/event/event-sse-stream.ts +++ b/backend/src/ee/services/event/event-sse-stream.ts @@ -3,7 +3,7 @@ import { Readable } from "node:stream"; import { MongoAbility, PureAbility } from "@casl/ability"; import { MongoQuery } from "@ucast/mongo2js"; -import Redis from "ioredis"; +import { Cluster, Redis } from "ioredis"; import { nanoid } from "nanoid"; import { ProjectType } from "@app/db/schemas"; @@ -65,7 +65,7 @@ export type EventStreamClient = { matcher: PureAbility; }; -export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient { +export function createEventStreamClient(redis: Redis | Cluster, options: IEventStreamClientOpts): EventStreamClient { const rules = options.registered.map((r) => { const secretPath = r.conditions?.secretPath; const hasConditions = r.conditions?.environmentSlug || r.conditions?.secretPath; diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts b/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts new file mode 100644 index 000000000..e67d4e890 --- /dev/null +++ b/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts @@ -0,0 +1,2 @@ +export const GATEWAY_ROUTING_INFO_OID = "1.3.6.1.4.1.12345.100.1"; +export const GATEWAY_ACTOR_OID = "1.3.6.1.4.1.12345.100.2"; diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts b/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts new file mode 100644 index 000000000..da9d3c1ef --- /dev/null +++ b/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts @@ -0,0 +1,60 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { GatewaysV2Schema, TableName, TGatewaysV2 } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex"; + +export type TGatewayV2DALFactory = ReturnType; + +export const gatewayV2DalFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.GatewayV2); + + const find = async (filter: TFindFilter, { offset, limit, sort, tx }: TFindOpt = {}) => { + try { + const query = (tx || db.replicaNode())(TableName.GatewayV2) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .where(buildFindFilter(filter, TableName.GatewayV2)) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.GatewayV2}.identityId`) + .join( + TableName.IdentityOrgMembership, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.GatewayV2}.identityId` + ) + .select(selectAllTableCols(TableName.GatewayV2)) + .select(db.ref("name").withSchema(TableName.Identity).as("identityName")); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = await query; + + return docs.map((el) => ({ + ...GatewaysV2Schema.parse(el), + identity: { id: el.identityId, name: el.identityName } + })); + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.GatewayV2}: Find` }); + } + }; + + const findById = async (id: string, tx?: Knex) => { + try { + const doc = await (tx || db.replicaNode())(TableName.GatewayV2) + .join(TableName.Organization, `${TableName.GatewayV2}.orgId`, `${TableName.Organization}.id`) + .where(`${TableName.GatewayV2}.id`, id) + .select(selectAllTableCols(TableName.GatewayV2)) + .select(db.ref("name").withSchema(TableName.Organization).as("orgName")) + .first(); + + return doc; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.GatewayV2}: Find by id` }); + } + }; + + return { ...orm, find, findById }; +}; diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts new file mode 100644 index 000000000..317e5da6d --- /dev/null +++ b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts @@ -0,0 +1,656 @@ +import net from "node:net"; + +import { ForbiddenError } from "@casl/ability"; +import * as x509 from "@peculiar/x509"; + +import { TRelays } from "@app/db/schemas"; +import { PgSqlLock } from "@app/keystore/keystore"; +import { crypto } from "@app/lib/crypto"; +import { DatabaseErrorCode } from "@app/lib/error-codes"; +import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; +import { GatewayProxyProtocol } from "@app/lib/gateway/types"; +import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; +import { OrgServiceActor } from "@app/lib/types"; +import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; +import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { + createSerialNumber, + keyAlgorithmToAlgCfg +} from "@app/services/certificate-authority/certificate-authority-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { TLicenseServiceFactory } from "../license/license-service"; +import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; +import { TRelayDALFactory } from "../relay/relay-dal"; +import { TRelayServiceFactory } from "../relay/relay-service"; +import { GATEWAY_ACTOR_OID, GATEWAY_ROUTING_INFO_OID } from "./gateway-v2-constants"; +import { TGatewayV2DALFactory } from "./gateway-v2-dal"; +import { TOrgGatewayConfigV2DALFactory } from "./org-gateway-config-v2-dal"; + +type TGatewayV2ServiceFactoryDep = { + orgGatewayConfigV2DAL: Pick; + licenseService: Pick; + kmsService: TKmsServiceFactory; + relayService: TRelayServiceFactory; + gatewayV2DAL: TGatewayV2DALFactory; + relayDAL: TRelayDALFactory; + permissionService: TPermissionServiceFactory; +}; + +export type TGatewayV2ServiceFactory = ReturnType; + +export const gatewayV2ServiceFactory = ({ + orgGatewayConfigV2DAL, + licenseService, + kmsService, + relayService, + gatewayV2DAL, + relayDAL, + permissionService +}: TGatewayV2ServiceFactoryDep) => { + const $validateIdentityAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => { + const orgLicensePlan = await licenseService.getPlan(orgId); + if (!orgLicensePlan.gateway) { + throw new BadRequestError({ + message: + "Gateway operation failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan." + }); + } + + const { permission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + actorId, + orgId, + actorAuthMethod, + orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.CreateGateways, + OrgPermissionSubjects.Gateway + ); + }; + + const $getOrgCAs = async (orgId: string) => { + const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const orgCAs = await orgGatewayConfigV2DAL.transaction(async (tx) => { + const orgGatewayConfigV2 = await orgGatewayConfigV2DAL.findOne({ orgId }); + if (orgGatewayConfigV2) return orgGatewayConfigV2; + + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayV2Init(orgId)]); + + // generate root CA + const rootCaKeyAlgorithm = CertKeyAlgorithm.RSA_2048; + const alg = keyAlgorithmToAlgCfg(rootCaKeyAlgorithm); + const rootCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + const rootCaSerialNumber = createSerialNumber(); + const rootCaSkObj = crypto.nativeCrypto.KeyObject.from(rootCaKeys.privateKey); + const rootCaIssuedAt = new Date(); + const rootCaExpiration = new Date(new Date().setFullYear(2045)); + + const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({ + name: `O=${orgId},CN=Infisical Gateway Root CA`, + serialNumber: rootCaSerialNumber, + notBefore: rootCaIssuedAt, + notAfter: rootCaExpiration, + signingAlgorithm: alg, + keys: rootCaKeys, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), + await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey) + ] + }); + + // generate server CA + const serverCaSerialNumber = createSerialNumber(); + const serverCaIssuedAt = new Date(); + const serverCaExpiration = new Date(new Date().setFullYear(2045)); + const serverCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const serverCaSkObj = crypto.nativeCrypto.KeyObject.from(serverCaKeys.privateKey); + const serverCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: serverCaSerialNumber, + subject: `O=${orgId},CN=Infisical Gateway Server CA`, + issuer: rootCaCert.subject, + notBefore: serverCaIssuedAt, + notAfter: serverCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: serverCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(serverCaKeys.publicKey) + ] + }); + + // generate client CA + const clientCaSerialNumber = createSerialNumber(); + const clientCaIssuedAt = new Date(); + const clientCaExpiration = new Date(new Date().setFullYear(2045)); + const clientCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const clientCaSkObj = crypto.nativeCrypto.KeyObject.from(clientCaKeys.privateKey); + const clientCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientCaSerialNumber, + subject: `O=${orgId},CN=Infisical Gateway Client CA`, + issuer: rootCaCert.subject, + notBefore: clientCaIssuedAt, + notAfter: clientCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: clientCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(clientCaKeys.publicKey) + ] + }); + + const encryptedRootGatewayCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from( + rootCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + }).cipherTextBlob; + const encryptedRootGatewayCaCertificate = orgKmsEncryptor({ + plainText: Buffer.from(rootCaCert.rawData) + }).cipherTextBlob; + + const encryptedGatewayServerCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from(serverCaSkObj.export({ type: "pkcs8", format: "der" })) + }).cipherTextBlob; + const encryptedGatewayServerCaCertificate = orgKmsEncryptor({ + plainText: Buffer.from(serverCaCert.rawData) + }).cipherTextBlob; + const encryptedGatewayServerCaCertificateChain = orgKmsEncryptor({ + plainText: Buffer.from(constructPemChainFromCerts([rootCaCert])) + }).cipherTextBlob; + + const encryptedGatewayClientCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from(clientCaSkObj.export({ type: "pkcs8", format: "der" })) + }).cipherTextBlob; + const encryptedGatewayClientCaCertificate = orgKmsEncryptor({ + plainText: Buffer.from(clientCaCert.rawData) + }).cipherTextBlob; + const encryptedGatewayClientCaCertificateChain = orgKmsEncryptor({ + plainText: Buffer.from(constructPemChainFromCerts([rootCaCert])) + }).cipherTextBlob; + + return orgGatewayConfigV2DAL.create({ + orgId, + encryptedRootGatewayCaPrivateKey, + encryptedRootGatewayCaCertificate, + encryptedGatewayServerCaPrivateKey, + encryptedGatewayServerCaCertificate, + encryptedGatewayServerCaCertificateChain, + encryptedGatewayClientCaPrivateKey, + encryptedGatewayClientCaCertificate, + encryptedGatewayClientCaCertificateChain + }); + }); + + const rootGatewayCaPrivateKey = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedRootGatewayCaPrivateKey }); + const rootGatewayCaCertificate = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedRootGatewayCaCertificate }); + + const gatewayServerCaPrivateKey = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedGatewayServerCaPrivateKey }); + const gatewayServerCaCertificate = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedGatewayServerCaCertificate }); + const gatewayServerCaCertificateChain = orgKmsDecryptor({ + cipherTextBlob: orgCAs.encryptedGatewayServerCaCertificateChain + }); + + const gatewayClientCaPrivateKey = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedGatewayClientCaPrivateKey }); + const gatewayClientCaCertificate = orgKmsDecryptor({ + cipherTextBlob: orgCAs.encryptedGatewayClientCaCertificate + }); + const gatewayClientCaCertificateChain = orgKmsDecryptor({ + cipherTextBlob: orgCAs.encryptedGatewayClientCaCertificateChain + }); + + return { + rootGatewayCaPrivateKey, + rootGatewayCaCertificate, + gatewayServerCaPrivateKey, + gatewayServerCaCertificate, + gatewayServerCaCertificateChain, + gatewayClientCaPrivateKey, + gatewayClientCaCertificate, + gatewayClientCaCertificateChain + }; + }; + + const listGateways = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.ListGateways, + OrgPermissionSubjects.Gateway + ); + + const gateways = await gatewayV2DAL.find({ + orgId: orgPermission.orgId + }); + + return gateways; + }; + + const getPlatformConnectionDetailsByGatewayId = async ({ + gatewayId, + targetHost, + targetPort + }: { + gatewayId: string; + targetHost: string; + targetPort: number; + }) => { + const gateway = await gatewayV2DAL.findById(gatewayId); + if (!gateway) { + return; + } + + const orgGatewayConfig = await orgGatewayConfigV2DAL.findOne({ orgId: gateway.orgId }); + if (!orgGatewayConfig) { + throw new NotFoundError({ message: `Gateway Config for org ${gateway.orgId} not found.` }); + } + + if (!gateway.relayId) { + throw new BadRequestError({ + message: "Gateway is not associated with a relay" + }); + } + + const orgLicensePlan = await licenseService.getPlan(orgGatewayConfig.orgId); + if (!orgLicensePlan.gateway) { + throw new BadRequestError({ + message: "Please upgrade your instance to Infisical's Enterprise plan to use gateways." + }); + } + + const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: orgGatewayConfig.orgId + }); + + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + + const rootGatewayCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedRootGatewayCaCertificate + }) + ); + + const gatewayClientCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedGatewayClientCaCertificate + }) + ); + + const gatewayServerCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedGatewayServerCaCertificate + }) + ); + + const gatewayClientCaPrivateKey = orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedGatewayClientCaPrivateKey + }); + + const gatewayClientCaSkObj = crypto.nativeCrypto.createPrivateKey({ + key: gatewayClientCaPrivateKey, + format: "der", + type: "pkcs8" + }); + + const importedGatewayClientCaPrivateKey = await crypto.nativeCrypto.subtle.importKey( + "pkcs8", + gatewayClientCaSkObj.export({ format: "der", type: "pkcs8" }), + alg, + true, + ["sign"] + ); + + const clientCertIssuedAt = new Date(); + const clientCertExpiration = new Date(new Date().getTime() + 5 * 60 * 1000); + const clientKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const clientCertSerialNumber = createSerialNumber(); + + const routingInfo = { + targetHost, + targetPort + }; + + const routingExtension = new x509.Extension( + GATEWAY_ROUTING_INFO_OID, + false, + Buffer.from(JSON.stringify(routingInfo)) + ); + + const actorExtension = new x509.Extension( + GATEWAY_ACTOR_OID, + false, + Buffer.from(JSON.stringify({ type: ActorType.PLATFORM })) + ); + + const clientCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientCertSerialNumber, + subject: `O=${orgGatewayConfig.orgId},OU=gateway-client,CN=${ActorType.PLATFORM}:${gatewayId}`, + issuer: gatewayClientCaCert.subject, + notAfter: clientCertExpiration, + notBefore: clientCertIssuedAt, + signingKey: importedGatewayClientCaPrivateKey, + publicKey: clientKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(gatewayClientCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(clientKeys.publicKey), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | + x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] | + x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true), + routingExtension, + actorExtension + ] + }); + + const gatewayClientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey); + + const relayCredentials = await relayService.getCredentialsForClient({ + relayId: gateway.relayId, + orgId: gateway.orgId, + orgName: gateway.orgName, + gatewayId + }); + + return { + relayHost: relayCredentials.relayHost, + gateway: { + clientCertificate: clientCert.toString("pem"), + clientPrivateKey: gatewayClientCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), + serverCertificateChain: constructPemChainFromCerts([gatewayServerCaCert, rootGatewayCaCert]) + }, + relay: { + clientCertificate: relayCredentials.clientCertificate, + clientPrivateKey: relayCredentials.clientPrivateKey, + serverCertificateChain: relayCredentials.serverCertificateChain + } + }; + }; + + const registerGateway = async ({ + orgId, + actorId, + actorAuthMethod, + relayName, + name + }: { + orgId: string; + actorId: string; + actorAuthMethod: ActorAuthMethod; + relayName: string; + name: string; + }) => { + await $validateIdentityAccessToGateway(orgId, actorId, actorAuthMethod); + const orgCAs = await $getOrgCAs(orgId); + + let relay: TRelays = await relayDAL.findOne({ orgId, name: relayName }); + if (!relay) { + relay = await relayDAL.findOne({ name: relayName, orgId: null }); + } + + if (!relay) { + throw new NotFoundError({ message: `Relay ${relayName} not found` }); + } + + try { + const [gateway] = await gatewayV2DAL.upsert( + [ + { + orgId, + name, + identityId: actorId, + relayId: relay.id + } + ], + ["identityId"] + ); + + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + const gatewayServerCaCert = new x509.X509Certificate(orgCAs.gatewayServerCaCertificate); + const rootGatewayCaCert = new x509.X509Certificate(orgCAs.rootGatewayCaCertificate); + const gatewayClientCaCert = new x509.X509Certificate(orgCAs.gatewayClientCaCertificate); + + const gatewayServerCaSkObj = crypto.nativeCrypto.createPrivateKey({ + key: orgCAs.gatewayServerCaPrivateKey, + format: "der", + type: "pkcs8" + }); + const gatewayServerCaPrivateKey = await crypto.nativeCrypto.subtle.importKey( + "pkcs8", + gatewayServerCaSkObj.export({ format: "der", type: "pkcs8" }), + alg, + true, + ["sign"] + ); + + const gatewayServerKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const gatewayServerCertIssuedAt = new Date(); + const gatewayServerCertExpireAt = new Date(new Date().setDate(new Date().getDate() + 1)); + const gatewayServerCertPrivateKey = crypto.nativeCrypto.KeyObject.from(gatewayServerKeys.privateKey); + + const gatewayServerCertExtensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(gatewayServerCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(gatewayServerKeys.publicKey), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true), + new x509.SubjectAlternativeNameExtension([ + { type: "dns", value: "localhost" }, + { type: "ip", value: "127.0.0.1" }, + { type: "ip", value: "::1" } + ]) + ]; + + const gatewayServerSerialNumber = createSerialNumber(); + const gatewayServerCertificate = await x509.X509CertificateGenerator.create({ + serialNumber: gatewayServerSerialNumber, + subject: `O=${orgId},CN=Gateway`, + issuer: gatewayServerCaCert.subject, + notBefore: gatewayServerCertIssuedAt, + notAfter: gatewayServerCertExpireAt, + signingKey: gatewayServerCaPrivateKey, + publicKey: gatewayServerKeys.publicKey, + signingAlgorithm: alg, + extensions: gatewayServerCertExtensions + }); + + const relayCredentials = await relayService.getCredentialsForGateway({ + relayName, + orgId, + gatewayId: gateway.id + }); + + return { + gatewayId: gateway.id, + relayHost: relayCredentials.relayHost, + pki: { + serverCertificate: gatewayServerCertificate.toString("pem"), + serverPrivateKey: gatewayServerCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), + clientCertificateChain: constructPemChainFromCerts([gatewayClientCaCert, rootGatewayCaCert]) + }, + ssh: { + clientCertificate: relayCredentials.clientSshCert, + clientPrivateKey: relayCredentials.clientSshPrivateKey, + serverCAPublicKey: relayCredentials.serverCAPublicKey + } + }; + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ message: `Gateway with name "${name}" already exists` }); + } + + throw err; + } + }; + + const heartbeat = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => { + await $validateIdentityAccessToGateway(orgPermission.orgId, orgPermission.id, orgPermission.authMethod); + + const gateway = await gatewayV2DAL.findOne({ + orgId: orgPermission.orgId, + identityId: orgPermission.id + }); + + if (!gateway) { + throw new NotFoundError({ message: `Gateway for identity ${orgPermission.id} not found.` }); + } + + const gatewayV2ConnectionDetails = await getPlatformConnectionDetailsByGatewayId({ + gatewayId: gateway.id, + targetHost: "health-check", + targetPort: 443 + }); + + if (!gatewayV2ConnectionDetails) { + throw new NotFoundError({ message: `Gateway connection details for gateway ${gateway.id} not found.` }); + } + + const isGatewayReachable = await withGatewayV2Proxy( + async (port) => { + return new Promise((resolve, reject) => { + const socket = new net.Socket(); + let responseReceived = false; + let isResolved = false; + + // Set socket timeout + socket.setTimeout(10000); + + const cleanup = () => { + if (!socket.destroyed) { + socket.destroy(); + } + }; + + socket.on("data", (data: Buffer) => { + const response = data.toString().trim(); + if (response === "PONG" && !isResolved) { + isResolved = true; + responseReceived = true; + cleanup(); + resolve(true); + } + }); + + socket.on("error", (err: Error) => { + if (!isResolved) { + isResolved = true; + cleanup(); + reject(new Error(`TCP connection error: ${err.message}`)); + } + }); + + socket.on("timeout", () => { + if (!isResolved) { + isResolved = true; + cleanup(); + reject(new Error("TCP connection timeout")); + } + }); + + socket.on("close", () => { + if (!isResolved && !responseReceived) { + isResolved = true; + cleanup(); + reject(new Error("Connection closed without receiving PONG")); + } + }); + + socket.connect(port, "localhost"); + }); + }, + { + protocol: GatewayProxyProtocol.Ping, + relayHost: gatewayV2ConnectionDetails.relayHost, + gateway: gatewayV2ConnectionDetails.gateway, + relay: gatewayV2ConnectionDetails.relay + } + ); + + if (!isGatewayReachable) { + throw new BadRequestError({ message: `Gateway ${gateway.id} is not reachable` }); + } + + await gatewayV2DAL.updateById(gateway.id, { heartbeat: new Date() }); + }; + + const deleteGatewayById = async ({ orgPermission, id }: { orgPermission: OrgServiceActor; id: string }) => { + const gateway = await gatewayV2DAL.findOne({ id, orgId: orgPermission.orgId }); + if (!gateway) { + throw new NotFoundError({ message: `Gateway ${id} not found` }); + } + + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + gateway.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.DeleteGateways, + OrgPermissionSubjects.Gateway + ); + + return gatewayV2DAL.deleteById(gateway.id); + }; + + return { + listGateways, + registerGateway, + getPlatformConnectionDetailsByGatewayId, + deleteGatewayById, + heartbeat + }; +}; diff --git a/backend/src/ee/services/gateway-v2/org-gateway-config-v2-dal.ts b/backend/src/ee/services/gateway-v2/org-gateway-config-v2-dal.ts new file mode 100644 index 000000000..8f16d798a --- /dev/null +++ b/backend/src/ee/services/gateway-v2/org-gateway-config-v2-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TOrgGatewayConfigV2DALFactory = ReturnType; + +export const orgGatewayConfigV2DalFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.OrgGatewayConfigV2); + + return orm; +}; diff --git a/backend/src/ee/services/gateway/gateway-dal.ts b/backend/src/ee/services/gateway/gateway-dal.ts index 31b4b727b..c21ff31c0 100644 --- a/backend/src/ee/services/gateway/gateway-dal.ts +++ b/backend/src/ee/services/gateway/gateway-dal.ts @@ -13,7 +13,7 @@ export const gatewayDALFactory = (db: TDbClient) => { { offset, limit, sort, tx }: TFindOpt = {} ) => { try { - const query = (tx || db)(TableName.Gateway) + const query = (tx || db.replicaNode())(TableName.Gateway) // eslint-disable-next-line @typescript-eslint/no-misused-promises .where(buildFindFilter(filter, TableName.Gateway, ["orgId"])) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) diff --git a/backend/src/ee/services/group/user-group-membership-dal.ts b/backend/src/ee/services/group/user-group-membership-dal.ts index 5ee97e457..374459b0c 100644 --- a/backend/src/ee/services/group/user-group-membership-dal.ts +++ b/backend/src/ee/services/group/user-group-membership-dal.ts @@ -23,7 +23,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { .whereIn(`${TableName.ProjectMembership}.projectId`, projectIds) .pluck(`${TableName.ProjectMembership}.projectId`); - const userGroupMemberships: string[] = await (tx || db)(TableName.UserGroupMembership) + const userGroupMemberships: string[] = await (tx || db.replicaNode())(TableName.UserGroupMembership) .where(`${TableName.UserGroupMembership}.userId`, userId) .whereNot(`${TableName.UserGroupMembership}.groupId`, groupId) .join( @@ -79,7 +79,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { .pluck(`${TableName.GroupProjectMembership}.groupId`); // main query - const members = await (tx || db)(TableName.UserGroupMembership) + const members = await (tx || db.replicaNode())(TableName.UserGroupMembership) .where(`${TableName.UserGroupMembership}.groupId`, groupId) .where(`${TableName.UserGroupMembership}.isPending`, false) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index a72d50760..8643ecdac 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -5,6 +5,7 @@ import { OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; +import { throwOnPlanSeatLimitReached } from "@app/ee/services/license/license-fns"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -127,6 +128,20 @@ export const ldapConfigServiceFactory = ({ message: "Failed to create LDAP configuration due to plan restriction. Upgrade plan to create LDAP configuration." }); + + const org = await orgDAL.findOrgById(orgId); + + if (!org) { + throw new NotFoundError({ message: `Could not find organization with ID "${orgId}"` }); + } + + if (org.googleSsoAuthEnforced && isActive) { + throw new BadRequestError({ + message: + "You cannot enable LDAP SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable LDAP SSO." + }); + } + const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId @@ -233,6 +248,19 @@ export const ldapConfigServiceFactory = ({ "Failed to update LDAP configuration due to plan restriction. Upgrade plan to update LDAP configuration." }); + const org = await orgDAL.findOrgById(orgId); + + if (!org) { + throw new NotFoundError({ message: `Could not find organization with ID "${orgId}"` }); + } + + if (org.googleSsoAuthEnforced && isActive) { + throw new BadRequestError({ + message: + "You cannot enable LDAP SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable LDAP SSO." + }); + } + const updateQuery: TLdapConfigsUpdate = { isActive, url, @@ -390,14 +418,6 @@ export const ldapConfigServiceFactory = ({ } }); } else { - const plan = await licenseService.getPlan(orgId); - if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { - // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed - throw new BadRequestError({ - message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members." - }); - } - userAlias = await userDAL.transaction(async (tx) => { let newUser: TUsers | undefined; newUser = await userDAL.findOne( @@ -446,6 +466,8 @@ export const ldapConfigServiceFactory = ({ ); if (!orgMembership) { + await throwOnPlanSeatLimitReached(licenseService, orgId, UserAliasType.LDAP); + const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); await orgMembershipDAL.create( diff --git a/backend/src/ee/services/license/license-dal.ts b/backend/src/ee/services/license/license-dal.ts index 88a2dadf6..cfea2573d 100644 --- a/backend/src/ee/services/license/license-dal.ts +++ b/backend/src/ee/services/license/license-dal.ts @@ -28,7 +28,7 @@ export const licenseDALFactory = (db: TDbClient) => { const countOrgUsersAndIdentities = async (orgId: string | null, tx?: Knex) => { try { // count org users - const userDoc = await (tx || db)(TableName.OrgMembership) + const userDoc = await (tx || db.replicaNode())(TableName.OrgMembership) .where({ status: OrgMembershipStatus.Accepted }) .andWhere((bd) => { if (orgId) { @@ -42,7 +42,7 @@ export const licenseDALFactory = (db: TDbClient) => { const userCount = Number(userDoc?.[0].count); // count org identities - const identityDoc = await (tx || db)(TableName.IdentityOrgMembership) + const identityDoc = await (tx || db.replicaNode())(TableName.IdentityOrgMembership) .where((bd) => { if (orgId) { void bd.where({ orgId }); diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 8d2d6fdbe..a302e956a 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -1,8 +1,11 @@ import axios, { AxiosError } from "axios"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { UserAliasType } from "@app/services/user-alias/user-alias-types"; import { TFeatureSet } from "./license-types"; @@ -133,3 +136,18 @@ export const setupLicenseRequestWithStore = ( return { request: licenseReq, refreshLicense }; }; + +export const throwOnPlanSeatLimitReached = async ( + licenseService: Pick, + orgId: string, + type?: UserAliasType +) => { + const plan = await licenseService.getPlan(orgId); + + if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { + // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed + throw new BadRequestError({ + message: `Failed to create new member${type ? ` via ${type.toUpperCase()}` : ""} due to member limit reached. Upgrade plan to add more members.` + }); + } +}; diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 891fa208f..a5353b7e5 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -99,6 +99,17 @@ export const licenseServiceFactory = ({ const workspacesUsed = await projectDAL.countOfOrgProjects(null); currentPlan.workspacesUsed = workspacesUsed; + const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null); + if (usedIdentitySeats !== currentPlan.identitiesUsed) { + const usedSeats = await licenseDAL.countOfOrgMembers(null); + await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, { + usedSeats, + usedIdentitySeats + }); + currentPlan.identitiesUsed = usedIdentitySeats; + currentPlan.membersUsed = usedSeats; + } + onPremFeatures = currentPlan; logger.info("Successfully synchronized license key features"); } catch (error) { @@ -226,10 +237,13 @@ export const licenseServiceFactory = ({ }; const refreshPlan = async (orgId: string) => { + await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); if (instanceType === InstanceType.Cloud) { - await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); await getPlan(orgId); } + if (instanceType === InstanceType.EnterpriseOnPrem) { + await syncLicenseKeyOnPremFeatures(true); + } }; const generateOrgCustomerId = async (orgName: string, email?: string | null) => { @@ -296,8 +310,19 @@ export const licenseServiceFactory = ({ return data; }; - const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => { + const getOrgPlan = async ({ + orgId, + actor, + actorId, + actorOrgId, + actorAuthMethod, + projectId, + refreshCache + }: TOrgPlanDTO) => { await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + if (refreshCache) { + await refreshPlan(orgId); + } const plan = await getPlan(orgId, projectId); return plan; }; @@ -323,6 +348,8 @@ export const licenseServiceFactory = ({ }); } + await updateSubscriptionOrgMemberCount(orgId); + const { data: { url } } = await licenseServerCloudApi.request.post( @@ -415,6 +442,62 @@ export const licenseServiceFactory = ({ }; }; + const calculateUsageValue = ( + rowName: string, + field: string, + projectCount: number, + totalIdentities: number + ): string => { + if (rowName === BillingPlanRows.WorkspaceLimit.name || field === BillingPlanRows.WorkspaceLimit.field) { + return projectCount.toString(); + } + if (rowName === BillingPlanRows.IdentityLimit.name || field === BillingPlanRows.IdentityLimit.field) { + return totalIdentities.toString(); + } + return "-"; + }; + + const fetchPlanTableFromServer = async (customerId: string | null | undefined) => { + if (!customerId) { + throw new NotFoundError({ message: "Organization customer ID is required for plan table retrieval" }); + } + + const baseUrl = `/api/license-server/v1/customers/${customerId}`; + + if (instanceType === InstanceType.Cloud) { + const { data } = await licenseServerCloudApi.request.get<{ + head: { name: string }[]; + rows: { name: string; allowed: boolean }[]; + }>(`${baseUrl}/cloud-plan/table`); + return data; + } + + if (instanceType === InstanceType.EnterpriseOnPrem) { + const { data } = await licenseServerOnPremApi.request.get<{ + head: { name: string }[]; + rows: { name: string; allowed: boolean }[]; + }>(`${baseUrl}/on-prem-plan/table`); + return data; + } + + throw new Error(`Unsupported instance type for server-based plan table: ${instanceType}`); + }; + + const getUsageMetrics = async (orgId: string) => { + const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([ + orgDAL.countAllOrgMembers(orgId), + identityOrgMembershipDAL.countAllOrgIdentities({ orgId }), + projectDAL.countOfOrgProjects(orgId) + ]); + + return { + orgMembersUsed, + identityUsed, + projectCount, + totalIdentities: identityUsed + orgMembersUsed + }; + }; + // returns org current plan feature table const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); @@ -427,55 +510,25 @@ export const licenseServiceFactory = ({ }); } - const orgMembersUsed = await orgDAL.countAllOrgMembers(orgId); - const identityUsed = await identityOrgMembershipDAL.countAllOrgIdentities({ orgId }); - const projects = await projectDAL.find({ orgId }); - const projectCount = projects.length; + const { projectCount, totalIdentities } = await getUsageMetrics(orgId); - if (instanceType === InstanceType.Cloud) { - const { data } = await licenseServerCloudApi.request.get<{ - head: { name: string }[]; - rows: { name: string; allowed: boolean }[]; - }>(`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table`); + if (instanceType === InstanceType.Cloud || instanceType === InstanceType.EnterpriseOnPrem) { + const tableResponse = await fetchPlanTableFromServer(organization.customerId); - const formattedData = { - head: data.head, - rows: data.rows.map((el) => { - let used = "-"; - - if (el.name === BillingPlanRows.WorkspaceLimit.name) { - used = projectCount.toString(); - } else if (el.name === BillingPlanRows.IdentityLimit.name) { - used = (identityUsed + orgMembersUsed).toString(); - } - - return { - ...el, - used - }; - }) + return { + head: tableResponse.head, + rows: tableResponse.rows.map((row) => ({ + ...row, + used: calculateUsageValue(row.name, "", projectCount, totalIdentities) + })) }; - return formattedData; } - const mappedRows = await Promise.all( - Object.values(BillingPlanRows).map(async ({ name, field }: { name: string; field: string }) => { - const allowed = onPremFeatures[field as keyof TFeatureSet]; - let used = "-"; - - if (field === BillingPlanRows.WorkspaceLimit.field) { - used = projectCount.toString(); - } else if (field === BillingPlanRows.IdentityLimit.field) { - used = (identityUsed + orgMembersUsed).toString(); - } - - return { - name, - allowed, - used - }; - }) - ); + const mappedRows = Object.values(BillingPlanRows).map(({ name, field }) => ({ + name, + allowed: onPremFeatures[field as keyof TFeatureSet] || false, + used: calculateUsageValue(name, field, projectCount, totalIdentities) + })); return { head: Object.values(BillingPlanTableHead), @@ -722,6 +775,16 @@ export const licenseServiceFactory = ({ await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); }; + const getCustomerId = () => { + if (!selfHostedLicense) return "unknown"; + return selfHostedLicense?.customerId; + }; + + const getLicenseId = () => { + if (!selfHostedLicense) return "unknown"; + return selfHostedLicense?.licenseId; + }; + return { generateOrgCustomerId, removeOrgCustomer, @@ -736,6 +799,8 @@ export const licenseServiceFactory = ({ return onPremFeatures; }, getPlan, + getCustomerId, + getLicenseId, invalidateGetPlan, updateSubscriptionOrgMemberCount, refreshPlan, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 345e26638..2ccd3ac8f 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -87,6 +87,7 @@ export type TOrgPlansTableDTO = { export type TOrgPlanDTO = { projectId?: string; + refreshCache?: boolean; } & TOrgPermission; export type TStartOrgTrialDTO = { diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 8f479b12c..445ace2b5 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -8,6 +8,7 @@ import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/a import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; +import { throwOnPlanSeatLimitReached } from "@app/ee/services/license/license-fns"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -294,6 +295,8 @@ export const oidcConfigServiceFactory = ({ ); if (!orgMembership) { + await throwOnPlanSeatLimitReached(licenseService, orgId, UserAliasType.OIDC); + const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); await orgMembershipDAL.create( @@ -499,6 +502,13 @@ export const oidcConfigServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); + if (org.googleSsoAuthEnforced && isActive) { + throw new BadRequestError({ + message: + "You cannot enable OIDC SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable OIDC SSO." + }); + } + const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId: org.id @@ -586,6 +596,13 @@ export const oidcConfigServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); + if (org.googleSsoAuthEnforced && isActive) { + throw new BadRequestError({ + message: + "You cannot enable OIDC SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable OIDC SSO." + }); + } + const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId: org.id diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 9329c3c7f..953d0195e 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -2,6 +2,7 @@ import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability" import { ProjectPermissionActions, + ProjectPermissionAppConnectionActions, ProjectPermissionAuditLogsActions, ProjectPermissionCertificateActions, ProjectPermissionCmekActions, @@ -264,6 +265,17 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.SecretEvents ); + can( + [ + ProjectPermissionAppConnectionActions.Create, + ProjectPermissionAppConnectionActions.Edit, + ProjectPermissionAppConnectionActions.Delete, + ProjectPermissionAppConnectionActions.Read, + ProjectPermissionAppConnectionActions.Connect + ], + ProjectPermissionSub.AppConnections + ); + return rules; }; @@ -477,6 +489,8 @@ const buildMemberPermissionRules = () => { ProjectPermissionSub.SecretEvents ); + can(ProjectPermissionAppConnectionActions.Connect, ProjectPermissionSub.AppConnections); + return rules; }; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index d4155d02c..89a518032 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -87,6 +87,7 @@ export enum OrgPermissionBillingActions { export enum OrgPermissionSubjects { Workspace = "workspace", + Project = "project", Role = "role", Member = "member", Settings = "settings", @@ -117,6 +118,7 @@ export type AppConnectionSubjectFields = { export type OrgPermissionSet = | [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace] + | [OrgPermissionActions.Create, OrgPermissionSubjects.Project] | [OrgPermissionActions, OrgPermissionSubjects.Role] | [OrgPermissionActions, OrgPermissionSubjects.Member] | [OrgPermissionActions, OrgPermissionSubjects.Settings] @@ -166,6 +168,10 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [ subject: z.literal(OrgPermissionSubjects.Workspace).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_ENUM([OrgPermissionActions.Create]).describe("Describe what action an entity can take.") }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Project).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_ENUM([OrgPermissionActions.Create]).describe("Describe what action an entity can take.") + }), z.object({ subject: z.literal(OrgPermissionSubjects.Role).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") @@ -280,6 +286,7 @@ const buildAdminPermission = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); // ws permissions can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Project); // role permission can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Create, OrgPermissionSubjects.Role); @@ -413,6 +420,7 @@ const buildMemberPermission = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Project); can(OrgPermissionActions.Read, OrgPermissionSubjects.Member); can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 20b4344d3..099461f7f 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -147,6 +147,14 @@ export enum ProjectPermissionSecretScanningDataSourceActions { ReadResources = "read-data-source-resources" } +export enum ProjectPermissionAppConnectionActions { + Read = "read-app-connections", + Create = "create-app-connections", + Edit = "edit-app-connections", + Delete = "delete-app-connections", + Connect = "connect-app-connections" +} + export enum ProjectPermissionSecretScanningFindingActions { Read = "read-findings", Update = "update-findings" @@ -208,7 +216,8 @@ export enum ProjectPermissionSub { SecretScanningDataSources = "secret-scanning-data-sources", SecretScanningFindings = "secret-scanning-findings", SecretScanningConfigs = "secret-scanning-configs", - SecretEvents = "secret-events" + SecretEvents = "secret-events", + AppConnections = "app-connections" } export type SecretSubjectFields = { @@ -272,6 +281,10 @@ export type PkiSubscriberSubjectFields = { // (dangtony98): consider adding [commonName] as a subject field in the future }; +export type AppConnectionSubjectFields = { + connectionId: string; +}; + export type ProjectPermissionSet = | [ ProjectPermissionSecretActions, @@ -365,6 +378,13 @@ export type ProjectPermissionSet = | [ ProjectPermissionSecretEventActions, ProjectPermissionSub.SecretEvents | (ForcedSubject & SecretEventSubjectFields) + ] + | [ + ProjectPermissionAppConnectionActions, + ( + | ProjectPermissionSub.AppConnections + | (ForcedSubject & AppConnectionSubjectFields) + ) ]; const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; @@ -580,6 +600,21 @@ const PkiTemplateConditionSchema = z }) .partial(); +const AppConnectionConditionSchema = z + .object({ + connectionId: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + }) + .partial() + ]) + }) + .partial(); + const GeneralPermissionSchema = [ z.object({ subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), @@ -760,6 +795,16 @@ const GeneralPermissionSchema = [ action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretScanningConfigActions).describe( "Describe what action an entity can take." ) + }), + z.object({ + subject: z.literal(ProjectPermissionSub.AppConnections).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionAppConnectionActions).describe( + "Describe what action an entity can take." + ), + conditions: AppConnectionConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() }) ]; diff --git a/backend/src/ee/services/relay/instance-relay-config-dal.ts b/backend/src/ee/services/relay/instance-relay-config-dal.ts new file mode 100644 index 000000000..6db3b93e7 --- /dev/null +++ b/backend/src/ee/services/relay/instance-relay-config-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TInstanceRelayConfigDALFactory = ReturnType; + +export const instanceRelayConfigDalFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.InstanceRelayConfig); + + return orm; +}; diff --git a/backend/src/ee/services/relay/org-relay-config-dal.ts b/backend/src/ee/services/relay/org-relay-config-dal.ts new file mode 100644 index 000000000..7da35b9dc --- /dev/null +++ b/backend/src/ee/services/relay/org-relay-config-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TOrgRelayConfigDALFactory = ReturnType; + +export const orgRelayConfigDalFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.OrgRelayConfig); + + return orm; +}; diff --git a/backend/src/ee/services/relay/relay-dal.ts b/backend/src/ee/services/relay/relay-dal.ts new file mode 100644 index 000000000..9107e0807 --- /dev/null +++ b/backend/src/ee/services/relay/relay-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TRelayDALFactory = ReturnType; + +export const relayDalFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Relay); + + return orm; +}; diff --git a/backend/src/ee/services/relay/relay-service.ts b/backend/src/ee/services/relay/relay-service.ts new file mode 100644 index 000000000..92401faaf --- /dev/null +++ b/backend/src/ee/services/relay/relay-service.ts @@ -0,0 +1,1003 @@ +import * as x509 from "@peculiar/x509"; + +import { TRelays } from "@app/db/schemas"; +import { PgSqlLock } from "@app/keystore/keystore"; +import { crypto } from "@app/lib/crypto"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { + createSerialNumber, + keyAlgorithmToAlgCfg +} from "@app/services/certificate-authority/certificate-authority-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { verifyHostInputValidity } from "../dynamic-secret/dynamic-secret-fns"; +import { createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns"; +import { SshCertType } from "../ssh/ssh-certificate-authority-types"; +import { SshCertKeyAlgorithm } from "../ssh-certificate/ssh-certificate-types"; +import { TInstanceRelayConfigDALFactory } from "./instance-relay-config-dal"; +import { TOrgRelayConfigDALFactory } from "./org-relay-config-dal"; +import { TRelayDALFactory } from "./relay-dal"; + +export type TRelayServiceFactory = ReturnType; + +const INSTANCE_RELAY_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; + +export const relayServiceFactory = ({ + instanceRelayConfigDAL, + orgRelayConfigDAL, + relayDAL, + kmsService +}: { + instanceRelayConfigDAL: TInstanceRelayConfigDALFactory; + orgRelayConfigDAL: TOrgRelayConfigDALFactory; + relayDAL: TRelayDALFactory; + kmsService: TKmsServiceFactory; +}) => { + const $getInstanceCAs = async () => { + const instanceConfig = await instanceRelayConfigDAL.transaction(async (tx) => { + const existingInstanceRelayConfig = await instanceRelayConfigDAL.findById(INSTANCE_RELAY_CONFIG_UUID); + if (existingInstanceRelayConfig) return existingInstanceRelayConfig; + + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.InstanceRelayConfigInit()]); + + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + const rootCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + // generate root CA + const rootCaSerialNumber = createSerialNumber(); + const rootCaSkObj = crypto.nativeCrypto.KeyObject.from(rootCaKeys.privateKey); + const rootCaIssuedAt = new Date(); + const rootCaExpiration = new Date(new Date().setFullYear(2045)); + const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({ + name: `O=Infisical,CN=Infisical Instance Root Relay CA`, + serialNumber: rootCaSerialNumber, + notBefore: rootCaIssuedAt, + notAfter: rootCaExpiration, + signingAlgorithm: alg, + keys: rootCaKeys, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), + await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey) + ] + }); + + // generate org relay CA + const orgRelayCaSerialNumber = createSerialNumber(); + const orgRelayCaIssuedAt = new Date(); + const orgRelayCaExpiration = new Date(new Date().setFullYear(2045)); + const orgRelayCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const orgRelayCaSkObj = crypto.nativeCrypto.KeyObject.from(orgRelayCaKeys.privateKey); + const orgRelayCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: orgRelayCaSerialNumber, + subject: `O=Infisical,CN=Infisical Organization Relay CA`, + issuer: rootCaCert.subject, + notBefore: orgRelayCaIssuedAt, + notAfter: orgRelayCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: orgRelayCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 2, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(orgRelayCaKeys.publicKey) + ] + }); + const orgRelayCaChain = constructPemChainFromCerts([rootCaCert]); + + // generate instance relay CA + const instanceRelayCaSerialNumber = createSerialNumber(); + const instanceRelayCaIssuedAt = new Date(); + const instanceRelayCaExpiration = new Date(new Date().setFullYear(2045)); + const instanceRelayCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const instanceRelayCaSkObj = crypto.nativeCrypto.KeyObject.from(instanceRelayCaKeys.privateKey); + const instanceRelayCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: instanceRelayCaSerialNumber, + subject: `O=Infisical,CN=Infisical Instance Relay CA`, + issuer: rootCaCert.subject, + notBefore: instanceRelayCaIssuedAt, + notAfter: instanceRelayCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: instanceRelayCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 1, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(instanceRelayCaKeys.publicKey) + ] + }); + const instanceRelayCaChain = constructPemChainFromCerts([rootCaCert]); + + // generate instance relay client CA + const instanceRelayClientCaSerialNumber = createSerialNumber(); + const instanceRelayClientCaIssuedAt = new Date(); + const instanceRelayClientCaExpiration = new Date(new Date().setFullYear(2045)); + const instanceRelayClientCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const instanceRelayClientCaSkObj = crypto.nativeCrypto.KeyObject.from(instanceRelayClientCaKeys.privateKey); + const instanceRelayClientCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: instanceRelayClientCaSerialNumber, + subject: `O=Infisical,CN=Infisical Instance Relay Client CA`, + issuer: instanceRelayCaCert.subject, + notBefore: instanceRelayClientCaIssuedAt, + notAfter: instanceRelayClientCaExpiration, + signingKey: instanceRelayCaKeys.privateKey, + publicKey: instanceRelayClientCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(instanceRelayCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(instanceRelayClientCaKeys.publicKey) + ] + }); + const instanceRelayClientCaChain = constructPemChainFromCerts([instanceRelayCaCert, rootCaCert]); + + // generate instance relay server CA + const instanceRelayServerCaSerialNumber = createSerialNumber(); + const instanceRelayServerCaIssuedAt = new Date(); + const instanceRelayServerCaExpiration = new Date(new Date().setFullYear(2045)); + const instanceRelayServerCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const instanceRelayServerCaSkObj = crypto.nativeCrypto.KeyObject.from(instanceRelayServerCaKeys.privateKey); + const instanceRelayServerCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: instanceRelayServerCaSerialNumber, + subject: `O=Infisical,CN=Infisical Instance Relay Server CA`, + issuer: instanceRelayCaCert.subject, + notBefore: instanceRelayServerCaIssuedAt, + notAfter: instanceRelayServerCaExpiration, + signingKey: instanceRelayCaKeys.privateKey, + publicKey: instanceRelayServerCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(instanceRelayCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(instanceRelayServerCaKeys.publicKey) + ] + }); + const instanceRelayServerCaChain = constructPemChainFromCerts([instanceRelayCaCert, rootCaCert]); + + const instanceSshServerCaKeyPair = await createSshKeyPair(SshCertKeyAlgorithm.RSA_2048); + const instanceSshClientCaKeyPair = await createSshKeyPair(SshCertKeyAlgorithm.RSA_2048); + + const encryptWithRoot = kmsService.encryptWithRootKey(); + + // root relay CA + const encryptedRootRelayPkiCaPrivateKey = encryptWithRoot( + Buffer.from( + rootCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + ); + const encryptedRootRelayPkiCaCertificate = encryptWithRoot(Buffer.from(rootCaCert.rawData)); + + // org relay CA + const encryptedOrgRelayPkiCaPrivateKey = encryptWithRoot( + Buffer.from( + orgRelayCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + ); + const encryptedOrgRelayPkiCaCertificate = encryptWithRoot(Buffer.from(orgRelayCaCert.rawData)); + const encryptedOrgRelayPkiCaCertificateChain = encryptWithRoot(Buffer.from(orgRelayCaChain)); + + // instance relay CA + const encryptedInstanceRelayPkiCaPrivateKey = encryptWithRoot( + Buffer.from( + instanceRelayCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + ); + const encryptedInstanceRelayPkiCaCertificate = encryptWithRoot(Buffer.from(instanceRelayCaCert.rawData)); + const encryptedInstanceRelayPkiCaCertificateChain = encryptWithRoot(Buffer.from(instanceRelayCaChain)); + + // instance relay client CA + const encryptedInstanceRelayPkiClientCaPrivateKey = encryptWithRoot( + Buffer.from( + instanceRelayClientCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + ); + const encryptedInstanceRelayPkiClientCaCertificate = encryptWithRoot( + Buffer.from(instanceRelayClientCaCert.rawData) + ); + const encryptedInstanceRelayPkiClientCaCertificateChain = encryptWithRoot( + Buffer.from(instanceRelayClientCaChain) + ); + + // instance relay server CA + const encryptedInstanceRelayPkiServerCaPrivateKey = encryptWithRoot( + Buffer.from( + instanceRelayServerCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + ); + const encryptedInstanceRelayPkiServerCaCertificate = encryptWithRoot( + Buffer.from(instanceRelayServerCaCert.rawData) + ); + const encryptedInstanceRelayPkiServerCaCertificateChain = encryptWithRoot( + Buffer.from(instanceRelayServerCaChain) + ); + + const encryptedInstanceRelaySshClientCaPublicKey = encryptWithRoot( + Buffer.from(instanceSshClientCaKeyPair.publicKey) + ); + const encryptedInstanceRelaySshClientCaPrivateKey = encryptWithRoot( + Buffer.from(instanceSshClientCaKeyPair.privateKey) + ); + + const encryptedInstanceRelaySshServerCaPublicKey = encryptWithRoot( + Buffer.from(instanceSshServerCaKeyPair.publicKey) + ); + const encryptedInstanceRelaySshServerCaPrivateKey = encryptWithRoot( + Buffer.from(instanceSshServerCaKeyPair.privateKey) + ); + + return instanceRelayConfigDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: INSTANCE_RELAY_CONFIG_UUID, + encryptedRootRelayPkiCaPrivateKey, + encryptedRootRelayPkiCaCertificate, + encryptedInstanceRelayPkiCaPrivateKey, + encryptedInstanceRelayPkiCaCertificate, + encryptedInstanceRelayPkiCaCertificateChain, + encryptedInstanceRelayPkiClientCaPrivateKey, + encryptedInstanceRelayPkiClientCaCertificate, + encryptedInstanceRelayPkiClientCaCertificateChain, + encryptedInstanceRelayPkiServerCaPrivateKey, + encryptedInstanceRelayPkiServerCaCertificate, + encryptedInstanceRelayPkiServerCaCertificateChain, + encryptedOrgRelayPkiCaPrivateKey, + encryptedOrgRelayPkiCaCertificate, + encryptedOrgRelayPkiCaCertificateChain, + encryptedInstanceRelaySshClientCaPublicKey, + encryptedInstanceRelaySshClientCaPrivateKey, + encryptedInstanceRelaySshServerCaPublicKey, + encryptedInstanceRelaySshServerCaPrivateKey + }); + }); + + // decrypt the instance config + const decryptWithRoot = kmsService.decryptWithRootKey(); + + // decrypt root relay CA + const rootRelayPkiCaPrivateKey = decryptWithRoot(instanceConfig.encryptedRootRelayPkiCaPrivateKey); + const rootRelayPkiCaCertificate = decryptWithRoot(instanceConfig.encryptedRootRelayPkiCaCertificate); + + // decrypt org relay CA + const orgRelayPkiCaPrivateKey = decryptWithRoot(instanceConfig.encryptedOrgRelayPkiCaPrivateKey); + const orgRelayPkiCaCertificate = decryptWithRoot(instanceConfig.encryptedOrgRelayPkiCaCertificate); + const orgRelayPkiCaCertificateChain = decryptWithRoot(instanceConfig.encryptedOrgRelayPkiCaCertificateChain); + + // decrypt instance relay CA + const instanceRelayPkiCaPrivateKey = decryptWithRoot(instanceConfig.encryptedInstanceRelayPkiCaPrivateKey); + const instanceRelayPkiCaCertificate = decryptWithRoot(instanceConfig.encryptedInstanceRelayPkiCaCertificate); + const instanceRelayPkiCaCertificateChain = decryptWithRoot( + instanceConfig.encryptedInstanceRelayPkiCaCertificateChain + ); + + // decrypt instance relay client CA + const instanceRelayPkiClientCaPrivateKey = decryptWithRoot( + instanceConfig.encryptedInstanceRelayPkiClientCaPrivateKey + ); + const instanceRelayPkiClientCaCertificate = decryptWithRoot( + instanceConfig.encryptedInstanceRelayPkiClientCaCertificate + ); + const instanceRelayPkiClientCaCertificateChain = decryptWithRoot( + instanceConfig.encryptedInstanceRelayPkiClientCaCertificateChain + ); + + // decrypt instance relay server CA + const instanceRelayPkiServerCaPrivateKey = decryptWithRoot( + instanceConfig.encryptedInstanceRelayPkiServerCaPrivateKey + ); + const instanceRelayPkiServerCaCertificate = decryptWithRoot( + instanceConfig.encryptedInstanceRelayPkiServerCaCertificate + ); + const instanceRelayPkiServerCaCertificateChain = decryptWithRoot( + instanceConfig.encryptedInstanceRelayPkiServerCaCertificateChain + ); + + // decrypt SSH keys + const instanceRelaySshClientCaPublicKey = decryptWithRoot( + instanceConfig.encryptedInstanceRelaySshClientCaPublicKey + ); + const instanceRelaySshClientCaPrivateKey = decryptWithRoot( + instanceConfig.encryptedInstanceRelaySshClientCaPrivateKey + ); + const instanceRelaySshServerCaPublicKey = decryptWithRoot( + instanceConfig.encryptedInstanceRelaySshServerCaPublicKey + ); + const instanceRelaySshServerCaPrivateKey = decryptWithRoot( + instanceConfig.encryptedInstanceRelaySshServerCaPrivateKey + ); + + return { + rootRelayPkiCaPrivateKey, + rootRelayPkiCaCertificate, + orgRelayPkiCaPrivateKey, + orgRelayPkiCaCertificate, + orgRelayPkiCaCertificateChain, + instanceRelayPkiCaPrivateKey, + instanceRelayPkiCaCertificate, + instanceRelayPkiCaCertificateChain, + instanceRelayPkiClientCaPrivateKey, + instanceRelayPkiClientCaCertificate, + instanceRelayPkiClientCaCertificateChain, + instanceRelayPkiServerCaPrivateKey, + instanceRelayPkiServerCaCertificate, + instanceRelayPkiServerCaCertificateChain, + instanceRelaySshClientCaPublicKey, + instanceRelaySshClientCaPrivateKey, + instanceRelaySshServerCaPublicKey, + instanceRelaySshServerCaPrivateKey + }; + }; + + const $getOrgCAs = async (orgId: string) => { + const instanceCAs = await $getInstanceCAs(); + const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const orgRelayConfig = await orgRelayConfigDAL.transaction(async (tx) => { + const existingOrgRelayConfig = await orgRelayConfigDAL.findOne( + { + orgId + }, + tx + ); + + if (existingOrgRelayConfig) { + return existingOrgRelayConfig; + } + + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgRelayConfigInit(orgId)]); + + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + const orgRelayCaCert = new x509.X509Certificate(instanceCAs.orgRelayPkiCaCertificate); + const rootRelayCaCert = new x509.X509Certificate(instanceCAs.rootRelayPkiCaCertificate); + const orgRelayCaSkObj = crypto.nativeCrypto.createPrivateKey({ + key: instanceCAs.orgRelayPkiCaPrivateKey, + format: "der", + type: "pkcs8" + }); + const orgRelayCaPrivateKey = await crypto.nativeCrypto.subtle.importKey( + "pkcs8", + orgRelayCaSkObj.export({ format: "der", type: "pkcs8" }), + alg, + true, + ["sign"] + ); + + // generate org relay client CA + const orgRelayClientCaSerialNumber = createSerialNumber(); + const orgRelayClientCaIssuedAt = new Date(); + const orgRelayClientCaExpiration = new Date(new Date().setFullYear(2045)); + const orgRelayClientCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const orgRelayClientCaSkObj = crypto.nativeCrypto.KeyObject.from(orgRelayClientCaKeys.privateKey); + const orgRelayClientCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: orgRelayClientCaSerialNumber, + subject: `O=${orgId},CN=Infisical Org Relay Client CA`, + issuer: orgRelayCaCert.subject, + notBefore: orgRelayClientCaIssuedAt, + notAfter: orgRelayClientCaExpiration, + signingKey: orgRelayCaPrivateKey, + publicKey: orgRelayClientCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(orgRelayCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(orgRelayClientCaKeys.publicKey) + ] + }); + const orgRelayClientCaChain = constructPemChainFromCerts([orgRelayCaCert, rootRelayCaCert]); + + // generate org SSH CA + const orgSshServerCaKeyPair = await createSshKeyPair(SshCertKeyAlgorithm.RSA_2048); + const orgSshClientCaKeyPair = await createSshKeyPair(SshCertKeyAlgorithm.RSA_2048); + + // generate org relay server CA + const orgRelayServerCaSerialNumber = createSerialNumber(); + const orgRelayServerCaIssuedAt = new Date(); + const orgRelayServerCaExpiration = new Date(new Date().setFullYear(2045)); + const orgRelayServerCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const orgRelayServerCaSkObj = crypto.nativeCrypto.KeyObject.from(orgRelayServerCaKeys.privateKey); + const orgRelayServerCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: orgRelayServerCaSerialNumber, + subject: `O=${orgId},CN=Infisical Org Relay Server CA`, + issuer: orgRelayCaCert.subject, + notBefore: orgRelayServerCaIssuedAt, + notAfter: orgRelayServerCaExpiration, + signingKey: orgRelayCaPrivateKey, + publicKey: orgRelayServerCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(orgRelayCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(orgRelayServerCaKeys.publicKey) + ] + }); + const orgRelayServerCaChain = constructPemChainFromCerts([orgRelayCaCert, rootRelayCaCert]); + + const encryptedRelayPkiClientCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from( + orgRelayClientCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + }).cipherTextBlob; + const encryptedRelayPkiClientCaCertificate = orgKmsEncryptor({ + plainText: Buffer.from(orgRelayClientCaCert.rawData) + }).cipherTextBlob; + + const encryptedRelayPkiClientCaCertificateChain = orgKmsEncryptor({ + plainText: Buffer.from(orgRelayClientCaChain) + }).cipherTextBlob; + + const encryptedRelayPkiServerCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from( + orgRelayServerCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + }).cipherTextBlob; + const encryptedRelayPkiServerCaCertificate = orgKmsEncryptor({ + plainText: Buffer.from(orgRelayServerCaCert.rawData) + }).cipherTextBlob; + const encryptedRelayPkiServerCaCertificateChain = orgKmsEncryptor({ + plainText: Buffer.from(orgRelayServerCaChain) + }).cipherTextBlob; + + const encryptedRelaySshClientCaPublicKey = orgKmsEncryptor({ + plainText: Buffer.from(orgSshClientCaKeyPair.publicKey) + }).cipherTextBlob; + const encryptedRelaySshClientCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from(orgSshClientCaKeyPair.privateKey) + }).cipherTextBlob; + + const encryptedRelaySshServerCaPublicKey = orgKmsEncryptor({ + plainText: Buffer.from(orgSshServerCaKeyPair.publicKey) + }).cipherTextBlob; + const encryptedRelaySshServerCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from(orgSshServerCaKeyPair.privateKey) + }).cipherTextBlob; + + return orgRelayConfigDAL.create({ + orgId, + encryptedRelayPkiClientCaPrivateKey, + encryptedRelayPkiClientCaCertificate, + encryptedRelayPkiClientCaCertificateChain, + encryptedRelayPkiServerCaPrivateKey, + encryptedRelayPkiServerCaCertificate, + encryptedRelayPkiServerCaCertificateChain, + encryptedRelaySshClientCaPublicKey, + encryptedRelaySshClientCaPrivateKey, + encryptedRelaySshServerCaPublicKey, + encryptedRelaySshServerCaPrivateKey + }); + }); + + const relayPkiClientCaPrivateKey = orgKmsDecryptor({ + cipherTextBlob: orgRelayConfig.encryptedRelayPkiClientCaPrivateKey + }); + const relayPkiClientCaCertificate = orgKmsDecryptor({ + cipherTextBlob: orgRelayConfig.encryptedRelayPkiClientCaCertificate + }); + const relayPkiClientCaCertificateChain = orgKmsDecryptor({ + cipherTextBlob: orgRelayConfig.encryptedRelayPkiClientCaCertificateChain + }); + + const relayPkiServerCaPrivateKey = orgKmsDecryptor({ + cipherTextBlob: orgRelayConfig.encryptedRelayPkiServerCaPrivateKey + }); + const relayPkiServerCaCertificate = orgKmsDecryptor({ + cipherTextBlob: orgRelayConfig.encryptedRelayPkiServerCaCertificate + }); + const relayPkiServerCaCertificateChain = orgKmsDecryptor({ + cipherTextBlob: orgRelayConfig.encryptedRelayPkiServerCaCertificateChain + }); + + const relaySshClientCaPublicKey = orgKmsDecryptor({ + cipherTextBlob: orgRelayConfig.encryptedRelaySshClientCaPublicKey + }); + const relaySshClientCaPrivateKey = orgKmsDecryptor({ + cipherTextBlob: orgRelayConfig.encryptedRelaySshClientCaPrivateKey + }); + + const relaySshServerCaPublicKey = orgKmsDecryptor({ + cipherTextBlob: orgRelayConfig.encryptedRelaySshServerCaPublicKey + }); + const relaySshServerCaPrivateKey = orgKmsDecryptor({ + cipherTextBlob: orgRelayConfig.encryptedRelaySshServerCaPrivateKey + }); + + return { + relayPkiClientCaPrivateKey, + relayPkiClientCaCertificate, + relayPkiClientCaCertificateChain, + relayPkiServerCaPrivateKey, + relayPkiServerCaCertificate, + relayPkiServerCaCertificateChain, + relaySshClientCaPublicKey, + relaySshClientCaPrivateKey, + relaySshServerCaPublicKey, + relaySshServerCaPrivateKey + }; + }; + + const $generateRelayServerCredentials = async ({ + host, + orgId, + relayPkiServerCaCertificate, + relayPkiServerCaPrivateKey, + relayPkiClientCaCertificate, + relayPkiClientCaCertificateChain, + relaySshClientCaPublicKey, + relaySshServerCaPrivateKey + }: { + host: string; + relayPkiServerCaCertificate: Buffer; + relayPkiServerCaPrivateKey: Buffer; + relayPkiClientCaCertificateChain: Buffer; + relayPkiClientCaCertificate: Buffer; + relaySshServerCaPrivateKey: Buffer; + relaySshClientCaPublicKey: Buffer; + orgId?: string; + }) => { + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + const relayServerCaCert = new x509.X509Certificate(relayPkiServerCaCertificate); + const relayClientCaCert = new x509.X509Certificate(relayPkiClientCaCertificate); + const relayServerCaSkObj = crypto.nativeCrypto.createPrivateKey({ + key: relayPkiServerCaPrivateKey, + format: "der", + type: "pkcs8" + }); + + const relayServerCaPrivateKey = await crypto.nativeCrypto.subtle.importKey( + "pkcs8", + relayServerCaSkObj.export({ format: "der", type: "pkcs8" }), + alg, + true, + ["sign"] + ); + + const relayServerKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const relayServerCertIssuedAt = new Date(); + const relayServerCertExpireAt = new Date(new Date().setDate(new Date().getDate() + 1)); + const relayServerCertPrivateKey = crypto.nativeCrypto.KeyObject.from(relayServerKeys.privateKey); + + const relayServerCertExtensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(relayServerCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(relayServerKeys.publicKey), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true), + // san + new x509.SubjectAlternativeNameExtension([{ type: "ip", value: host }], false) + ]; + + const relayServerSerialNumber = createSerialNumber(); + const relayServerCertificate = await x509.X509CertificateGenerator.create({ + serialNumber: relayServerSerialNumber, + subject: `CN=${host},O=${orgId ?? "Infisical"},OU=Relay`, + issuer: relayServerCaCert.subject, + notBefore: relayServerCertIssuedAt, + notAfter: relayServerCertExpireAt, + signingKey: relayServerCaPrivateKey, + publicKey: relayServerKeys.publicKey, + signingAlgorithm: alg, + extensions: relayServerCertExtensions + }); + + // generate relay server SSH certificate + const keyAlgorithm = SshCertKeyAlgorithm.RSA_2048; + const { publicKey: relayServerSshPublicKey, privateKey: relayServerSshPrivateKey } = + await createSshKeyPair(keyAlgorithm); + + const relayServerSshCert = await createSshCert({ + caPrivateKey: relaySshServerCaPrivateKey.toString("utf8"), + clientPublicKey: relayServerSshPublicKey, + keyId: "relay-server", + principals: [`${host}:2222`], + certType: SshCertType.HOST, + requestedTtl: "30d" + }); + + return { + pki: { + serverCertificate: relayServerCertificate.toString("pem"), + serverPrivateKey: relayServerCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), + clientCertificateChain: prependCertToPemChain( + relayClientCaCert, + relayPkiClientCaCertificateChain.toString("utf8") + ) + }, + ssh: { + serverCertificate: relayServerSshCert.signedPublicKey, + serverPrivateKey: relayServerSshPrivateKey, + clientCAPublicKey: relaySshClientCaPublicKey.toString("utf8") + } + }; + }; + + const $generateRelayClientCredentials = async ({ + gatewayId, + orgId, + orgName, + relayPkiClientCaCertificate, + relayPkiClientCaPrivateKey, + relayPkiServerCaCertificate, + relayPkiServerCaCertificateChain + }: { + gatewayId: string; + orgId: string; + orgName: string; + relayPkiClientCaCertificate: Buffer; + relayPkiClientCaPrivateKey: Buffer; + relayPkiServerCaCertificate: Buffer; + relayPkiServerCaCertificateChain: Buffer; + }) => { + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + const relayClientCaCert = new x509.X509Certificate(relayPkiClientCaCertificate); + const relayServerCaCert = new x509.X509Certificate(relayPkiServerCaCertificate); + const relayClientCaSkObj = crypto.nativeCrypto.createPrivateKey({ + key: relayPkiClientCaPrivateKey, + format: "der", + type: "pkcs8" + }); + + const importedRelayClientCaPrivateKey = await crypto.nativeCrypto.subtle.importKey( + "pkcs8", + relayClientCaSkObj.export({ format: "der", type: "pkcs8" }), + alg, + true, + ["sign"] + ); + + const clientCertIssuedAt = new Date(); + const clientCertExpiration = new Date(new Date().getTime() + 5 * 60 * 1000); + const clientKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const clientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey); + const clientCertSerialNumber = createSerialNumber(); + + // Build standard extensions + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(relayClientCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(clientKeys.publicKey), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | + x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] | + x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true) + ]; + + const clientCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientCertSerialNumber, + subject: `O=${orgName}-${orgId},OU=relay-client,CN=${gatewayId}`, + issuer: relayClientCaCert.subject, + notAfter: clientCertExpiration, + notBefore: clientCertIssuedAt, + signingKey: importedRelayClientCaPrivateKey, + publicKey: clientKeys.publicKey, + signingAlgorithm: alg, + extensions + }); + + return { + clientCertificate: clientCert.toString("pem"), + clientPrivateKey: clientCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), + serverCertificateChain: prependCertToPemChain( + relayServerCaCert, + relayPkiServerCaCertificateChain.toString("utf8") + ) + }; + }; + + const getCredentialsForGateway = async ({ + relayName, + orgId, + gatewayId + }: { + relayName: string; + orgId: string; + gatewayId: string; + }) => { + let relay: TRelays | null = await relayDAL.findOne({ + orgId, + name: relayName + }); + + if (!relay) { + relay = await relayDAL.findOne({ + name: relayName, + orgId: null + }); + } + + if (!relay) { + throw new NotFoundError({ + message: "Relay not found" + }); + } + + const keyAlgorithm = SshCertKeyAlgorithm.RSA_2048; + const { publicKey: relayClientSshPublicKey, privateKey: relayClientSshPrivateKey } = + await createSshKeyPair(keyAlgorithm); + + if (relay.orgId === null) { + const instanceCAs = await $getInstanceCAs(); + const relayClientSshCert = await createSshCert({ + caPrivateKey: instanceCAs.instanceRelaySshClientCaPrivateKey.toString("utf8"), + clientPublicKey: relayClientSshPublicKey, + keyId: `client-${relayName}`, + principals: [gatewayId], + certType: SshCertType.USER, + requestedTtl: "1d" + }); + + return { + relayHost: relay.host, + clientSshCert: relayClientSshCert.signedPublicKey, + clientSshPrivateKey: relayClientSshPrivateKey, + serverCAPublicKey: instanceCAs.instanceRelaySshServerCaPublicKey.toString("utf8") + }; + } + + const orgCAs = await $getOrgCAs(orgId); + const relayClientSshCert = await createSshCert({ + caPrivateKey: orgCAs.relaySshClientCaPrivateKey.toString("utf8"), + clientPublicKey: relayClientSshPublicKey, + keyId: `relay-client-${relay.id}`, + principals: [gatewayId], + certType: SshCertType.USER, + requestedTtl: "30d" + }); + + return { + relayHost: relay.host, + clientSshCert: relayClientSshCert.signedPublicKey, + clientSshPrivateKey: relayClientSshPrivateKey, + serverCAPublicKey: orgCAs.relaySshServerCaPublicKey.toString("utf8") + }; + }; + + const getCredentialsForClient = async ({ + relayId, + orgId, + orgName, + gatewayId + }: { + relayId: string; + orgId: string; + orgName: string; + gatewayId: string; + }) => { + const relay = await relayDAL.findOne({ + id: relayId + }); + + if (!relay) { + throw new NotFoundError({ + message: "Relay not found" + }); + } + + await verifyHostInputValidity(relay.host); + + if (relay.orgId === null) { + const instanceCAs = await $getInstanceCAs(); + const relayCertificateCredentials = await $generateRelayClientCredentials({ + gatewayId, + orgId, + orgName, + relayPkiClientCaCertificate: instanceCAs.instanceRelayPkiClientCaCertificate, + relayPkiClientCaPrivateKey: instanceCAs.instanceRelayPkiClientCaPrivateKey, + relayPkiServerCaCertificate: instanceCAs.instanceRelayPkiServerCaCertificate, + relayPkiServerCaCertificateChain: instanceCAs.instanceRelayPkiServerCaCertificateChain + }); + + return { + ...relayCertificateCredentials, + relayHost: relay.host + }; + } + + const orgCAs = await $getOrgCAs(orgId); + const relayCertificateCredentials = await $generateRelayClientCredentials({ + gatewayId, + orgId, + orgName, + relayPkiClientCaCertificate: orgCAs.relayPkiClientCaCertificate, + relayPkiClientCaPrivateKey: orgCAs.relayPkiClientCaPrivateKey, + relayPkiServerCaCertificate: orgCAs.relayPkiServerCaCertificate, + relayPkiServerCaCertificateChain: orgCAs.relayPkiServerCaCertificateChain + }); + + return { + ...relayCertificateCredentials, + relayHost: relay.host + }; + }; + + const registerRelay = async ({ + host, + name, + identityId, + orgId + }: { + host: string; + name: string; + identityId?: string; + orgId?: string; + }) => { + let relay: TRelays; + const isOrgRelay = identityId && orgId; + + await verifyHostInputValidity(host); + + if (isOrgRelay) { + relay = await relayDAL.transaction(async (tx) => { + const existingRelay = await relayDAL.findOne( + { + identityId, + orgId + }, + tx + ); + + if (existingRelay && (existingRelay.host !== host || existingRelay.name !== name)) { + return relayDAL.updateById(existingRelay.id, { host, name }, tx); + } + + if (!existingRelay) { + return relayDAL.create( + { + host, + name, + identityId, + orgId + }, + tx + ); + } + + return existingRelay; + }); + } else { + relay = await relayDAL.transaction(async (tx) => { + const existingRelay = await relayDAL.findOne( + { + name, + orgId: null + }, + tx + ); + + if (existingRelay && existingRelay.host !== host) { + return relayDAL.updateById(existingRelay.id, { host }, tx); + } + + if (!existingRelay) { + return relayDAL.create( + { + host, + name + }, + tx + ); + } + + return existingRelay; + }); + } + + if (relay.orgId === null) { + const instanceCAs = await $getInstanceCAs(); + return $generateRelayServerCredentials({ + host, + relayPkiServerCaCertificate: instanceCAs.instanceRelayPkiServerCaCertificate, + relayPkiServerCaPrivateKey: instanceCAs.instanceRelayPkiServerCaPrivateKey, + relayPkiClientCaCertificate: instanceCAs.instanceRelayPkiClientCaCertificate, + relayPkiClientCaCertificateChain: instanceCAs.instanceRelayPkiClientCaCertificateChain, + relaySshServerCaPrivateKey: instanceCAs.instanceRelaySshServerCaPrivateKey, + relaySshClientCaPublicKey: instanceCAs.instanceRelaySshClientCaPublicKey + }); + } + + if (relay.orgId) { + const orgCAs = await $getOrgCAs(relay.orgId); + return $generateRelayServerCredentials({ + host, + orgId: relay.orgId, + relayPkiServerCaCertificate: orgCAs.relayPkiServerCaCertificate, + relayPkiServerCaPrivateKey: orgCAs.relayPkiServerCaPrivateKey, + relayPkiClientCaCertificate: orgCAs.relayPkiClientCaCertificate, + relayPkiClientCaCertificateChain: orgCAs.relayPkiClientCaCertificateChain, + relaySshServerCaPrivateKey: orgCAs.relaySshServerCaPrivateKey, + relaySshClientCaPublicKey: orgCAs.relaySshClientCaPublicKey + }); + } + + throw new BadRequestError({ + message: "Unhandled relay type" + }); + }; + + return { + registerRelay, + getCredentialsForGateway, + getCredentialsForClient + }; +}; diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 6b8bbe304..31aaa70aa 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { OrgMembershipStatus, TableName, TSamlConfigs, TSamlConfigsUpdate, TUsers } from "@app/db/schemas"; +import { throwOnPlanSeatLimitReached } from "@app/ee/services/license/license-fns"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -82,6 +83,19 @@ export const samlConfigServiceFactory = ({ "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to create SSO configuration." }); + const org = await orgDAL.findOrgById(orgId); + + if (!org) { + throw new NotFoundError({ message: `Could not find organization with ID "${orgId}"` }); + } + + if (org.googleSsoAuthEnforced && isActive) { + throw new BadRequestError({ + message: + "You cannot enable SAML SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable SAML SSO." + }); + } + const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId @@ -120,6 +134,19 @@ export const samlConfigServiceFactory = ({ "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." }); + const org = await orgDAL.findOrgById(orgId); + + if (!org) { + throw new NotFoundError({ message: `Could not find organization with ID "${orgId}"` }); + } + + if (org.googleSsoAuthEnforced && isActive) { + throw new BadRequestError({ + message: + "Cannot enable SAML SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable SAML SSO." + }); + } + const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null }; const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -310,14 +337,6 @@ export const samlConfigServiceFactory = ({ return foundUser; }); } else { - const plan = await licenseService.getPlan(orgId); - if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { - // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed - throw new BadRequestError({ - message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members." - }); - } - user = await userDAL.transaction(async (tx) => { let newUser: TUsers | undefined; newUser = await userDAL.findOne( @@ -365,6 +384,8 @@ export const samlConfigServiceFactory = ({ ); if (!orgMembership) { + await throwOnPlanSeatLimitReached(licenseService, orgId, UserAliasType.SAML); + const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); await orgMembershipDAL.create( diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index fe4ca94e1..01caef223 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -345,7 +345,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { const findProjectRequestCount = async (projectId: string, userId: string, policyId?: string, tx?: Knex) => { try { - const docs = await (tx || db) + const docs = await (tx || db.replicaNode()) .with( "temp", (tx || db.replicaNode())(TableName.SecretApprovalRequest) @@ -494,7 +494,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { .distinctOn(`${TableName.SecretApprovalRequest}.id`) .as("inner"); - const query = (tx || db) + const query = (tx || db.replicaNode()) .select("*") .select(db.raw("count(*) OVER() as total_count")) .from(innerQuery) diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts index c1b18e43d..17182cddf 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts @@ -377,7 +377,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { // special query for migration to v2 secret const findByProjectId = async (projectId: string, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.SecretApprovalRequestSecret) + const docs = await (tx || db.replicaNode())(TableName.SecretApprovalRequestSecret) .join( TableName.SecretApprovalRequest, `${TableName.SecretApprovalRequest}.id`, diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index d485f7ea0..17b7d8347 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -787,6 +787,7 @@ export const secretApprovalRequestServiceFactory = ({ }, tx ); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId, tx); return { secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret }, approval: updatedSecretApproval @@ -976,6 +977,7 @@ export const secretApprovalRequestServiceFactory = ({ }, tx ); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId, tx); return { secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret }, approval: updatedSecretApproval @@ -983,7 +985,6 @@ export const secretApprovalRequestServiceFactory = ({ }); } - await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); await snapshotService.performSnapshot(folderId); const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]); if (!folder) { diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts index 4a5558f46..93147d9e4 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -59,7 +59,7 @@ type TSecretReplicationServiceFactoryDep = { TSecretVersionV2DALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany" >; - secretImportDAL: Pick; + secretImportDAL: Pick; folderDAL: Pick< TSecretFolderDALFactory, "findSecretPathByFolderIds" | "findBySecretPath" | "create" | "findOne" | "findByManySecretPath" @@ -509,9 +509,9 @@ export const secretReplicationServiceFactory = ({ tx ); } + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId, tx); }); - await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); await secretQueueService.syncSecrets({ projectId, orgId, diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts index 07cf97a7e..9b1fd14a0 100644 --- a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts @@ -175,7 +175,8 @@ export const ldapPasswordRotationFactory: TRotationFactory< const encryptedCredentials = await encryptAppConnectionCredentials({ credentials: updatedCredentials, orgId, - kmsService + kmsService, + projectId: connection.projectId }); await appConnectionDAL.updateById(connection.id, { encryptedCredentials }); diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-dal.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-dal.ts index 787c07bae..cf236b56f 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-dal.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-dal.ts @@ -52,6 +52,7 @@ const baseSecretRotationV2Query = ({ db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"), db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"), + db.ref("projectId").withSchema(TableName.AppConnection).as("connectionProjectId"), db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"), db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), db @@ -106,6 +107,7 @@ const expandSecretRotation = ; + appConnectionService: Pick; permissionService: Pick; projectBotService: Pick; kmsService: Pick; @@ -110,6 +111,7 @@ export type TSecretRotationV2ServiceFactoryDep = { appConnectionDAL: Pick; folderCommitService: Pick; gatewayService: Pick; + gatewayV2Service: Pick; }; export type TSecretRotationV2ServiceFactory = ReturnType; @@ -153,7 +155,8 @@ export const secretRotationV2ServiceFactory = ({ queueService, folderCommitService, appConnectionDAL, - gatewayService + gatewayService, + gatewayV2Service }: TSecretRotationV2ServiceFactoryDep) => { const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => { const appCfg = getConfig(); @@ -456,7 +459,11 @@ export const secretRotationV2ServiceFactory = ({ const typeApp = SECRET_ROTATION_CONNECTION_MAP[payload.type]; // validates permission to connect and app is valid for rotation type - const connection = await appConnectionService.connectAppConnectionById(typeApp, payload.connectionId, actor); + const connection = await appConnectionService.validateAppConnectionUsageById( + typeApp, + { connectionId: payload.connectionId, projectId }, + actor + ); const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type]( { @@ -467,7 +474,8 @@ export const secretRotationV2ServiceFactory = ({ } as TSecretRotationV2WithConnection, appConnectionDAL, kmsService, - gatewayService + gatewayService, + gatewayV2Service ); // even though we have a db constraint we want to check before any rotation of credentials is attempted @@ -831,7 +839,8 @@ export const secretRotationV2ServiceFactory = ({ } as TSecretRotationV2WithConnection, appConnectionDAL, kmsService, - gatewayService + gatewayService, + gatewayV2Service ); const generatedCredentials = await decryptSecretRotationCredentials({ @@ -915,7 +924,8 @@ export const secretRotationV2ServiceFactory = ({ } as TSecretRotationV2WithConnection, appConnectionDAL, kmsService, - gatewayService + gatewayService, + gatewayV2Service ); const updatedRotation = await rotationFactory.rotateCredentials( diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts index ab348f172..2af2ddc7b 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts @@ -6,6 +6,7 @@ import { TAppConnectionDALFactory } from "@app/services/app-connection/app-conne import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; +import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; import { TAuth0ClientSecretRotation, TAuth0ClientSecretRotationGeneratedCredentials, @@ -253,7 +254,8 @@ export type TRotationFactory< secretRotation: T, appConnectionDAL: Pick, kmsService: Pick, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { issueCredentials: TRotationFactoryIssueCredentials; revokeCredentials: TRotationFactoryRevokeCredentials; diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts index 1da1db376..6673baab1 100644 --- a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts @@ -41,7 +41,7 @@ const ORACLE_PASSWORD_REQUIREMENTS = { export const sqlCredentialsRotationFactory: TRotationFactory< TSqlCredentialsRotationWithConnection, TSqlCredentialsRotationGeneratedCredentials -> = (secretRotation, _appConnectionDAL, _kmsService, gatewayService) => { +> = (secretRotation, _appConnectionDAL, _kmsService, gatewayService, gatewayV2Service) => { const { connection, parameters: { username1, username2 }, @@ -67,6 +67,7 @@ export const sqlCredentialsRotationFactory: TRotationFactory< credentials: finalCredentials }, gatewayService, + gatewayV2Service, (client) => operation(client) ); }; diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts index 1d5c1cedf..eefe6b63a 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts @@ -361,9 +361,8 @@ export const secretRotationQueueFactory = ({ }, tx ); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secretRotation.projectId, tx); }); - - await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secretRotation.projectId); } else { if (!botKey) throw new NotFoundError({ @@ -432,7 +431,7 @@ export const secretRotationQueueFactory = ({ numberOfSecrets: numberOfSecretsRotated, environment: secretRotation.environment.slug, secretPath: secretRotation.secretPath, - workspaceId: secretRotation.projectId + projectId: secretRotation.projectId } }); diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-dal.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-dal.ts index c6ca50c5e..405e60159 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-dal.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-dal.ts @@ -50,6 +50,7 @@ const baseSecretScanningDataSourceQuery = ({ db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"), db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"), + db.ref("projectId").withSchema(TableName.AppConnection).as("connectionProjectId"), db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"), db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), db @@ -84,6 +85,7 @@ const expandSecretScanningDataSource = < connectionVersion, connectionIsPlatformManagedCredentials, connectionGatewayId, + connectionProjectId, ...el } = dataSource; @@ -103,7 +105,8 @@ const expandSecretScanningDataSource = < updatedAt: connectionUpdatedAt, version: connectionVersion, isPlatformManagedCredentials: connectionIsPlatformManagedCredentials, - gatewayId: connectionGatewayId + gatewayId: connectionGatewayId, + projectId: connectionProjectId } : undefined }; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts index 6bef41e10..c48139e17 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts @@ -60,7 +60,7 @@ import { TSecretScanningV2QueueServiceFactory } from "./secret-scanning-v2-queue export type TSecretScanningV2ServiceFactoryDep = { secretScanningV2DAL: TSecretScanningV2DALFactory; - appConnectionService: Pick; + appConnectionService: Pick; appConnectionDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -252,9 +252,9 @@ export const secretScanningV2ServiceFactory = ({ let connection: TAppConnection | null = null; if (payload.connectionId) { // validates permission to connect and app is valid for data source - connection = await appConnectionService.connectAppConnectionById( + connection = await appConnectionService.validateAppConnectionUsageById( SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[payload.type], - payload.connectionId, + { connectionId: payload.connectionId, projectId: payload.projectId }, actor ); } @@ -373,9 +373,9 @@ export const secretScanningV2ServiceFactory = ({ let connection: TAppConnection | null = null; if (dataSource.connectionId) { // validates permission to connect and app is valid for data source - connection = await appConnectionService.connectAppConnectionById( + connection = await appConnectionService.validateAppConnectionUsageById( SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSource.type], - dataSource.connectionId, + { connectionId: dataSource.connectionId, projectId: dataSource.projectId }, actor ); } diff --git a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts index c547d85c2..17f1fad05 100644 --- a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts +++ b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts @@ -265,7 +265,7 @@ export const snapshotDALFactory = (db: TDbClient) => { // then joins with respective secrets and folder const findRecursivelySnapshots = async (snapshotId: string, tx?: Knex) => { try { - const data = await (tx || db) + const data = await (tx || db.replicaNode()) .withRecursive("parent", (qb) => { void qb .from(TableName.Snapshot) @@ -419,7 +419,7 @@ export const snapshotDALFactory = (db: TDbClient) => { // then joins with respective secrets and folder const findRecursivelySnapshotsV2Bridge = async (snapshotId: string, tx?: Knex) => { try { - const data = await (tx || db) + const data = await (tx || db.replicaNode()) .withRecursive("parent", (qb) => { void qb .from(TableName.Snapshot) @@ -581,7 +581,11 @@ export const snapshotDALFactory = (db: TDbClient) => { const docs = await (tx || db.replicaNode())(TableName.Snapshot) .where(`${TableName.Snapshot}.folderId`, folderId) .join( - (tx || db)(TableName.Snapshot).groupBy("folderId").max("createdAt").select("folderId").as("latestVersion"), + (tx || db.replicaNode())(TableName.Snapshot) + .groupBy("folderId") + .max("createdAt") + .select("folderId") + .as("latestVersion"), (bd) => { bd.on(`${TableName.Snapshot}.folderId`, "latestVersion.folderId").andOn( `${TableName.Snapshot}.createdAt`, @@ -766,7 +770,7 @@ export const snapshotDALFactory = (db: TDbClient) => { ) .orderBy(`${TableName.Snapshot}.createdAt`, "desc") .where(`${TableName.Snapshot}.folderId`, folderId); - const data = await (tx || db) + const data = await (tx || db.replicaNode()) .with("w", query) .select("*") .from[number]>("w") diff --git a/backend/src/keystore/key-value-store-dal.ts b/backend/src/keystore/key-value-store-dal.ts new file mode 100644 index 000000000..bccedf4ac --- /dev/null +++ b/backend/src/keystore/key-value-store-dal.ts @@ -0,0 +1,91 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify, TOrmify } from "@app/lib/knex"; +import { logger } from "@app/lib/logger"; +import { QueueName } from "@app/queue"; + +export interface TKeyValueStoreDALFactory extends TOrmify { + incrementBy: (key: string, dto: { incr?: number; tx?: Knex; expiresAt?: Date }) => Promise; + findOneInt: (key: string, tx?: Knex) => Promise; + pruneExpiredKeys: () => Promise; +} + +const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes +const CACHE_KEY_PRUNE_BATCH_SIZE = 10000; +const MAX_RETRY_ON_FAILURE = 3; + +export const keyValueStoreDALFactory = (db: TDbClient): TKeyValueStoreDALFactory => { + const keyValueStoreOrm = ormify(db, TableName.KeyValueStore); + + const incrementBy: TKeyValueStoreDALFactory["incrementBy"] = async (key, { incr = 1, tx, expiresAt }) => { + return (tx || db)(TableName.KeyValueStore) + .insert({ key, integerValue: 1, expiresAt }) + .onConflict("key") + .merge({ + integerValue: db.raw(`"${TableName.KeyValueStore}"."integerValue" + ?`, [incr]), + expiresAt + }) + .returning("integerValue") + .then((result) => Number(result[0]?.integerValue || 0)); + }; + + const findOneInt: TKeyValueStoreDALFactory["findOneInt"] = async (key, tx) => { + const doc = await (tx || db.replicaNode())(TableName.KeyValueStore) + .where({ key }) + .andWhere( + (builder) => + void builder + .whereNull("expiresAt") // no expiry + .orWhere("expiresAt", ">", db.fn.now()) // or not expired + ) + .first() + .select("integerValue"); + return Number(doc?.integerValue || 0); + }; + + const pruneExpiredKeys: TKeyValueStoreDALFactory["pruneExpiredKeys"] = async () => { + let deletedIds: { key: string }[] = []; + let numberOfRetryOnFailure = 0; + let isRetrying = false; + + logger.info(`${QueueName.DailyResourceCleanUp}: db key value store clean up started`); + do { + try { + // eslint-disable-next-line no-await-in-loop + deletedIds = await db.transaction(async (trx) => { + await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`); + + const findExpiredKeysSubQuery = trx(TableName.KeyValueStore) + .where("expiresAt", "<", db.fn.now()) + .select("key") + .limit(CACHE_KEY_PRUNE_BATCH_SIZE); + + // eslint-disable-next-line no-await-in-loop + const results = await trx(TableName.KeyValueStore) + .whereIn("key", findExpiredKeysSubQuery) + .del() + .returning("key"); + + return results; + }); + + numberOfRetryOnFailure = 0; // reset + } catch (error) { + numberOfRetryOnFailure += 1; + deletedIds = []; + logger.error(error, "Failed to clean up db key value"); + } finally { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 10); // time to breathe for db + }); + } + isRetrying = numberOfRetryOnFailure > 0; + } while (deletedIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE)); + logger.info(`${QueueName.DailyResourceCleanUp}: db key value store clean up completed`); + }; + + return { ...keyValueStoreOrm, incrementBy, findOneInt, pruneExpiredKeys }; +}; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 777e73fe2..8b72ce464 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -1,9 +1,15 @@ +import { Cluster, Redis } from "ioredis"; +import { Knex } from "knex"; + import { buildRedisFromConfig, TRedisConfigKeys } from "@app/lib/config/redis"; import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext"; import { applyJitter } from "@app/lib/dates"; import { delay as delayMs } from "@app/lib/delay"; +import { ms } from "@app/lib/ms"; import { ExecutionResult, Redlock, Settings } from "@app/lib/red-lock"; +import { TKeyValueStoreDALFactory } from "./key-value-store-dal"; + export const PgSqlLock = { BootUpMigration: 2023, SuperAdminInit: 2024, @@ -14,6 +20,9 @@ export const PgSqlLock = { CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`), CreateFolder: (envId: string, projectId: string) => pgAdvisoryLockHashText(`create-folder:${envId}-${projectId}`), SshInit: (projectId: string) => pgAdvisoryLockHashText(`ssh-bootstrap:${projectId}`), + InstanceRelayConfigInit: () => pgAdvisoryLockHashText("instance-relay-config-init"), + OrgGatewayV2Init: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-v2-init:${orgId}`), + OrgRelayConfigInit: (orgId: string) => pgAdvisoryLockHashText(`org-relay-config-init:${orgId}`), IdentityLogin: (identityId: string, nonce: string) => pgAdvisoryLockHashText(`identity-login:${identityId}:${nonce}`) } as const; @@ -38,6 +47,7 @@ export const KeyStorePrefixes = { SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) => `sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const, SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const, + AppConnectionConcurrentJobs: (connectionId: string) => `app-connection-concurrency-${connectionId}` as const, SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const, SecretScanningLock: (dataSourceId: string, resourceExternalId: string) => `secret-scanning-v2-mutex-${dataSourceId}-${resourceExternalId}` as const, @@ -92,39 +102,73 @@ export type TKeyStoreFactory = { deleteItemsByKeyIn: (keys: string[]) => Promise; deleteItems: (arg: TDeleteItems) => Promise; incrementBy: (key: string, value: number) => Promise; + getKeysByPattern: (pattern: string, limit?: number) => Promise; + // pg + pgIncrementBy: (key: string, dto: { incr?: number; expiry?: string; tx?: Knex }) => Promise; + pgGetIntItem: (key: string, prefix?: string) => Promise; + // locks acquireLock( resources: string[], duration: number, settings?: Partial ): Promise<{ release: () => Promise }>; waitTillReady: ({ key, waitingCb, keyCheckCb, waitIteration, delay, jitter }: TWaitTillReady) => Promise; - getKeysByPattern: (pattern: string, limit?: number) => Promise; }; -export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFactory => { - const redis = buildRedisFromConfig(redisConfigKeys); - const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 }); +const pickPrimaryOrSecondaryRedis = (primary: Redis | Cluster, secondaries?: Array) => { + if (!secondaries || !secondaries.length) return primary; + const selectedReplica = secondaries[Math.floor(Math.random() * secondaries.length)]; + return selectedReplica; +}; + +interface TKeyStoreFactoryDTO extends TRedisConfigKeys { + REDIS_READ_REPLICAS?: { host: string; port: number }[]; +} + +export const keyStoreFactory = ( + redisConfigKeys: TKeyStoreFactoryDTO, + keyValueStoreDAL: TKeyValueStoreDALFactory +): TKeyStoreFactory => { + const primaryRedis = buildRedisFromConfig(redisConfigKeys); + const redisReadReplicas = redisConfigKeys.REDIS_READ_REPLICAS?.map((el) => { + if (redisConfigKeys.REDIS_URL) { + const primaryNode = new URL(redisConfigKeys?.REDIS_URL); + primaryNode.hostname = el.host; + primaryNode.port = String(el.port); + return buildRedisFromConfig({ ...redisConfigKeys, REDIS_URL: primaryNode.toString() }); + } + + if (redisConfigKeys.REDIS_SENTINEL_HOSTS) { + return buildRedisFromConfig({ ...redisConfigKeys, REDIS_SENTINEL_HOSTS: [el] }); + } + + return buildRedisFromConfig({ ...redisConfigKeys, REDIS_CLUSTER_HOSTS: [el] }); + }); + const redisLock = new Redlock([primaryRedis], { retryCount: 2, retryDelay: 200 }); const setItem = async (key: string, value: string | number | Buffer, prefix?: string) => - redis.set(prefix ? `${prefix}:${key}` : key, value); + primaryRedis.set(prefix ? `${prefix}:${key}` : key, value); - const getItem = async (key: string, prefix?: string) => redis.get(prefix ? `${prefix}:${key}` : key); + const getItem = async (key: string, prefix?: string) => + pickPrimaryOrSecondaryRedis(primaryRedis, redisReadReplicas).get(prefix ? `${prefix}:${key}` : key); const getItems = async (keys: string[], prefix?: string) => - redis.mget(keys.map((key) => (prefix ? `${prefix}:${key}` : key))); + pickPrimaryOrSecondaryRedis(primaryRedis, redisReadReplicas).mget( + keys.map((key) => (prefix ? `${prefix}:${key}` : key)) + ); const setItemWithExpiry = async ( key: string, expiryInSeconds: number | string, value: string | number | Buffer, prefix?: string - ) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", expiryInSeconds); + ) => primaryRedis.set(prefix ? `${prefix}:${key}` : key, value, "EX", expiryInSeconds); - const deleteItem = async (key: string) => redis.del(key); + const deleteItem = async (key: string) => primaryRedis.del(key); const deleteItemsByKeyIn = async (keys: string[]) => { if (keys.length === 0) return 0; - return redis.del(keys); + return primaryRedis.del(keys); }; const deleteItems = async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }: TDeleteItems) => { @@ -134,12 +178,12 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac do { // Await in loop is needed so that Redis is not overwhelmed // eslint-disable-next-line no-await-in-loop - const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); // Count should be 1000 - 5000 for prod loads + const [nextCursor, keys] = await primaryRedis.scan(cursor, "MATCH", pattern, "COUNT", 1000); // Count should be 1000 - 5000 for prod loads cursor = nextCursor; for (let i = 0; i < keys.length; i += batchSize) { const batch = keys.slice(i, i + batchSize); - const pipeline = redis.pipeline(); + const pipeline = primaryRedis.pipeline(); for (const key of batch) { pipeline.unlink(key); } @@ -155,9 +199,41 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac return totalDeleted; }; - const incrementBy = async (key: string, value: number) => redis.incrby(key, value); + const incrementBy = async (key: string, value: number) => primaryRedis.incrby(key, value); - const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds); + const setExpiry = async (key: string, expiryInSeconds: number) => primaryRedis.expire(key, expiryInSeconds); + + const getKeysByPattern = async (pattern: string, limit?: number) => { + let cursor = "0"; + const allKeys: string[] = []; + + do { + // eslint-disable-next-line no-await-in-loop + const [nextCursor, keys] = await pickPrimaryOrSecondaryRedis(primaryRedis, redisReadReplicas).scan( + cursor, + "MATCH", + pattern, + "COUNT", + 1000 + ); + cursor = nextCursor; + allKeys.push(...keys); + + if (limit && allKeys.length >= limit) { + return allKeys.slice(0, limit); + } + } while (cursor !== "0"); + + return allKeys; + }; + + const pgIncrementBy: TKeyStoreFactory["pgIncrementBy"] = async (key, { incr = 1, tx, expiry }) => { + const expiresAt = expiry ? new Date(Date.now() + ms(expiry)) : undefined; + return keyValueStoreDAL.incrementBy(key, { incr, expiresAt, tx }); + }; + + const pgGetIntItem = async (key: string, prefix?: string) => + keyValueStoreDAL.findOneInt(prefix ? `${prefix}:${key}` : key); const waitTillReady = async ({ key, @@ -182,24 +258,6 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac } }; - const getKeysByPattern = async (pattern: string, limit?: number) => { - let cursor = "0"; - const allKeys: string[] = []; - - do { - // eslint-disable-next-line no-await-in-loop - const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); - cursor = nextCursor; - allKeys.push(...keys); - - if (limit && allKeys.length >= limit) { - return allKeys.slice(0, limit); - } - } while (cursor !== "0"); - - return allKeys; - }; - return { setItem, getItem, @@ -214,6 +272,8 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac waitTillReady, getKeysByPattern, deleteItemsByKeyIn, - getItems + getItems, + pgGetIntItem, + pgIncrementBy }; }; diff --git a/backend/src/keystore/memory.ts b/backend/src/keystore/memory.ts index cf9ba83bd..2f9b77ced 100644 --- a/backend/src/keystore/memory.ts +++ b/backend/src/keystore/memory.ts @@ -53,6 +53,15 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => { } return null; }, + pgGetIntItem: async (key) => { + const value = store[key]; + if (typeof value === "number") { + return Number(value); + } + }, + pgIncrementBy: async () => { + return 1; + }, incrementBy: async () => { return 1; }, diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index d61694d8f..53a2ca993 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -242,7 +242,12 @@ export const LDAP_AUTH = { accessTokenTTL: "The lifetime for an access token in seconds.", accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.", accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.", - accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from." + accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", + lockoutEnabled: "Whether the lockout feature is enabled.", + lockoutThreshold: "The amount of times login must fail before locking the identity auth method.", + lockoutDurationSeconds: "How long an identity auth method lockout lasts.", + lockoutCounterResetSeconds: + "How long to wait from the most recent failed login until resetting the lockout counter." }, UPDATE: { identityId: "The ID of the identity to update the configuration for.", @@ -257,13 +262,21 @@ export const LDAP_AUTH = { accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.", accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.", accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", - templateId: "The ID of the identity auth template to update the configuration to." + templateId: "The ID of the identity auth template to update the configuration to.", + lockoutEnabled: "Whether the lockout feature is enabled.", + lockoutThreshold: "The amount of times login must fail before locking the identity auth method.", + lockoutDurationSeconds: "How long an identity auth method lockout lasts.", + lockoutCounterResetSeconds: + "How long to wait from the most recent failed login until resetting the lockout counter." }, RETRIEVE: { identityId: "The ID of the identity to retrieve the configuration for." }, REVOKE: { identityId: "The ID of the identity to revoke the configuration for." + }, + CLEAR_CLIENT_LOCKOUTS: { + identityId: "The ID of the identity to clear the client lockouts from." } } as const; @@ -711,13 +724,13 @@ export const PROJECTS = { template: "The name of the project template, if specified, to apply to this project." }, DELETE: { - workspaceId: "The ID of the project to delete." + projectId: "The ID of the project to delete." }, GET: { - workspaceId: "The ID of the project." + projectId: "The ID of the project." }, UPDATE: { - workspaceId: "The ID of the project to update.", + projectId: "The ID of the project to update.", name: "The new name of the project.", projectDescription: "An optional description label for the project.", autoCapitalization: "Disable or enable auto-capitalization for the project.", @@ -729,10 +742,10 @@ export const PROJECTS = { secretDetectionIgnoreValues: "The list of secret values to ignore for secret detection." }, GET_KEY: { - workspaceId: "The ID of the project to get the key from." + projectId: "The ID of the project to get the key from." }, GET_SNAPSHOTS: { - workspaceId: "The ID of the project to get snapshots from.", + projectId: "The ID of the project to get snapshots from.", environment: "The environment to get snapshots from.", path: "The secret path to get snapshots from.", offset: "The offset to start from. If you enter 10, it will start from the 10th snapshot.", @@ -759,10 +772,10 @@ export const PROJECTS = { projectId: "The ID of the project to list groups for." }, LIST_INTEGRATION: { - workspaceId: "The ID of the project to list integrations for." + projectId: "The ID of the project to list integrations for." }, LIST_INTEGRATION_AUTHORIZATION: { - workspaceId: "The ID of the project to list integration auths for." + projectId: "The ID of the project to list integration auths for." }, LIST_SSH_CAS: { projectId: "The ID of the project to list SSH CAs for." @@ -815,15 +828,15 @@ export const PROJECT_USERS = { usernames: "A list of usernames to remove from the project." }, GET_USER_MEMBERSHIPS: { - workspaceId: "The ID of the project to get memberships from." + projectId: "The ID of the project to get memberships from." }, GET_USER_MEMBERSHIP: { - workspaceId: "The ID of the project to get memberships from.", + projectId: "The ID of the project to get memberships from.", membershipId: "The ID of the user's project membership.", username: "The username to get project membership of. Email is the default username." }, UPDATE_USER_MEMBERSHIP: { - workspaceId: "The ID of the project to update the membership for.", + projectId: "The ID of the project to update the membership for.", membershipId: "The ID of the membership to update.", roles: "A list of roles to update the membership to." } @@ -877,31 +890,31 @@ export const PROJECT_IDENTITIES = { export const ENVIRONMENTS = { CREATE: { - workspaceId: "The ID of the project to create the environment in.", + projectId: "The ID of the project to create the environment in.", name: "The name of the environment to create.", slug: "The slug of the environment to create.", position: "The position of the environment. The lowest number will be displayed as the first environment." }, UPDATE: { - workspaceId: "The ID of the project to update the environment in.", + projectId: "The ID of the project to update the environment in.", id: "The ID of the environment to update.", name: "The new name of the environment.", slug: "The new slug of the environment.", position: "The new position of the environment. The lowest number will be displayed as the first environment." }, DELETE: { - workspaceId: "The ID of the project to delete the environment from.", + projectId: "The ID of the project to delete the environment from.", id: "The ID of the environment to delete." }, GET: { - workspaceId: "The ID of the project the environment belongs to.", + projectId: "The ID of the project the environment belongs to.", id: "The ID of the environment to fetch." } } as const; export const FOLDERS = { LIST: { - workspaceId: "The ID of the project to list folders from.", + projectId: "The ID of the project to list folders from.", environment: "The slug of the environment to list folders from.", path: "The path to list folders from.", directory: "The directory to list folders from. (Deprecated in favor of path)", @@ -913,7 +926,7 @@ export const FOLDERS = { folderId: "The ID of the folder to get details." }, CREATE: { - workspaceId: "The ID of the project to create the folder in.", + projectId: "The ID of the project to create the folder in.", environment: "The slug of the environment to create the folder in.", name: "The name of the folder to create.", path: "The path of the folder to create.", @@ -927,12 +940,12 @@ export const FOLDERS = { path: "The path of the folder to update.", directory: "The new directory of the folder to update. (Deprecated in favor of path)", projectSlug: "The slug of the project where the folder is located.", - workspaceId: "The ID of the project where the folder is located.", + projectId: "The ID of the project where the folder is located.", description: "An optional description label for the folder." }, DELETE: { folderIdOrName: "The ID or name of the folder to delete.", - workspaceId: "The ID of the project to delete the folder from.", + projectId: "The ID of the project to delete the folder from.", environment: "The slug of the environment where the folder is located.", directory: "The directory of the folder to delete. (Deprecated in favor of path)", path: "The path of the folder to delete." @@ -964,7 +977,7 @@ export const RAW_SECRETS = { expand: "Whether or not to expand secret references.", recursive: "Whether or not to fetch all secrets from the specified base path, and all of its subdirectories. Note, the max depth is 20 deep.", - workspaceId: "The ID of the project to list secrets from.", + projectId: "The ID of the project to list secrets from.", workspaceSlug: "The slug of the project to list secrets from. This parameter is only applicable by machine identities.", environment: "The slug of the environment to list secrets from.", @@ -984,7 +997,7 @@ export const RAW_SECRETS = { secretValue: "The value of the secret to create.", skipMultilineEncoding: "Skip multiline encoding for the secret value.", type: "The type of the secret to create.", - workspaceId: "The ID of the project to create the secret in.", + projectId: "The ID of the project to create the secret in.", tagIds: "The ID of the tags to be attached to the created secret.", secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days.", secretReminderNote: "Note to be attached in notification email." @@ -992,7 +1005,7 @@ export const RAW_SECRETS = { GET: { expand: "Whether or not to expand secret references.", secretName: "The name of the secret to get.", - workspaceId: "The ID of the project to get the secret from.", + projectId: "The ID of the project to get the secret from.", workspaceSlug: "The slug of the project to get the secret from.", environment: "The slug of the environment to get the secret from.", secretPath: "The path of the secret to get.", @@ -1011,7 +1024,7 @@ export const RAW_SECRETS = { skipMultilineEncoding: "Skip multiline encoding for the secret value.", type: "The type of the secret to update.", projectSlug: "The slug of the project to update the secret in.", - workspaceId: "The ID of the project to update the secret in.", + projectId: "The ID of the project to update the secret in.", tagIds: "The ID of the tags to be attached to the updated secret.", secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days.", secretReminderNote: "Note to be attached in notification email.", @@ -1025,11 +1038,11 @@ export const RAW_SECRETS = { secretPath: "The path of the secret.", type: "The type of the secret to delete.", projectSlug: "The slug of the project to delete the secret in.", - workspaceId: "The ID of the project where the secret is located." + projectId: "The ID of the project where the secret is located." }, GET_REFERENCE_TREE: { secretName: "The name of the secret to get the reference tree for.", - workspaceId: "The ID of the project where the secret is located.", + projectId: "The ID of the project where the secret is located.", environment: "The slug of the environment where the the secret is located.", secretPath: "The folder path where the secret is located." }, @@ -1043,7 +1056,7 @@ export const RAW_SECRETS = { export const SECRET_IMPORTS = { LIST: { - workspaceId: "The ID of the project to list secret imports from.", + projectId: "The ID of the project to list secret imports from.", environment: "The slug of the environment to list secret imports from.", path: "The path to list secret imports from." }, @@ -1053,7 +1066,7 @@ export const SECRET_IMPORTS = { CREATE: { environment: "The slug of the environment to import into.", path: "The path to import into.", - workspaceId: "The ID of the project you are working in.", + projectId: "The ID of the project you are working in.", isReplication: "When true, secrets from the source will be automatically sent to the destination. If approval policies exist at the destination, the secrets will be sent as approval requests instead of being applied immediately.", import: { @@ -1070,10 +1083,10 @@ export const SECRET_IMPORTS = { position: "The new position of the secret import. The lowest number will be displayed as the first import." }, path: "The path of the secret import to update.", - workspaceId: "The ID of the project where the secret import is located." + projectId: "The ID of the project where the secret import is located." }, DELETE: { - workspaceId: "The ID of the project to delete the secret import from.", + projectId: "The ID of the project to delete the secret import from.", secretImportId: "The ID of the secret import to delete.", environment: "The slug of the environment where the secret import is located.", path: "The path of the secret import to delete." @@ -2185,11 +2198,15 @@ export const CertificateAuthorities = { }; export const AppConnections = { + LIST: (app?: AppConnection) => ({ + projectId: `The ID of the project to list ${app ? APP_CONNECTION_NAME_MAP[app] : "App"} Connections from.` + }), GET_BY_ID: (app: AppConnection) => ({ connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.` }), GET_BY_NAME: (app: AppConnection) => ({ - connectionName: `The name of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.` + connectionName: `The name of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`, + projectId: `The project ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection is associated with. Leave unspecified to get organization-level connections.` }), CREATE: (app: AppConnection) => { const appName = APP_CONNECTION_NAME_MAP[app]; @@ -2198,7 +2215,8 @@ export const AppConnections = { description: `An optional description for the ${appName} Connection.`, credentials: `The credentials used to connect with ${appName}.`, method: `The method used to authenticate with ${appName}.`, - isPlatformManagedCredentials: `Whether or not the ${appName} Connection credentials should be managed by Infisical. Once enabled this cannot be reversed.` + isPlatformManagedCredentials: `Whether or not the ${appName} Connection credentials should be managed by Infisical. Once enabled this cannot be reversed.`, + projectId: `The ID of the project to create the ${appName} Connection in.` }; }, UPDATE: (app: AppConnection) => { diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 586a69655..da0dd61a7 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -37,6 +37,8 @@ const envSchema = z .default("false") .transform((el) => el === "true"), REDIS_URL: zpStr(z.string().optional()), + REDIS_USERNAME: zpStr(z.string().optional()), + REDIS_PASSWORD: zpStr(z.string().optional()), REDIS_SENTINEL_HOSTS: zpStr( z .string() @@ -49,6 +51,28 @@ const envSchema = z REDIS_SENTINEL_ENABLE_TLS: zodStrBool.optional().describe("Whether to use TLS/SSL for Redis Sentinel connection"), REDIS_SENTINEL_USERNAME: zpStr(z.string().optional().describe("Authentication username for Redis Sentinel")), REDIS_SENTINEL_PASSWORD: zpStr(z.string().optional().describe("Authentication password for Redis Sentinel")), + REDIS_CLUSTER_HOSTS: zpStr( + z + .string() + .optional() + .describe("Comma-separated list of Redis Cluster host:port pairs. Eg: 192.168.65.254:6379,192.168.65.254:6380") + ), + REDIS_READ_REPLICAS: zpStr( + z + .string() + .optional() + .describe( + "Comma-separated list of Redis read replicas host:port pairs. Eg: 192.168.65.254:6379,192.168.65.254:6380" + ) + ), + REDIS_CLUSTER_ENABLE_TLS: z + .enum(["true", "false"]) + .default("false") + .transform((el) => el === "true"), + REDIS_CLUSTER_AWS_ELASTICACHE_DNS_LOOKUP_MODE: z + .enum(["true", "false"]) + .default("false") + .transform((el) => el === "true"), HOST: zpStr(z.string().default("localhost")), DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default( `postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}` @@ -218,6 +242,8 @@ const envSchema = z ), PARAMS_FOLDER_SECRET_DETECTION_ENTROPY: z.coerce.number().optional().default(3.7), + INFISICAL_PRIMARY_INSTANCE_URL: zpStr(z.string().optional()), + // HSM HSM_LIB_PATH: zpStr(z.string().optional()), HSM_PIN: zpStr(z.string().optional()), @@ -233,6 +259,8 @@ const envSchema = z GATEWAY_RELAY_REALM: zpStr(z.string().optional()), GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()), + RELAY_AUTH_SECRET: zpStr(z.string().optional()), + DYNAMIC_SECRET_ALLOW_INTERNAL_IP: zodStrBool.default("false"), DYNAMIC_SECRET_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()).default( process.env.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID @@ -335,8 +363,8 @@ const envSchema = z "Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined." ) .refine( - (data) => Boolean(data.REDIS_URL) || Boolean(data.REDIS_SENTINEL_HOSTS), - "Either REDIS_URL or REDIS_SENTINEL_HOSTS must be defined." + (data) => Boolean(data.REDIS_URL) || Boolean(data.REDIS_SENTINEL_HOSTS) || Boolean(data.REDIS_CLUSTER_HOSTS), + "Either REDIS_URL, REDIS_SENTINEL_HOSTS or REDIS_CLUSTER_HOSTS must be defined." ) .transform((data) => ({ ...data, @@ -346,7 +374,7 @@ const envSchema = z : undefined, isCloud: Boolean(data.LICENSE_SERVER_KEY), isSmtpConfigured: Boolean(data.SMTP_HOST), - isRedisConfigured: Boolean(data.REDIS_URL || data.REDIS_SENTINEL_HOSTS), + isRedisConfigured: Boolean(data.REDIS_URL || data.REDIS_SENTINEL_HOSTS || data.REDIS_CLUSTER_HOSTS), isDevelopmentMode: data.NODE_ENV === "development", isTestMode: data.NODE_ENV === "test", isRotationDevelopmentMode: @@ -361,6 +389,18 @@ const envSchema = z const [host, port] = el.trim().split(":"); return { host: host.trim(), port: Number(port.trim()) }; }), + REDIS_CLUSTER_HOSTS: data.REDIS_CLUSTER_HOSTS?.trim() + ?.split(",") + .map((el) => { + const [host, port] = el.trim().split(":"); + return { host: host.trim(), port: Number(port.trim()) }; + }), + REDIS_READ_REPLICAS: data.REDIS_READ_REPLICAS?.trim() + ?.split(",") + .map((el) => { + const [host, port] = el.trim().split(":"); + return { host: host.trim(), port: Number(port.trim()) }; + }), isSecretScanningConfigured: Boolean(data.SECRET_SCANNING_GIT_APP_ID) && Boolean(data.SECRET_SCANNING_PRIVATE_KEY) && @@ -372,6 +412,7 @@ const envSchema = z Boolean(data.INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID) && Boolean(data.INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET) && Boolean(data.INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET), + isSecondaryInstance: Boolean(data.INFISICAL_PRIMARY_INSTANCE_URL), isHsmConfigured: Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined, samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG, diff --git a/backend/src/lib/config/redis.ts b/backend/src/lib/config/redis.ts index 987518dd5..5187f740a 100644 --- a/backend/src/lib/config/redis.ts +++ b/backend/src/lib/config/redis.ts @@ -2,6 +2,14 @@ import { Redis } from "ioredis"; export type TRedisConfigKeys = Partial<{ REDIS_URL: string; + REDIS_USERNAME: string; + REDIS_PASSWORD: string; + + REDIS_CLUSTER_HOSTS: { host: string; port: number }[]; + REDIS_CLUSTER_ENABLE_TLS: boolean; + // ref: https://github.com/redis/ioredis?tab=readme-ov-file#special-note-aws-elasticache-clusters-with-tls + REDIS_CLUSTER_AWS_ELASTICACHE_DNS_LOOKUP_MODE: boolean; + REDIS_SENTINEL_HOSTS: { host: string; port: number }[]; REDIS_SENTINEL_MASTER_NAME: string; REDIS_SENTINEL_ENABLE_TLS: boolean; @@ -12,6 +20,19 @@ export type TRedisConfigKeys = Partial<{ export const buildRedisFromConfig = (cfg: TRedisConfigKeys) => { if (cfg.REDIS_URL) return new Redis(cfg.REDIS_URL, { maxRetriesPerRequest: null }); + if (cfg.REDIS_CLUSTER_HOSTS) { + return new Redis.Cluster(cfg.REDIS_CLUSTER_HOSTS, { + dnsLookup: cfg.REDIS_CLUSTER_AWS_ELASTICACHE_DNS_LOOKUP_MODE + ? (address, callback) => callback(null, address) + : undefined, + redisOptions: { + username: cfg.REDIS_USERNAME, + password: cfg.REDIS_PASSWORD, + tls: cfg?.REDIS_CLUSTER_ENABLE_TLS ? {} : undefined + } + }); + } + return new Redis({ // refine at tope will catch this case sentinels: cfg.REDIS_SENTINEL_HOSTS!, @@ -19,6 +40,8 @@ export const buildRedisFromConfig = (cfg: TRedisConfigKeys) => { maxRetriesPerRequest: null, sentinelUsername: cfg.REDIS_SENTINEL_USERNAME, sentinelPassword: cfg.REDIS_SENTINEL_PASSWORD, - enableTLSForSentinelMode: cfg.REDIS_SENTINEL_ENABLE_TLS + enableTLSForSentinelMode: cfg.REDIS_SENTINEL_ENABLE_TLS, + username: cfg.REDIS_USERNAME, + password: cfg.REDIS_PASSWORD }); }; diff --git a/backend/src/lib/crypto/cryptography/crypto.ts b/backend/src/lib/crypto/cryptography/crypto.ts index b8fc45645..45c7a1986 100644 --- a/backend/src/lib/crypto/cryptography/crypto.ts +++ b/backend/src/lib/crypto/cryptography/crypto.ts @@ -250,8 +250,11 @@ const cryptographyFactory = () => { }; }; - const encryptWithRootEncryptionKey = (data: string) => { - const appCfg = getConfig(); + const encryptWithRootEncryptionKey = ( + data: string, + appCfgOverride?: Pick + ) => { + const appCfg = appCfgOverride || getConfig(); const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY; const encryptionKey = appCfg.ENCRYPTION_KEY; @@ -421,7 +424,8 @@ const cryptographyFactory = () => { constants: crypto.constants, X509Certificate: crypto.X509Certificate, KeyObject: crypto.KeyObject, - Hash: crypto.Hash + Hash: crypto.Hash, + timingSafeEqual: crypto.timingSafeEqual } }; }; diff --git a/backend/src/lib/gateway-v2/gateway-v2.ts b/backend/src/lib/gateway-v2/gateway-v2.ts new file mode 100644 index 000000000..e6e873f11 --- /dev/null +++ b/backend/src/lib/gateway-v2/gateway-v2.ts @@ -0,0 +1,281 @@ +import net from "node:net"; +import tls from "node:tls"; + +import axios from "axios"; +import https from "https"; + +import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; +import { splitPemChain } from "@app/services/certificate/certificate-fns"; + +import { BadRequestError } from "../errors"; +import { GatewayProxyProtocol } from "../gateway/types"; +import { logger } from "../logger"; + +interface IGatewayRelayServer { + server: net.Server; + port: number; + cleanup: () => Promise; + getRelayError: () => string; +} + +const createRelayConnection = async ({ + relayHost, + clientCertificate, + clientPrivateKey, + serverCertificateChain +}: { + relayHost: string; + clientCertificate: string; + clientPrivateKey: string; + serverCertificateChain: string; +}): Promise => { + const [targetHost] = await verifyHostInputValidity(relayHost); + const [, portStr] = relayHost.split(":"); + const port = parseInt(portStr, 10) || 8443; + + const serverCAs = splitPemChain(serverCertificateChain); + const tlsOptions: tls.ConnectionOptions = { + host: targetHost, + servername: relayHost, + port, + cert: clientCertificate, + key: clientPrivateKey, + ca: serverCAs, + minVersion: "TLSv1.2", + rejectUnauthorized: true + }; + + return new Promise((resolve, reject) => { + try { + const socket = tls.connect(tlsOptions, () => { + logger.info("Relay TLS connection established successfully"); + resolve(socket); + }); + + socket.on("error", (err: Error) => { + reject(new Error(`TLS connection error: ${err.message}`)); + }); + + socket.on("close", (hadError: boolean) => { + if (hadError) { + logger.error("TLS connection closed with error"); + } + }); + + socket.on("timeout", () => { + logger.error(`TLS connection timeout after 30 seconds`); + socket.destroy(); + reject(new Error("TLS connection timeout")); + }); + + socket.setTimeout(30000); + } catch (error: unknown) { + reject(new Error(`Failed to create TLS connection: ${error instanceof Error ? error.message : String(error)}`)); + } + }); +}; + +const createGatewayConnection = async ( + relayConn: net.Socket, + gateway: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string }, + protocol: GatewayProxyProtocol +): Promise => { + const protocolToAlpn = { + [GatewayProxyProtocol.Http]: "infisical-http-proxy", + [GatewayProxyProtocol.Tcp]: "infisical-tcp-proxy", + [GatewayProxyProtocol.Ping]: "infisical-ping" + }; + + const tlsOptions: tls.ConnectionOptions = { + socket: relayConn, + cert: gateway.clientCertificate, + key: gateway.clientPrivateKey, + ca: splitPemChain(gateway.serverCertificateChain), + minVersion: "TLSv1.2", + maxVersion: "TLSv1.3", + rejectUnauthorized: true, + ALPNProtocols: [protocolToAlpn[protocol]] + }; + + return new Promise((resolve, reject) => { + try { + const gatewaySocket = tls.connect(tlsOptions, () => { + if (!gatewaySocket.authorized) { + const error = gatewaySocket.authorizationError; + gatewaySocket.destroy(); + reject(new Error(`Gateway TLS authorization failed: ${error?.message}`)); + return; + } + + logger.info("Gateway mTLS connection established successfully"); + resolve(gatewaySocket); + }); + + gatewaySocket.on("error", (err: Error) => { + reject(new Error(`Failed to establish gateway mTLS: ${err.message}`)); + }); + + gatewaySocket.setTimeout(30000); + gatewaySocket.on("timeout", () => { + gatewaySocket.destroy(); + reject(new Error("Gateway connection timeout")); + }); + } catch (error: unknown) { + reject( + new Error(`Failed to create gateway TLS connection: ${error instanceof Error ? error.message : String(error)}`) + ); + } + }); +}; + +const setupRelayServer = async ({ + protocol, + relayHost, + gateway, + relay, + httpsAgent +}: { + protocol: GatewayProxyProtocol; + relayHost: string; + gateway: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string }; + relay: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string }; + httpsAgent?: https.Agent; +}): Promise => { + const relayErrorMsg: string[] = []; + + return new Promise((resolve, reject) => { + const server = net.createServer(); + + server.on("connection", (clientConn) => { + void (async () => { + try { + clientConn.setKeepAlive(true, 30000); + clientConn.setNoDelay(true); + + // Stage 1: Connect to relay with TLS + const relayConn = await createRelayConnection({ + relayHost, + clientCertificate: relay.clientCertificate, + clientPrivateKey: relay.clientPrivateKey, + serverCertificateChain: relay.serverCertificateChain + }); + + // Stage 2: Establish mTLS connection to gateway through the relay + const gatewayConn = await createGatewayConnection(relayConn, gateway, protocol); + + // Send protocol-specific configuration for HTTP requests + if (protocol === GatewayProxyProtocol.Http) { + if (httpsAgent) { + const agentOptions = httpsAgent.options; + if (agentOptions && agentOptions.ca) { + const caCert = Array.isArray(agentOptions.ca) ? agentOptions.ca.join("\n") : agentOptions.ca; + const caB64 = Buffer.from(caCert as string).toString("base64"); + const rejectUnauthorized = agentOptions.rejectUnauthorized !== false; + + const configCommand = `CONFIG ca=${caB64} verify=${rejectUnauthorized}\n`; + gatewayConn.write(Buffer.from(configCommand)); + } else { + // Send empty config to signal end of configuration + gatewayConn.write(Buffer.from("CONFIG\n")); + } + } else { + // Send empty config to signal end of configuration + gatewayConn.write(Buffer.from("CONFIG\n")); + } + } + + // Bidirectional data forwarding + clientConn.pipe(gatewayConn); + gatewayConn.pipe(clientConn); + + // Handle connection closure + clientConn.on("close", () => { + relayConn.destroy(); + gatewayConn.destroy(); + }); + + relayConn.on("close", () => { + clientConn.destroy(); + gatewayConn.destroy(); + }); + + gatewayConn.on("close", () => { + clientConn.destroy(); + relayConn.destroy(); + }); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + relayErrorMsg.push(errorMsg); + clientConn.destroy(); + } + })(); + }); + + server.on("error", (err) => { + reject(err); + }); + + server.listen(0, () => { + const address = server.address(); + if (!address || typeof address === "string") { + server.close(); + reject(new Error("Failed to get server port")); + return; + } + + resolve({ + server, + port: address.port, + cleanup: async () => { + try { + server.close(); + } catch (err) { + logger.debug("Error closing server:", err instanceof Error ? err.message : String(err)); + } + }, + getRelayError: () => relayErrorMsg.join(",") + }); + }); + }); +}; + +export const withGatewayV2Proxy = async ( + callback: (port: number) => Promise, + options: { + protocol: GatewayProxyProtocol; + relayHost: string; + gateway: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string }; + relay: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string }; + httpsAgent?: https.Agent; + } +): Promise => { + const { protocol, relayHost, gateway, relay, httpsAgent } = options; + + const { port, cleanup, getRelayError } = await setupRelayServer({ + protocol, + relayHost, + gateway, + relay, + httpsAgent + }); + + try { + // Execute the callback with the allocated port + return await callback(port); + } catch (err) { + const relayErrorMessage = getRelayError(); + if (relayErrorMessage) { + logger.error("Relay error:", relayErrorMessage); + } + logger.error("Gateway error:", err instanceof Error ? err.message : String(err)); + let errorMessage = relayErrorMessage || (err instanceof Error ? err.message : String(err)); + if (axios.isAxiosError(err) && (err.response?.data as { message?: string })?.message) { + errorMessage = (err.response?.data as { message: string }).message; + } + + throw new BadRequestError({ message: errorMessage }); + } finally { + // Ensure cleanup happens regardless of success or failure + await cleanup(); + } +}; diff --git a/backend/src/lib/gateway/types.ts b/backend/src/lib/gateway/types.ts index 8552fbf54..e9b8b7114 100644 --- a/backend/src/lib/gateway/types.ts +++ b/backend/src/lib/gateway/types.ts @@ -6,7 +6,8 @@ export type TGatewayTlsOptions = { ca: string; cert: string; key: string }; export enum GatewayProxyProtocol { Http = "http", - Tcp = "tcp" + Tcp = "tcp", + Ping = "ping" } export enum GatewayHttpProxyActions { diff --git a/backend/src/lib/ip/index.test.ts b/backend/src/lib/ip/index.test.ts deleted file mode 100644 index 5f3b64fac..000000000 --- a/backend/src/lib/ip/index.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { extractIPDetails, IPType, isValidCidr, isValidIp, isValidIpOrCidr } from "./index"; - -describe("IP Validation", () => { - describe("isValidIp", () => { - test("should validate IPv4 addresses with ports", () => { - expect(isValidIp("192.168.1.1:8080")).toBe(true); - expect(isValidIp("10.0.0.1:1234")).toBe(true); - expect(isValidIp("172.16.0.1:80")).toBe(true); - }); - - test("should validate IPv6 addresses with ports", () => { - expect(isValidIp("[2001:db8::1]:8080")).toBe(true); - expect(isValidIp("[fe80::1ff:fe23:4567:890a]:1234")).toBe(true); - expect(isValidIp("[::1]:80")).toBe(true); - }); - - test("should validate regular IPv4 addresses", () => { - expect(isValidIp("192.168.1.1")).toBe(true); - expect(isValidIp("10.0.0.1")).toBe(true); - expect(isValidIp("172.16.0.1")).toBe(true); - }); - - test("should validate regular IPv6 addresses", () => { - expect(isValidIp("2001:db8::1")).toBe(true); - expect(isValidIp("fe80::1ff:fe23:4567:890a")).toBe(true); - expect(isValidIp("::1")).toBe(true); - }); - - test("should reject invalid IP addresses", () => { - expect(isValidIp("256.256.256.256")).toBe(false); - expect(isValidIp("192.168.1")).toBe(false); - expect(isValidIp("192.168.1.1.1")).toBe(false); - expect(isValidIp("2001:db8::1::1")).toBe(false); - expect(isValidIp("invalid")).toBe(false); - }); - - test("should reject malformed IP addresses with ports", () => { - expect(isValidIp("192.168.1.1:")).toBe(false); - expect(isValidIp("192.168.1.1:abc")).toBe(false); - expect(isValidIp("[2001:db8::1]")).toBe(false); - expect(isValidIp("[2001:db8::1]:")).toBe(false); - expect(isValidIp("[2001:db8::1]:abc")).toBe(false); - }); - }); - - describe("isValidCidr", () => { - test("should validate IPv4 CIDR blocks", () => { - expect(isValidCidr("192.168.1.0/24")).toBe(true); - expect(isValidCidr("10.0.0.0/8")).toBe(true); - expect(isValidCidr("172.16.0.0/16")).toBe(true); - }); - - test("should validate IPv6 CIDR blocks", () => { - expect(isValidCidr("2001:db8::/32")).toBe(true); - expect(isValidCidr("fe80::/10")).toBe(true); - expect(isValidCidr("::/0")).toBe(true); - }); - - test("should reject invalid CIDR blocks", () => { - expect(isValidCidr("192.168.1.0/33")).toBe(false); - expect(isValidCidr("2001:db8::/129")).toBe(false); - expect(isValidCidr("192.168.1.0/abc")).toBe(false); - expect(isValidCidr("invalid/24")).toBe(false); - }); - }); - - describe("isValidIpOrCidr", () => { - test("should validate both IP addresses and CIDR blocks", () => { - expect(isValidIpOrCidr("192.168.1.1")).toBe(true); - expect(isValidIpOrCidr("2001:db8::1")).toBe(true); - expect(isValidIpOrCidr("192.168.1.0/24")).toBe(true); - expect(isValidIpOrCidr("2001:db8::/32")).toBe(true); - }); - - test("should reject invalid inputs", () => { - expect(isValidIpOrCidr("invalid")).toBe(false); - expect(isValidIpOrCidr("192.168.1.0/33")).toBe(false); - expect(isValidIpOrCidr("2001:db8::/129")).toBe(false); - }); - }); - - describe("extractIPDetails", () => { - test("should extract IPv4 address details", () => { - const result = extractIPDetails("192.168.1.1"); - expect(result).toEqual({ - ipAddress: "192.168.1.1", - type: IPType.IPV4 - }); - }); - - test("should extract IPv6 address details", () => { - const result = extractIPDetails("2001:db8::1"); - expect(result).toEqual({ - ipAddress: "2001:db8::1", - type: IPType.IPV6 - }); - }); - - test("should extract IPv4 CIDR details", () => { - const result = extractIPDetails("192.168.1.0/24"); - expect(result).toEqual({ - ipAddress: "192.168.1.0", - type: IPType.IPV4, - prefix: 24 - }); - }); - - test("should extract IPv6 CIDR details", () => { - const result = extractIPDetails("2001:db8::/32"); - expect(result).toEqual({ - ipAddress: "2001:db8::", - type: IPType.IPV6, - prefix: 32 - }); - }); - - test("should throw error for invalid IP", () => { - expect(() => extractIPDetails("invalid")).toThrow("Failed to extract IP details"); - }); - }); -}); diff --git a/backend/src/lib/ip/index.ts b/backend/src/lib/ip/index.ts index 9b594a583..0b35a2759 100644 --- a/backend/src/lib/ip/index.ts +++ b/backend/src/lib/ip/index.ts @@ -1,7 +1,5 @@ import net from "node:net"; -import RE2 from "re2"; - import { ForbiddenRequestError } from "../errors"; export enum IPType { @@ -9,55 +7,25 @@ export enum IPType { IPV6 = "ipv6" } -const PORT_REGEX = new RE2(/^\d+$/); - -/** - * Strips port from IP address if present. - * Handles both IPv4 (e.g. 1.2.3.4:1234) and IPv6 (e.g. [2001:db8::1]:8080) formats. - * Returns the IP address without port and a boolean indicating if a port was present. - */ -const stripPort = (ip: string): { ipAddress: string } => { - // Handle IPv6 with port (e.g. [2001:db8::1]:8080) - if (ip.startsWith("[") && ip.includes("]:")) { - const endBracketIndex = ip.indexOf("]"); - if (endBracketIndex === -1) return { ipAddress: ip }; - const ipPart = ip.slice(1, endBracketIndex); - const portPart = ip.slice(endBracketIndex + 2); - if (!portPart || !PORT_REGEX.test(portPart)) return { ipAddress: ip }; - return { ipAddress: ipPart }; - } - - // Handle IPv4 with port (e.g. 1.2.3.4:1234) - if (ip.includes(":")) { - const [ipPart, portPart] = ip.split(":"); - if (!portPart || !PORT_REGEX.test(portPart)) return { ipAddress: ip }; - return { ipAddress: ipPart }; - } - - return { ipAddress: ip }; -}; - /** * Return details of IP [ip]: * - If [ip] is a specific IP address then return the IPv4/IPv6 address * - If [ip] is a subnet then return the network IPv4/IPv6 address and prefix */ export const extractIPDetails = (ip: string) => { - const { ipAddress } = stripPort(ip); - - if (net.isIPv4(ipAddress)) + if (net.isIPv4(ip)) return { - ipAddress, + ipAddress: ip, type: IPType.IPV4 }; - if (net.isIPv6(ipAddress)) + if (net.isIPv6(ip)) return { - ipAddress, + ipAddress: ip, type: IPType.IPV6 }; - const [ipNet, prefix] = ipAddress.split("/"); + const [ipNet, prefix] = ip.split("/"); let type; switch (net.isIP(ipNet)) { @@ -89,8 +57,7 @@ export const extractIPDetails = (ip: string) => { * */ export const isValidCidr = (cidr: string): boolean => { - const { ipAddress } = stripPort(cidr); - const [ip, prefix] = ipAddress.split("/"); + const [ip, prefix] = cidr.split("/"); const prefixNum = parseInt(prefix, 10); @@ -123,15 +90,13 @@ export const isValidCidr = (cidr: string): boolean => { * */ export const isValidIpOrCidr = (ip: string): boolean => { - const { ipAddress } = stripPort(ip); - // if the string contains a slash, treat it as a CIDR block - if (ipAddress.includes("/")) { - return isValidCidr(ipAddress); + if (ip.includes("/")) { + return isValidCidr(ip); } // otherwise, treat it as a standalone IP address - if (net.isIPv4(ipAddress) || net.isIPv6(ipAddress)) { + if (net.isIPv4(ip) || net.isIPv6(ip)) { return true; } @@ -139,8 +104,7 @@ export const isValidIpOrCidr = (ip: string): boolean => { }; export const isValidIp = (ip: string) => { - const { ipAddress } = stripPort(ip); - return net.isIPv4(ipAddress) || net.isIPv6(ipAddress); + return net.isIPv4(ip) || net.isIPv6(ip); }; export type TIp = { @@ -148,7 +112,6 @@ export type TIp = { type: IPType; prefix: number; }; - /** * Validates the IP address [ipAddress] against the trusted IPs [trustedIps]. */ @@ -163,9 +126,8 @@ export const checkIPAgainstBlocklist = ({ ipAddress, trustedIps }: { ipAddress: } } - const { type, ipAddress: cleanIpAddress } = extractIPDetails(ipAddress); - - const check = blockList.check(cleanIpAddress, type); + const { type } = extractIPDetails(ipAddress); + const check = blockList.check(ipAddress, type); if (!check) throw new ForbiddenRequestError({ diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index 090df561a..499e7cb26 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -250,12 +250,12 @@ export const ormify = ( .returning("*"); if ($incr) { Object.entries($incr).forEach(([incrementField, incrementValue]) => { - void query.increment(incrementField, incrementValue); + void query.increment(incrementField, incrementValue as number); }); } if ($decr) { Object.entries($decr).forEach(([incrementField, incrementValue]) => { - void query.decrement(incrementField, incrementValue); + void query.decrement(incrementField, incrementValue as number); }); } const [docs] = await query; @@ -273,12 +273,12 @@ export const ormify = ( // increment and decrement operation in update if ($incr) { Object.entries($incr).forEach(([incrementField, incrementValue]) => { - void query.increment(incrementField, incrementValue); + void query.increment(incrementField, incrementValue as number); }); } if ($decr) { Object.entries($decr).forEach(([incrementField, incrementValue]) => { - void query.increment(incrementField, incrementValue); + void query.decrement(incrementField, incrementValue as number); }); } return (await query) as Tables[Tname]["base"][]; diff --git a/backend/src/main.ts b/backend/src/main.ts index 8af47eb0b..7be9f43ec 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -5,6 +5,7 @@ import "./lib/telemetry/instrumentation"; import dotenv from "dotenv"; import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; +import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; import { runMigrations } from "./auto-start-migrations"; import { initAuditLogDbConnection, initDbConnection } from "./db"; @@ -54,7 +55,8 @@ const run = async () => { await queue.initialize(); - const keyStore = keyStoreFactory(envConfig); + const keyValueStoreDAL = keyValueStoreDALFactory(db); + const keyStore = keyStoreFactory(envConfig, keyValueStoreDAL); const redis = buildRedisFromConfig(envConfig); const hsmModule = initializeHsmModule(envConfig); diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index f4c49d15a..5a7c92f22 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -23,6 +23,7 @@ import { logger } from "@app/lib/logger"; import { QueueWorkerProfile } from "@app/lib/types"; import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { ExternalPlatforms } from "@app/services/external-migration/external-migration-types"; +import { TCreateUserNotificationDTO } from "@app/services/notification/notification-types"; import { TFailedIntegrationSyncEmailsPayload, TIntegrationSyncPayload, @@ -67,7 +68,8 @@ export enum QueueName { SecretScanningV2 = "secret-scanning-v2", TelemetryAggregatedEvents = "telemetry-aggregated-events", DailyReminders = "daily-reminders", - SecretReminderMigration = "secret-reminder-migration" + SecretReminderMigration = "secret-reminder-migration", + UserNotification = "user-notification" } export enum QueueJobs { @@ -109,7 +111,8 @@ export enum QueueJobs { PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal", TelemetryAggregatedEvents = "telemetry-aggregated-events", DailyReminders = "daily-reminders", - SecretReminderMigration = "secret-reminder-migration" + SecretReminderMigration = "secret-reminder-migration", + UserNotification = "user-notification-job" } export type TQueueJobTypes = { @@ -313,6 +316,10 @@ export type TQueueJobTypes = { name: QueueJobs.TelemetryAggregatedEvents; payload: undefined; }; + [QueueName.UserNotification]: { + name: QueueJobs.UserNotification; + payload: { notifications: TCreateUserNotificationDTO[] }; + }; }; const SECRET_SCANNING_JOBS = [ @@ -415,6 +422,7 @@ export const queueServiceFactory = ( redisCfg: TRedisConfigKeys, { dbConnectionUrl, dbRootCert }: { dbConnectionUrl: string; dbRootCert?: string } ): TQueueServiceFactory => { + const isClusterMode = Boolean(redisCfg?.REDIS_CLUSTER_HOSTS); const connection = buildRedisFromConfig(redisCfg); const queueContainer = {} as Record< QueueName, @@ -457,6 +465,8 @@ export const queueServiceFactory = ( } queueContainer[name] = new Queue(name as string, { + // ref: docs.bullmq.io/bull/patterns/redis-cluster + prefix: isClusterMode ? `{${name}}` : undefined, ...queueSettings, ...(crypto.isFipsModeEnabled() ? { @@ -472,6 +482,7 @@ export const queueServiceFactory = ( const appCfg = getConfig(); if (appCfg.QUEUE_WORKERS_ENABLED && isQueueEnabled(name)) { workerContainer[name] = new Worker(name, jobFn, { + prefix: isClusterMode ? `{${name}}` : undefined, ...queueSettings, ...(crypto.isFipsModeEnabled() ? { diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 321a3656e..8cf23f703 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -12,7 +12,7 @@ import type { FastifyRateLimitOptions } from "@fastify/rate-limit"; import ratelimiter from "@fastify/rate-limit"; import { fastifyRequestContext } from "@fastify/request-context"; import fastify from "fastify"; -import { Redis } from "ioredis"; +import { Cluster, Redis } from "ioredis"; import { Knex } from "knex"; import { HsmModule } from "@app/ee/services/hsm/hsm-types"; @@ -43,7 +43,7 @@ type TMain = { queue: TQueueServiceFactory; keyStore: TKeyStoreFactory; hsmModule: HsmModule; - redis: Redis; + redis: Redis | Cluster; envConfig: TEnvConfig; superAdminDAL: TSuperAdminDALFactory; }; @@ -76,6 +76,7 @@ export const main = async ({ server.setValidatorCompiler(validatorCompiler); server.setSerializerCompiler(serializerCompiler); + // @ts-expect-error akhilmhdh: even on setting it fastify as Redis | Cluster it's throwing error server.decorate("redis", redis); server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => { try { diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 97c62b545..1bff11879 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -107,110 +107,122 @@ export const extractAuth = async (req: FastifyRequest, jwtSecret: string) => { }; // ! Important: You can only 100% count on the `req.permission.orgId` field being present when the auth method is Identity Access Token (Machine Identity). -export const injectIdentity = fp(async (server: FastifyZodProvider) => { - server.decorateRequest("auth", null); - server.addHook("onRequest", async (req) => { - const appCfg = getConfig(); +export const injectIdentity = fp( + async (server: FastifyZodProvider, opt: { shouldForwardWritesToPrimaryInstance?: boolean }) => { + server.decorateRequest("auth", null); + server.decorateRequest("shouldForwardWritesToPrimaryInstance", Boolean(opt.shouldForwardWritesToPrimaryInstance)); + server.addHook("onRequest", async (req) => { + const appCfg = getConfig(); - if (req.url.includes(".well-known/est") || req.url.includes("/api/v3/auth/")) { - return; - } - - // Authentication is handled on a route-level here. - if (req.url.includes("/api/v1/workflow-integrations/microsoft-teams/message-endpoint")) { - return; - } - - const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET); - - if (!authMode) return; - - switch (authMode) { - case AuthMode.JWT: { - const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token); - requestContext.set("orgId", orgId); - req.auth = { - authMode: AuthMode.JWT, - user, - userId: user.id, - tokenVersionId, - actor, - orgId: orgId as string, - authMethod: token.authMethod, - isMfaVerified: token.isMfaVerified, - token - }; - break; + if (opt.shouldForwardWritesToPrimaryInstance && req.method !== "GET") { + return; } - case AuthMode.IDENTITY_ACCESS_TOKEN: { - const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp); - const serverCfg = await getServerCfg(); - requestContext.set("orgId", identity.orgId); - req.auth = { - authMode: AuthMode.IDENTITY_ACCESS_TOKEN, - actor, - orgId: identity.orgId, - identityId: identity.identityId, - identityName: identity.name, - authMethod: null, - isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId), - token - }; - if (token?.identityAuth?.oidc) { - requestContext.set("identityAuthInfo", { - identityId: identity.identityId, - oidc: token?.identityAuth?.oidc - }); + + if (req.url.includes(".well-known/est") || req.url.includes("/api/v3/auth/")) { + return; + } + + // Authentication is handled on a route-level + if (req.url === "/api/v1/relays/register-instance-relay") { + return; + } + + // Authentication is handled on a route-level here. + if (req.url.includes("/api/v1/workflow-integrations/microsoft-teams/message-endpoint")) { + return; + } + + const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET); + + if (!authMode) return; + + switch (authMode) { + case AuthMode.JWT: { + const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token); + requestContext.set("orgId", orgId); + req.auth = { + authMode: AuthMode.JWT, + user, + userId: user.id, + tokenVersionId, + actor, + orgId: orgId as string, + authMethod: token.authMethod, + isMfaVerified: token.isMfaVerified, + token + }; + break; } - if (token?.identityAuth?.kubernetes) { - requestContext.set("identityAuthInfo", { + case AuthMode.IDENTITY_ACCESS_TOKEN: { + const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp); + const serverCfg = await getServerCfg(); + requestContext.set("orgId", identity.orgId); + req.auth = { + authMode: AuthMode.IDENTITY_ACCESS_TOKEN, + actor, + orgId: identity.orgId, identityId: identity.identityId, - kubernetes: token?.identityAuth?.kubernetes - }); + identityName: identity.name, + authMethod: null, + isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId), + token + }; + if (token?.identityAuth?.oidc) { + requestContext.set("identityAuthInfo", { + identityId: identity.identityId, + oidc: token?.identityAuth?.oidc + }); + } + if (token?.identityAuth?.kubernetes) { + requestContext.set("identityAuthInfo", { + identityId: identity.identityId, + kubernetes: token?.identityAuth?.kubernetes + }); + } + if (token?.identityAuth?.aws) { + requestContext.set("identityAuthInfo", { + identityId: identity.identityId, + aws: token?.identityAuth?.aws + }); + } + break; } - if (token?.identityAuth?.aws) { - requestContext.set("identityAuthInfo", { - identityId: identity.identityId, - aws: token?.identityAuth?.aws - }); + case AuthMode.SERVICE_TOKEN: { + const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token); + requestContext.set("orgId", serviceToken.orgId); + req.auth = { + orgId: serviceToken.orgId, + authMode: AuthMode.SERVICE_TOKEN as const, + serviceToken, + serviceTokenId: serviceToken.id, + actor, + authMethod: null, + token + }; + break; } - break; + case AuthMode.API_KEY: { + const user = await server.services.apiKey.fnValidateApiKey(token as string); + req.auth = { + authMode: AuthMode.API_KEY as const, + userId: user.id, + actor, + user, + orgId: "API_KEY", // We set the orgId to an arbitrary value, since we can't link an API key to a specific org. We have to deprecate API keys soon! + authMethod: null, + token: token as string + }; + break; + } + case AuthMode.SCIM_TOKEN: { + const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); + requestContext.set("orgId", orgId); + req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null }; + break; + } + default: + throw new BadRequestError({ message: "Invalid token strategy provided" }); } - case AuthMode.SERVICE_TOKEN: { - const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token); - requestContext.set("orgId", serviceToken.orgId); - req.auth = { - orgId: serviceToken.orgId, - authMode: AuthMode.SERVICE_TOKEN as const, - serviceToken, - serviceTokenId: serviceToken.id, - actor, - authMethod: null, - token - }; - break; - } - case AuthMode.API_KEY: { - const user = await server.services.apiKey.fnValidateApiKey(token as string); - req.auth = { - authMode: AuthMode.API_KEY as const, - userId: user.id, - actor, - user, - orgId: "API_KEY", // We set the orgId to an arbitrary value, since we can't link an API key to a specific org. We have to deprecate API keys soon! - authMethod: null, - token: token as string - }; - break; - } - case AuthMode.SCIM_TOKEN: { - const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); - requestContext.set("orgId", orgId); - req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null }; - break; - } - default: - throw new BadRequestError({ message: "Invalid token strategy provided" }); - } - }); -}); + }); + } +); diff --git a/backend/src/server/plugins/auth/verify-auth.ts b/backend/src/server/plugins/auth/verify-auth.ts index 44ea069dc..c63199769 100644 --- a/backend/src/server/plugins/auth/verify-auth.ts +++ b/backend/src/server/plugins/auth/verify-auth.ts @@ -10,6 +10,10 @@ interface TAuthOptions { export const verifyAuth = (authStrategies: AuthMode[], options: TAuthOptions = { requireOrg: true }) => (req: T, _res: FastifyReply, done: HookHandlerDoneFunction) => { + if (req.shouldForwardWritesToPrimaryInstance && req.method !== "GET") { + return done(); + } + if (!Array.isArray(authStrategies)) throw new Error("Auth strategy must be array"); if (!req.auth) throw new UnauthorizedError({ message: "Token missing" }); diff --git a/backend/src/server/plugins/primary-forwarding-mode.ts b/backend/src/server/plugins/primary-forwarding-mode.ts new file mode 100644 index 000000000..611806a6b --- /dev/null +++ b/backend/src/server/plugins/primary-forwarding-mode.ts @@ -0,0 +1,14 @@ +import replyFrom from "@fastify/reply-from"; +import fp from "fastify-plugin"; + +export const forwardWritesToPrimary = fp(async (server, opt: { primaryUrl: string }) => { + await server.register(replyFrom, { + base: opt.primaryUrl + }); + + server.addHook("preValidation", async (request, reply) => { + if (request.url.startsWith("/api") && ["POST", "PUT", "DELETE", "PATCH"].includes(request.method)) { + return reply.from(request.url); + } + }); +}); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 3773048b0..bb7d202da 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -38,6 +38,9 @@ import { externalKmsServiceFactory } from "@app/ee/services/external-kms/externa import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal"; +import { gatewayV2DalFactory } from "@app/ee/services/gateway-v2/gateway-v2-dal"; +import { gatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; +import { orgGatewayConfigV2DalFactory } from "@app/ee/services/gateway-v2/org-gateway-config-v2-dal"; import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal"; import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { groupDALFactory } from "@app/ee/services/group/group-dal"; @@ -72,6 +75,10 @@ import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/proje import { projectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service"; import { rateLimitDALFactory } from "@app/ee/services/rate-limit/rate-limit-dal"; import { rateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service"; +import { instanceRelayConfigDalFactory } from "@app/ee/services/relay/instance-relay-config-dal"; +import { orgRelayConfigDalFactory } from "@app/ee/services/relay/org-relay-config-dal"; +import { relayDalFactory } from "@app/ee/services/relay/relay-dal"; +import { relayServiceFactory } from "@app/ee/services/relay/relay-service"; import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal"; import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; import { scimDALFactory } from "@app/ee/services/scim/scim-dal"; @@ -123,6 +130,7 @@ import { sshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-grou import { sshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"; import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; +import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig, TEnvConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -218,6 +226,11 @@ import { kmsServiceFactory } from "@app/services/kms/kms-service"; import { microsoftTeamsIntegrationDALFactory } from "@app/services/microsoft-teams/microsoft-teams-integration-dal"; import { microsoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { projectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; +import { notificationQueueServiceFactory } from "@app/services/notification/notification-queue"; +import { notificationServiceFactory } from "@app/services/notification/notification-service"; +import { userNotificationDALFactory } from "@app/services/notification/user-notification-dal"; +import { offlineUsageReportDALFactory } from "@app/services/offline-usage-report/offline-usage-report-dal"; +import { offlineUsageReportServiceFactory } from "@app/services/offline-usage-report/offline-usage-report-service"; import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal"; import { orgBotDALFactory } from "@app/services/org/org-bot-dal"; import { orgDALFactory } from "@app/services/org/org-dal"; @@ -310,10 +323,12 @@ import { injectAssumePrivilege } from "../plugins/auth/inject-assume-privilege"; import { injectIdentity } from "../plugins/auth/inject-identity"; import { injectPermission } from "../plugins/auth/inject-permission"; import { injectRateLimits } from "../plugins/inject-rate-limits"; +import { forwardWritesToPrimary } from "../plugins/primary-forwarding-mode"; import { registerV1Routes } from "./v1"; import { initializeOauthConfigSync } from "./v1/sso-router"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; +import { registerV4Routes } from "./v4"; const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); @@ -385,6 +400,7 @@ export const registerRoutes = async ( const reminderRecipientDAL = reminderRecipientDALFactory(db); const integrationDAL = integrationDALFactory(db); + const offlineUsageReportDAL = offlineUsageReportDALFactory(db); const integrationAuthDAL = integrationAuthDALFactory(db); const webhookDAL = webhookDALFactory(db); const serviceTokenDAL = serviceTokenDALFactory(db); @@ -418,6 +434,7 @@ export const registerRoutes = async ( const telemetryDAL = telemetryDALFactory(db); const appConnectionDAL = appConnectionDALFactory(db); const secretSyncDAL = secretSyncDALFactory(db, folderDAL); + const userNotificationDAL = userNotificationDALFactory(db); // ee db layer ops const permissionDAL = permissionDALFactory(db); @@ -499,6 +516,7 @@ export const registerRoutes = async ( const microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDALFactory(db); const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db); const secretScanningV2DAL = secretScanningV2DALFactory(db); + const keyValueStoreDAL = keyValueStoreDALFactory(db); const eventBusService = eventBusFactory(server.redis); const sseService = sseServiceFactory(eventBusService, server.redis); @@ -555,20 +573,29 @@ export const registerRoutes = async ( permissionService }); + const auditLogStreamService = auditLogStreamServiceFactory({ + licenseService, + permissionService, + auditLogStreamDAL, + kmsService + }); + const auditLogQueue = await auditLogQueueServiceFactory({ auditLogDAL, queueService, projectDAL, licenseService, - auditLogStreamDAL + auditLogStreamService }); - const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue }); - const auditLogStreamService = auditLogStreamServiceFactory({ - licenseService, - permissionService, - auditLogStreamDAL + const notificationQueue = await notificationQueueServiceFactory({ + userNotificationDAL, + queueService }); + + const notificationService = notificationServiceFactory({ notificationQueue, userNotificationDAL }); + + const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue }); const secretApprovalPolicyService = secretApprovalPolicyServiceFactory({ projectEnvDAL, secretApprovalPolicyApproverDAL: sapApproverDAL, @@ -626,6 +653,7 @@ export const registerRoutes = async ( const folderTreeCheckpointDAL = folderTreeCheckpointDALFactory(db); const folderCommitDAL = folderCommitDALFactory(db); const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db); + const folderCommitQueueService = folderCommitQueueServiceFactory({ queueService, folderTreeCheckpointDAL, @@ -722,6 +750,7 @@ export const registerRoutes = async ( const userService = userServiceFactory({ userDAL, + orgDAL, orgMembershipDAL, tokenService, permissionService, @@ -790,6 +819,7 @@ export const registerRoutes = async ( groupDAL, orgBotDAL, oidcConfigDAL, + ldapConfigDAL, loginService, projectBotService, reminderService @@ -842,7 +872,14 @@ export const registerRoutes = async ( licenseService, kmsService, microsoftTeamsService, - invalidateCacheQueue + invalidateCacheQueue, + smtpService, + tokenService + }); + + const offlineUsageReportService = offlineUsageReportServiceFactory({ + offlineUsageReportDAL, + licenseService }); const orgAdminService = orgAdminServiceFactory({ @@ -941,6 +978,13 @@ export const registerRoutes = async ( const pkiSubscriberDAL = pkiSubscriberDALFactory(db); const pkiTemplatesDAL = pkiTemplatesDALFactory(db); + const instanceRelayConfigDAL = instanceRelayConfigDalFactory(db); + const orgRelayConfigDAL = orgRelayConfigDalFactory(db); + const relayDAL = relayDalFactory(db); + const gatewayV2DAL = gatewayV2DalFactory(db); + + const orgGatewayConfigV2DAL = orgGatewayConfigV2DalFactory(db); + const certificateService = certificateServiceFactory({ certificateDAL, certificateBodyDAL, @@ -1059,6 +1103,23 @@ export const registerRoutes = async ( keyStore }); + const relayService = relayServiceFactory({ + instanceRelayConfigDAL, + orgRelayConfigDAL, + relayDAL, + kmsService + }); + + const gatewayV2Service = gatewayV2ServiceFactory({ + kmsService, + licenseService, + relayService, + orgGatewayConfigV2DAL, + gatewayV2DAL, + relayDAL, + permissionService + }); + const secretSyncQueue = secretSyncQueueFactory({ queueService, secretSyncDAL, @@ -1083,7 +1144,8 @@ export const registerRoutes = async ( resourceMetadataDAL, appConnectionDAL, licenseService, - gatewayService + gatewayService, + gatewayV2Service }); const secretQueueService = secretQueueFactory({ @@ -1369,7 +1431,8 @@ export const registerRoutes = async ( kmsService, groupDAL, microsoftTeamsService, - projectMicrosoftTeamsConfigDAL + projectMicrosoftTeamsConfigDAL, + notificationService }); const secretReplicationService = secretReplicationServiceFactory({ @@ -1506,6 +1569,7 @@ export const registerRoutes = async ( permissionService, licenseService }); + const identityUaService = identityUaServiceFactory({ identityOrgMembershipDAL, permissionService, @@ -1523,6 +1587,8 @@ export const registerRoutes = async ( permissionService, licenseService, gatewayService, + gatewayV2Service, + gatewayV2DAL, gatewayDAL, kmsService }); @@ -1615,12 +1681,15 @@ export const registerRoutes = async ( identityOrgMembershipDAL, licenseService, identityDAL, - identityAuthTemplateDAL + identityAuthTemplateDAL, + keyStore }); const dynamicSecretProviders = buildDynamicSecretProviders({ - gatewayService + gatewayService, + gatewayV2Service }); + const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({ queueService, dynamicSecretLeaseDAL, @@ -1640,6 +1709,7 @@ export const registerRoutes = async ( licenseService, kmsService, gatewayDAL, + gatewayV2DAL, resourceMetadataDAL }); @@ -1656,6 +1726,7 @@ export const registerRoutes = async ( userDAL, identityDAL }); + const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ auditLogDAL, queueService, @@ -1667,7 +1738,9 @@ export const registerRoutes = async ( secretVersionV2DAL: secretVersionV2BridgeDAL, identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL, serviceTokenService, - orgService + orgService, + userNotificationDAL, + keyValueStoreDAL }); const dailyReminderQueueService = dailyReminderQueueServiceFactory({ @@ -1764,7 +1837,10 @@ export const registerRoutes = async ( kmsService, licenseService, gatewayService, - gatewayDAL + gatewayV2Service, + gatewayDAL, + gatewayV2DAL, + projectDAL }); const secretSyncService = secretSyncServiceFactory({ @@ -1863,7 +1939,8 @@ export const registerRoutes = async ( secretQueueService, queueService, appConnectionDAL, - gatewayService + gatewayService, + gatewayV2Service }); const certificateAuthorityService = certificateAuthorityServiceFactory({ @@ -2003,6 +2080,7 @@ export const registerRoutes = async ( apiKey: apiKeyService, authToken: tokenService, superAdmin: superAdminService, + offlineUsageReport: offlineUsageReportService, project: projectService, projectMembership: projectMembershipService, projectKey: projectKeyService, @@ -2088,6 +2166,8 @@ export const registerRoutes = async ( kmip: kmipService, kmipOperation: kmipOperationService, gateway: gatewayService, + relay: relayService, + gatewayV2: gatewayV2Service, secretRotationV2: secretRotationV2Service, microsoftTeams: microsoftTeamsService, assumePrivileges: assumePrivilegeService, @@ -2096,7 +2176,8 @@ export const registerRoutes = async ( secretScanningV2: secretScanningV2Service, reminder: reminderService, bus: eventBusService, - sse: sseService + sse: sseService, + notification: notificationService }); const cronJobs: CronJob[] = []; @@ -2135,8 +2216,14 @@ export const registerRoutes = async ( user: userDAL, kmipClient: kmipClientDAL }); + const shouldForwardWritesToPrimaryInstance = Boolean(envConfig.INFISICAL_PRIMARY_INSTANCE_URL); + if (shouldForwardWritesToPrimaryInstance) { + logger.info(`Infisical primary instance is configured: ${envConfig.INFISICAL_PRIMARY_INSTANCE_URL}`); - await server.register(injectIdentity, { userDAL, serviceTokenDAL }); + await server.register(forwardWritesToPrimary, { primaryUrl: envConfig.INFISICAL_PRIMARY_INSTANCE_URL as string }); + } + + await server.register(injectIdentity, { shouldForwardWritesToPrimaryInstance }); await server.register(injectAssumePrivilege); await server.register(injectPermission); await server.register(injectRateLimits); @@ -2210,6 +2297,7 @@ export const registerRoutes = async ( { prefix: "/api/v2" } ); await server.register(registerV3Routes, { prefix: "/api/v3" }); + await server.register(registerV4Routes, { prefix: "/api/v4" }); server.addHook("onClose", async () => { cronJobs.forEach((job) => job.stop()); diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 8af4baa8b..344224008 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -246,13 +246,6 @@ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({ metadata: ResourceMetadataSchema.optional() }); -export const SanitizedAuditLogStreamSchema = z.object({ - id: z.string(), - url: z.string(), - createdAt: z.date(), - updatedAt: z.date() -}); - export const SanitizedProjectSchema = ProjectsSchema.pick({ id: true, name: true, diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index ea3726c22..7f5a2f374 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -13,6 +13,7 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { addAuthOriginDomainCookie } from "@app/server/lib/cookie"; +import { GenericResourceNameSchema } from "@app/server/lib/schemas"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -53,7 +54,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { defaultAuthOrgAuthMethod: z.string().nullish(), isSecretScanningDisabled: z.boolean(), kubernetesAutoFetchServiceAccountToken: z.boolean(), - paramsFolderSecretDetectionEnabled: z.boolean() + paramsFolderSecretDetectionEnabled: z.boolean(), + isOfflineUsageReportsEnabled: z.boolean() }) }) } @@ -69,7 +71,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { isMigrationModeOn: serverEnvs.MAINTENANCE_MODE, isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING, kubernetesAutoFetchServiceAccountToken: serverEnvs.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN, - paramsFolderSecretDetectionEnabled: serverEnvs.PARAMS_FOLDER_SECRET_DETECTION_ENABLED + paramsFolderSecretDetectionEnabled: serverEnvs.PARAMS_FOLDER_SECRET_DETECTION_ENABLED, + isOfflineUsageReportsEnabled: !!serverEnvs.LICENSE_KEY_OFFLINE } }; } @@ -215,7 +218,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }), membershipId: z.string(), role: z.string(), - roleId: z.string().nullish() + roleId: z.string().nullish(), + status: z.string().nullish() }) .array(), projects: z @@ -838,4 +842,121 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }; } }); + + server.route({ + method: "POST", + url: "/organization-management/organizations", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + name: GenericResourceNameSchema, + inviteAdminEmails: z.string().email().array().min(1) + }), + response: { + 200: z.object({ + organization: OrganizationsSchema + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const organization = await server.services.superAdmin.createOrganization(req.body, req.permission); + return { organization }; + } + }); + + server.route({ + method: "POST", + url: "/organization-management/organizations/:organizationId/memberships/:membershipId/resend-invite", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + organizationId: z.string(), + membershipId: z.string() + }), + response: { + 200: z.object({ + organizationMembership: OrgMembershipsSchema + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const organizationMembership = await server.services.superAdmin.resendOrgInvite(req.params, req.permission); + return { organizationMembership }; + } + }); + + server.route({ + method: "POST", + url: "/organization-management/organizations/:organizationId/access", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + organizationId: z.string() + }), + response: { + 200: z.object({ + organizationMembership: OrgMembershipsSchema + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const organizationMembership = await server.services.superAdmin.joinOrganization( + req.params.organizationId, + req.permission + ); + return { organizationMembership }; + } + }); + + server.route({ + method: "POST", + url: "/usage-report/generate", + config: { + rateLimit: writeLimit + }, + schema: { + response: { + 200: z.object({ + csvContent: z.string(), + signature: z.string(), + filename: z.string() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async () => { + const result = await server.services.offlineUsageReport.generateUsageReportCSV(); + + return { + csvContent: result.csvContent, + signature: result.signature, + filename: result.filename + }; + } + }); }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts index 50111b109..720726c34 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts @@ -26,6 +26,7 @@ export const registerAppConnectionEndpoints = ; updateSchema: z.ZodType<{ name?: string; @@ -47,18 +48,27 @@ export const registerAppConnectionEndpoints = { - const appConnections = (await server.services.appConnection.listAppConnectionsByOrg(req.permission, app)) as T[]; + const { projectId } = req.query; + const appConnections = (await server.services.appConnection.listAppConnections( + req.permission, + app, + projectId + )) as T[]; await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, + projectId, event: { type: EventType.GET_APP_CONNECTIONS, metadata: { @@ -82,14 +92,19 @@ export const registerAppConnectionEndpoints = { + const { projectId } = req.query; const appConnections = await server.services.appConnection.listAvailableAppConnectionsForUser( app, - req.permission + req.permission, + projectId ); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, + projectId, event: { type: EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS, metadata: { @@ -149,6 +167,7 @@ export const registerAppConnectionEndpoints = { const { connectionName } = req.params; + const { projectId } = req.query; const appConnection = (await server.services.appConnection.findAppConnectionByName( app, - connectionName, + { + connectionName, + projectId + }, req.permission )) as T; await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, + projectId: appConnection.projectId ?? undefined, event: { type: EventType.GET_APP_CONNECTION, metadata: { @@ -216,9 +243,7 @@ export const registerAppConnectionEndpoints = { - const { name, method, credentials, description, isPlatformManagedCredentials, gatewayId } = req.body; + const { name, method, credentials, description, isPlatformManagedCredentials, gatewayId, projectId } = req.body; const appConnection = (await server.services.appConnection.createAppConnection( - { name, method, app, credentials, description, isPlatformManagedCredentials, gatewayId }, + { name, method, app, credentials, description, isPlatformManagedCredentials, gatewayId, projectId }, req.permission )) as T; await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, + projectId, event: { type: EventType.CREATE_APP_CONNECTION, metadata: { @@ -283,6 +309,7 @@ export const registerAppConnectionEndpoints = { + // const { connectionId } = req.params; + // + // const projects = await server.services.appConnection.findAppConnectionUsageById( + // app, + // connectionId, + // req.permission + // ); + // + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // orgId: req.permission.orgId, + // event: { + // type: EventType.GET_APP_CONNECTION_USAGE, + // metadata: { + // connectionId + // } + // } + // }); + // + // return { projects }; + // } + // }); }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index c2033f4b4..37558b817 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -1,12 +1,13 @@ import { z } from "zod"; +import { ProjectType } from "@app/db/schemas"; import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/ee/services/app-connections/oci"; import { OracleDBConnectionListItemSchema, SanitizedOracleDBConnectionSchema } from "@app/ee/services/app-connections/oracledb"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { ApiDocsTags } from "@app/lib/api-docs"; +import { ApiDocsTags, AppConnections } from "@app/lib/api-docs"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { @@ -210,6 +211,9 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) => hide: false, tags: [ApiDocsTags.AppConnections], description: "List the available App Connection Options.", + querystring: z.object({ + projectType: z.nativeEnum(ProjectType).optional() + }), response: { 200: z.object({ appConnectionOptions: AppConnectionOptionsSchema.array() @@ -217,8 +221,8 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) => } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: () => { - const appConnectionOptions = server.services.appConnection.listAppConnectionOptions(); + handler: (req) => { + const appConnectionOptions = server.services.appConnection.listAppConnectionOptions(req.query.projectType); return { appConnectionOptions }; } }); @@ -232,18 +236,27 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) => schema: { hide: false, tags: [ApiDocsTags.AppConnections], - description: "List all the App Connections for the current organization.", + description: "List all the App Connections for the current organization or project.", + querystring: z.object({ + projectId: z.string().optional().describe(AppConnections.LIST().projectId) + }), response: { 200: z.object({ appConnections: SanitizedAppConnectionSchema.array() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const appConnections = await server.services.appConnection.listAppConnectionsByOrg(req.permission); + const { projectId } = req.query; + const appConnections = await server.services.appConnection.listAppConnections( + req.permission, + undefined, + projectId + ); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, + projectId, event: { type: EventType.GET_APP_CONNECTIONS, metadata: { diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index a54bd5ccf..c394ee8e9 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -207,7 +207,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { const environments = req.query.environments.split(","); if (!projectId || environments.length === 0) - throw new BadRequestError({ message: "Missing workspace id or environment(s)" }); + throw new BadRequestError({ message: "Missing project id or environment(s)" }); const { shouldUseSecretV2Bridge } = await server.services.projectBot.getBotKey(projectId); @@ -474,7 +474,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secretCountFromEnv, - workspaceId: projectId, + projectId, environment, secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -696,7 +696,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { includeSecretRotations } = req.query; - if (!projectId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" }); + if (!projectId || !environment) throw new BadRequestError({ message: "Missing project id or environment" }); const { shouldUseSecretV2Bridge } = await server.services.projectBot.getBotKey(projectId); @@ -1001,7 +1001,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secretCount, - workspaceId: projectId, + projectId, environment, secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1168,7 +1168,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secretCountForEnv, - workspaceId: projectId, + projectId, environment, secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1361,7 +1361,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: projectId, + projectId, environment, secretPath, channel: getUserAgentType(req.headers["user-agent"]), diff --git a/backend/src/server/routes/v1/deprecated-project-env-router.ts b/backend/src/server/routes/v1/deprecated-project-env-router.ts new file mode 100644 index 000000000..1a187e2b1 --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-project-env-router.ts @@ -0,0 +1,298 @@ +import { z } from "zod"; + +import { ProjectEnvironmentsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, ENVIRONMENTS } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerDeprecatedProjectEnvRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/:workspaceId/environments/:envId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Environments], + description: "Get Environment", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + // NOTE(daniel): workspaceId isn't used, but we need to keep it for backwards compatibility. The endpoint defined below, uses no project ID, and is takes a pure environment ID. + workspaceId: z.string().trim().describe(ENVIRONMENTS.GET.projectId), + envId: z.string().trim().describe(ENVIRONMENTS.GET.id) + }), + response: { + 200: z.object({ + environment: ProjectEnvironmentsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const environment = await server.services.projectEnv.getEnvironmentById({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + id: req.params.envId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: environment.projectId, + event: { + type: EventType.GET_ENVIRONMENT, + metadata: { + id: environment.id + } + } + }); + + return { environment }; + } + }); + + server.route({ + method: "GET", + url: "/environments/:envId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Environments], + description: "Get Environment by ID", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + envId: z.string().trim().describe(ENVIRONMENTS.GET.id) + }), + response: { + 200: z.object({ + environment: ProjectEnvironmentsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const environment = await server.services.projectEnv.getEnvironmentById({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + id: req.params.envId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: environment.projectId, + event: { + type: EventType.GET_ENVIRONMENT, + metadata: { + id: environment.id + } + } + }); + + return { environment }; + } + }); + + server.route({ + method: "POST", + url: "/:workspaceId/environments", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Environments], + description: "Create environment", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().trim().describe(ENVIRONMENTS.CREATE.projectId) + }), + body: z.object({ + name: z.string().trim().describe(ENVIRONMENTS.CREATE.name), + position: z.number().min(1).optional().describe(ENVIRONMENTS.CREATE.position), + slug: slugSchema({ max: 64 }).describe(ENVIRONMENTS.CREATE.slug) + }), + response: { + 200: z.object({ + message: z.string(), + workspace: z.string(), + environment: ProjectEnvironmentsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const environment = await server.services.projectEnv.createEnvironment({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + projectId: req.params.workspaceId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: environment.projectId, + event: { + type: EventType.CREATE_ENVIRONMENT, + metadata: { + name: environment.name, + slug: environment.slug + } + } + }); + return { + message: "Successfully created new environment", + workspace: req.params.workspaceId, + environment + }; + } + }); + + server.route({ + method: "PATCH", + url: "/:workspaceId/environments/:id", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Environments], + description: "Update environment", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().trim().describe(ENVIRONMENTS.UPDATE.projectId), + id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id) + }), + body: z.object({ + slug: slugSchema({ max: 64 }).optional().describe(ENVIRONMENTS.UPDATE.slug), + name: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.name), + position: z.number().optional().describe(ENVIRONMENTS.UPDATE.position) + }), + response: { + 200: z.object({ + message: z.string(), + workspace: z.string(), + environment: ProjectEnvironmentsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { environment, old } = await server.services.projectEnv.updateEnvironment({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + id: req.params.id, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: environment.projectId, + event: { + type: EventType.UPDATE_ENVIRONMENT, + metadata: { + oldName: old.name, + oldSlug: old.slug, + oldPos: old.position, + newName: environment.name, + newSlug: environment.slug, + newPos: environment.position + } + } + }); + + return { + message: "Successfully updated environment", + workspace: req.params.workspaceId, + environment + }; + } + }); + + server.route({ + method: "DELETE", + url: "/:workspaceId/environments/:id", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Environments], + description: "Delete environment", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().trim().describe(ENVIRONMENTS.DELETE.projectId), + id: z.string().trim().describe(ENVIRONMENTS.DELETE.id) + }), + response: { + 200: z.object({ + message: z.string(), + workspace: z.string(), + environment: ProjectEnvironmentsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const environment = await server.services.projectEnv.deleteEnvironment({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + id: req.params.id + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: environment.projectId, + event: { + type: EventType.DELETE_ENVIRONMENT, + metadata: { + slug: environment.slug, + name: environment.name + } + } + }); + + return { + message: "Successfully deleted environment", + workspace: req.params.workspaceId, + environment + }; + } + }); +}; diff --git a/backend/src/server/routes/v1/deprecated-project-membership-router.ts b/backend/src/server/routes/v1/deprecated-project-membership-router.ts new file mode 100644 index 000000000..ab225929f --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-project-membership-router.ts @@ -0,0 +1,378 @@ +import { z } from "zod"; + +import { + OrgMembershipsSchema, + ProjectMembershipsSchema, + ProjectUserMembershipRolesSchema, + UserEncryptionKeysSchema, + UsersSchema +} from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, PROJECT_USERS } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; + +export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/:workspaceId/memberships", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], + description: "Return project user memberships", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIPS.projectId) + }), + response: { + 200: z.object({ + memberships: ProjectMembershipsSchema.extend({ + user: UsersSchema.pick({ + email: true, + firstName: true, + lastName: true, + id: true, + username: true + }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ) + }) + .omit({ updatedAt: true }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const memberships = await server.services.projectMembership.getProjectMemberships({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId + }); + return { memberships }; + } + }); + + server.route({ + method: "GET", + url: "/:workspaceId/memberships/:membershipId", + config: { + rateLimit: readLimit + }, + schema: { + description: "Return project user membership", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.projectId), + membershipId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.membershipId) + }), + response: { + 200: z.object({ + membership: ProjectMembershipsSchema.extend({ + user: UsersSchema.pick({ + email: true, + firstName: true, + lastName: true, + id: true, + username: true + }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ) + }).omit({ updatedAt: true }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const membership = await server.services.projectMembership.getProjectMembershipById({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + id: req.params.membershipId + }); + return { membership }; + } + }); + + server.route({ + method: "POST", + url: "/:workspaceId/memberships/details", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], + description: "Return project user memberships", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.projectId) + }), + body: z.object({ + username: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.username) + }), + response: { + 200: z.object({ + membership: ProjectMembershipsSchema.extend({ + user: UsersSchema.pick({ + email: true, + firstName: true, + lastName: true, + id: true + }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ) + }).omit({ createdAt: true, updatedAt: true }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const membership = await server.services.projectMembership.getProjectMembershipByUsername({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + username: req.body.username + }); + return { membership }; + } + }); + + server.route({ + method: "POST", + url: "/:workspaceId/memberships", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + members: z + .object({ + orgMembershipId: z.string().trim(), + workspaceEncryptedKey: z.string().trim(), + workspaceEncryptedNonce: z.string().trim() + }) + .array() + .min(1) + }), + response: { + 200: z.object({ + success: z.boolean(), + data: OrgMembershipsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const data = await server.services.projectMembership.addUsersToProject({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + members: req.body.members + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.params.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.ADD_BATCH_PROJECT_MEMBER, + metadata: data.map(({ userId }) => ({ + userId: userId || "", + email: "" + })) + } + }); + + return { data, success: true }; + } + }); + + server.route({ + method: "PATCH", + url: "/:workspaceId/memberships/:membershipId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], + description: "Update project user membership", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().trim().describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.projectId), + membershipId: z.string().trim().describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.membershipId) + }), + body: z.object({ + roles: z + .array( + z.union([ + z.object({ + role: z.string(), + isTemporary: z.literal(false).default(false) + }), + z.object({ + role: z.string(), + isTemporary: z.literal(true), + temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), + temporaryAccessStartTime: z.string().datetime() + }) + ]) + ) + .min(1) + .refine((data) => data.some(({ isTemporary }) => !isTemporary), "At least one long lived role is required") + .describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.roles) + }), + response: { + 200: z.object({ + roles: ProjectUserMembershipRolesSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const roles = await server.services.projectMembership.updateProjectMembership({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + membershipId: req.params.membershipId, + roles: req.body.roles + }); + + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // projectId: req.params.workspaceId, + // event: { + // type: EventType.UPDATE_USER_WORKSPACE_ROLE, + // metadata: { + // userId: membership.userId, + // newRole: req.body.role, + // oldRole: membership.role, + // email: "" + // } + // } + // }); + return { roles }; + } + }); + + server.route({ + method: "DELETE", + url: "/:workspaceId/memberships/:membershipId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Delete project user membership", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().trim(), + membershipId: z.string().trim() + }), + response: { + 200: z.object({ + membership: ProjectMembershipsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const membership = await server.services.projectMembership.deleteProjectMembership({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + membershipId: req.params.membershipId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.workspaceId, + event: { + type: EventType.REMOVE_PROJECT_MEMBER, + metadata: { + userId: membership.userId, + email: "" + } + } + }); + return { membership }; + } + }); +}; diff --git a/backend/src/server/routes/v1/deprecated-project-router.ts b/backend/src/server/routes/v1/deprecated-project-router.ts new file mode 100644 index 000000000..687d95b9b --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-project-router.ts @@ -0,0 +1,728 @@ +import { z } from "zod"; + +import { + IntegrationsSchema, + ProjectRolesSchema, + ProjectSlackConfigsSchema, + ProjectSshConfigsSchema, + ProjectType, + SortDirection +} from "@app/db/schemas"; +import { ProjectMicrosoftTeamsConfigsSchema } from "@app/db/schemas/project-microsoft-teams-configs"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; +import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; +import { re2Validator } from "@app/lib/zod"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns"; +import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types"; +import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators"; +import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types"; + +import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas"; + +const projectWithEnv = SanitizedProjectSchema.merge( + z.object({ + _id: z.string(), + environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array() + }) +); + +export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + querystring: z.object({ + includeRoles: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true"), + type: z.nativeEnum(ProjectType).optional() + }), + response: { + 200: z.object({ + workspaces: projectWithEnv + .extend({ + roles: ProjectRolesSchema.array().optional() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspaces = await server.services.project.getProjects({ + includeRoles: req.query.includeRoles, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + type: req.query.type + }); + return { workspaces }; + } + }); + + server.route({ + method: "GET", + url: "/:workspaceId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Projects], + description: "Get project", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().trim().describe(PROJECTS.GET.projectId) + }), + response: { + 200: z.object({ + workspace: projectWithEnv.optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspace = await server.services.project.getAProject({ + filter: { + type: ProjectFilterType.ID, + projectId: req.params.workspaceId + }, + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId + }); + return { workspace }; + } + }); + + server.route({ + method: "DELETE", + url: "/:workspaceId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Projects], + description: "Delete project", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().trim().describe(PROJECTS.DELETE.projectId) + }), + response: { + 200: z.object({ + workspace: SanitizedProjectSchema.optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspace = await server.services.project.deleteProject({ + filter: { + type: ProjectFilterType.ID, + projectId: req.params.workspaceId + }, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.params.workspaceId, + event: { + type: EventType.DELETE_PROJECT, + metadata: workspace + } + }); + + return { workspace }; + } + }); + + server.route({ + method: "PATCH", + url: "/:workspaceId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Projects], + description: "Update project", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().trim().describe(PROJECTS.UPDATE.projectId) + }), + body: z.object({ + name: z + .string() + .trim() + .max(64, { message: "Name must be 64 or fewer characters" }) + .optional() + .describe(PROJECTS.UPDATE.name), + description: z + .string() + .trim() + .max(256, { message: "Description must be 256 or fewer characters" }) + .optional() + .describe(PROJECTS.UPDATE.projectDescription), + autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization), + hasDeleteProtection: z.boolean().optional().describe(PROJECTS.UPDATE.hasDeleteProtection), + slug: z + .string() + .trim() + .max(64, { message: "Slug must be 64 characters or fewer" }) + .refine(re2Validator(/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/), { + message: + "Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore." + }) + .optional() + .describe(PROJECTS.UPDATE.slug), + secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing), + showSnapshotsLegacy: z.boolean().optional().describe(PROJECTS.UPDATE.showSnapshotsLegacy), + defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct), + secretDetectionIgnoreValues: z + .array(z.string()) + .optional() + .describe(PROJECTS.UPDATE.secretDetectionIgnoreValues) + }), + response: { + 200: z.object({ + workspace: SanitizedProjectSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspace = await server.services.project.updateProject({ + filter: { + type: ProjectFilterType.ID, + projectId: req.params.workspaceId + }, + update: { + name: req.body.name, + description: req.body.description, + autoCapitalization: req.body.autoCapitalization, + defaultProduct: req.body.defaultProduct, + hasDeleteProtection: req.body.hasDeleteProtection, + slug: req.body.slug, + secretSharing: req.body.secretSharing, + showSnapshotsLegacy: req.body.showSnapshotsLegacy, + secretDetectionIgnoreValues: req.body.secretDetectionIgnoreValues + }, + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.params.workspaceId, + event: { + type: EventType.UPDATE_PROJECT, + metadata: req.body + } + }); + + return { + workspace + }; + } + }); + + server.route({ + method: "PUT", + url: "/:workspaceSlug/audit-logs-retention", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + workspaceSlug: z.string().trim() + }), + body: z.object({ + auditLogsRetentionDays: z.number().min(0) + }), + response: { + 200: z.object({ + message: z.string(), + workspace: SanitizedProjectSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspace = await server.services.project.updateAuditLogsRetention({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + filter: { + type: ProjectFilterType.SLUG, + slug: req.params.workspaceSlug, + orgId: req.permission.orgId + }, + auditLogsRetentionDays: req.body.auditLogsRetentionDays + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: workspace.id, + event: { + type: EventType.UPDATE_PROJECT, + metadata: req.body + } + }); + + return { + message: "Successfully updated project's audit logs retention period", + workspace + }; + } + }); + + server.route({ + method: "GET", + url: "/:workspaceId/integrations", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Integrations], + description: "List integrations for a project.", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION.projectId) + }), + response: { + 200: z.object({ + integrations: IntegrationsSchema.merge( + z.object({ + environment: z.object({ + id: z.string(), + name: z.string(), + slug: z.string() + }) + }) + ).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const integrations = await server.services.integration.listIntegrationByProject({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId + }); + return { integrations }; + } + }); + + server.route({ + method: "GET", + url: "/:workspaceId/authorizations", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Integrations], + description: "List integration auth objects for a workspace.", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION_AUTHORIZATION.projectId) + }), + response: { + 200: z.object({ + authorizations: integrationAuthPubSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const authorizations = await server.services.integrationAuth.listIntegrationAuthByProjectId({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId + }); + return { authorizations }; + } + }); + + server.route({ + method: "GET", + url: "/:workspaceId/ssh-config", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + response: { + 200: ProjectSshConfigsSchema.pick({ + id: true, + createdAt: true, + updatedAt: true, + projectId: true, + defaultUserSshCaId: true, + defaultHostSshCaId: true + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const sshConfig = await server.services.project.getProjectSshConfig({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: sshConfig.projectId, + event: { + type: EventType.GET_PROJECT_SSH_CONFIG, + metadata: { + id: sshConfig.id, + projectId: sshConfig.projectId + } + } + }); + + return sshConfig; + } + }); + + server.route({ + method: "PATCH", + url: "/:workspaceId/ssh-config", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + defaultUserSshCaId: z.string().optional(), + defaultHostSshCaId: z.string().optional() + }), + response: { + 200: ProjectSshConfigsSchema.pick({ + id: true, + createdAt: true, + updatedAt: true, + projectId: true, + defaultUserSshCaId: true, + defaultHostSshCaId: true + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const sshConfig = await server.services.project.updateProjectSshConfig({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: sshConfig.projectId, + event: { + type: EventType.UPDATE_PROJECT_SSH_CONFIG, + metadata: { + id: sshConfig.id, + projectId: sshConfig.projectId, + defaultUserSshCaId: sshConfig.defaultUserSshCaId, + defaultHostSshCaId: sshConfig.defaultHostSshCaId + } + } + }); + + return sshConfig; + } + }); + + server.route({ + method: "GET", + url: "/:workspaceId/workflow-integration-config/:integration", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim(), + integration: z.nativeEnum(WorkflowIntegration) + }), + response: { + 200: z.discriminatedUnion("integration", [ + ProjectSlackConfigsSchema.pick({ + id: true, + isAccessRequestNotificationEnabled: true, + accessRequestChannels: true, + isSecretRequestNotificationEnabled: true, + secretRequestChannels: true + }).merge( + z.object({ + integration: z.literal(WorkflowIntegration.SLACK), + integrationId: z.string() + }) + ), + ProjectMicrosoftTeamsConfigsSchema.pick({ + id: true, + isAccessRequestNotificationEnabled: true, + accessRequestChannels: true, + isSecretRequestNotificationEnabled: true, + secretRequestChannels: true + }).merge( + z.object({ + integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS), + integrationId: z.string() + }) + ) + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const config = await server.services.project.getProjectWorkflowIntegrationConfig({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + integration: req.params.integration + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.workspaceId, + event: { + type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG, + metadata: { + id: config.id, + integration: config.integration + } + } + }); + + return config; + } + }); + + server.route({ + method: "DELETE", + url: "/:projectId/workflow-integration/:integration/:integrationId", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim(), + integration: z.nativeEnum(WorkflowIntegration), + integrationId: z.string() + }), + response: { + 200: z.object({ + integrationConfig: z.object({ + id: z.string() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const deletedIntegration = await server.services.project.deleteProjectWorkflowIntegration({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + projectId: req.params.projectId, + integration: req.params.integration, + integrationId: req.params.integrationId + }); + + return { + integrationConfig: deletedIntegration + }; + } + }); + + server.route({ + method: "PUT", + url: "/:workspaceId/workflow-integration", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + + body: z.discriminatedUnion("integration", [ + z.object({ + integration: z.literal(WorkflowIntegration.SLACK), + integrationId: z.string(), + accessRequestChannels: validateSlackChannelsField, + secretRequestChannels: validateSlackChannelsField, + isAccessRequestNotificationEnabled: z.boolean(), + isSecretRequestNotificationEnabled: z.boolean() + }), + z.object({ + integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS), + integrationId: z.string(), + accessRequestChannels: validateMicrosoftTeamsChannelsSchema, + secretRequestChannels: validateMicrosoftTeamsChannelsSchema, + isAccessRequestNotificationEnabled: z.boolean(), + isSecretRequestNotificationEnabled: z.boolean() + }) + ]), + response: { + 200: z.discriminatedUnion("integration", [ + ProjectSlackConfigsSchema.pick({ + id: true, + isAccessRequestNotificationEnabled: true, + accessRequestChannels: true, + isSecretRequestNotificationEnabled: true, + secretRequestChannels: true + }).merge( + z.object({ + integration: z.literal(WorkflowIntegration.SLACK), + integrationId: z.string() + }) + ), + ProjectMicrosoftTeamsConfigsSchema.pick({ + id: true, + isAccessRequestNotificationEnabled: true, + isSecretRequestNotificationEnabled: true + }).merge( + z.object({ + integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS), + integrationId: z.string(), + accessRequestChannels: validateMicrosoftTeamsChannelsSchema, + secretRequestChannels: validateMicrosoftTeamsChannelsSchema + }) + ) + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workflowIntegrationConfig = await server.services.project.updateProjectWorkflowIntegration({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.workspaceId, + event: { + type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG, + metadata: { + id: workflowIntegrationConfig.id, + integrationId: workflowIntegrationConfig.integrationId, + integration: workflowIntegrationConfig.integration, + isAccessRequestNotificationEnabled: workflowIntegrationConfig.isAccessRequestNotificationEnabled, + accessRequestChannels: workflowIntegrationConfig.accessRequestChannels, + isSecretRequestNotificationEnabled: workflowIntegrationConfig.isSecretRequestNotificationEnabled, + secretRequestChannels: workflowIntegrationConfig.secretRequestChannels + } + } + }); + + return workflowIntegrationConfig; + } + }); + + server.route({ + method: "POST", + url: "/search", + config: { + rateLimit: readLimit + }, + schema: { + body: z.object({ + limit: z.number().default(100), + offset: z.number().default(0), + type: z.nativeEnum(ProjectType).optional(), + orderBy: z.nativeEnum(SearchProjectSortBy).optional().default(SearchProjectSortBy.NAME), + orderDirection: z.nativeEnum(SortDirection).optional().default(SortDirection.ASC), + name: z + .string() + .trim() + .refine((val) => characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])(val), { + message: "Invalid pattern: only alphanumeric characters, - are allowed." + }) + .optional() + }), + response: { + 200: z.object({ + projects: SanitizedProjectSchema.extend({ isMember: z.boolean() }).array(), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { docs: projects, totalCount } = await server.services.project.searchProjects({ + permission: req.permission, + ...req.body + }); + + return { projects, totalCount }; + } + }); +}; diff --git a/backend/src/server/routes/v1/deprecated-secret-folder-router.ts b/backend/src/server/routes/v1/deprecated-secret-folder-router.ts new file mode 100644 index 000000000..ecb955025 --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-secret-folder-router.ts @@ -0,0 +1,444 @@ +import { z } from "zod"; + +import { SecretFoldersSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, FOLDERS } from "@app/lib/api-docs"; +import { prefixWithSlash, removeTrailingSlash } from "@app/lib/fn"; +import { isValidFolderName } from "@app/lib/validator"; +import { readLimit, secretsLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { booleanSchema } from "../sanitizedSchemas"; + +export const registerDeprecatedSecretFolderRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Folders], + description: "Create folders", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + workspaceId: z.string().trim().describe(FOLDERS.CREATE.projectId), + environment: z.string().trim().describe(FOLDERS.CREATE.environment), + name: z + .string() + .trim() + .describe(FOLDERS.CREATE.name) + .refine((name) => isValidFolderName(name), { + message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed." + }), + path: z + .string() + .trim() + .default("/") + .transform(prefixWithSlash) // Transformations get skipped if path is undefined + .transform(removeTrailingSlash) + .describe(FOLDERS.CREATE.path) + .optional(), + // backward compatibility with cli + directory: z + .string() + .trim() + .default("/") + .transform(prefixWithSlash) // Transformations get skipped if directory is undefined + .transform(removeTrailingSlash) + .describe(FOLDERS.CREATE.directory) + .optional(), + description: z.string().optional().nullable().describe(FOLDERS.CREATE.description) + }), + response: { + 200: z.object({ + folder: SecretFoldersSchema.extend({ + path: z.string() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const path = req.body.path || req.body.directory || "/"; + const folder = await server.services.folder.createFolder({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + projectId: req.body.workspaceId, + path, + description: req.body.description + }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.workspaceId, + event: { + type: EventType.CREATE_FOLDER, + metadata: { + environment: req.body.environment, + folderId: folder.id, + folderName: folder.name, + folderPath: path, + ...(req.body.description ? { description: req.body.description } : {}) + } + } + }); + return { folder }; + } + }); + + server.route({ + url: "/:folderId", + method: "PATCH", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Folders], + description: "Update folder", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + // old way this was name + folderId: z.string().describe(FOLDERS.UPDATE.folderId) + }), + body: z.object({ + workspaceId: z.string().trim().describe(FOLDERS.UPDATE.projectId), + environment: z.string().trim().describe(FOLDERS.UPDATE.environment), + name: z + .string() + .trim() + .describe(FOLDERS.UPDATE.name) + .refine((name) => isValidFolderName(name), { + message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed." + }), + path: z + .string() + .trim() + .default("/") + .transform(prefixWithSlash) // Transformations get skipped if path is undefined + .transform(removeTrailingSlash) + .describe(FOLDERS.UPDATE.path) + .optional(), + // backward compatibility with cli + directory: z + .string() + .trim() + .default("/") + .transform(prefixWithSlash) // Transformations get skipped if directory is undefined + .transform(removeTrailingSlash) + .describe(FOLDERS.UPDATE.directory) + .optional(), + description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description) + }), + response: { + 200: z.object({ + folder: SecretFoldersSchema.extend({ + path: z.string() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const path = req.body.path || req.body.directory || "/"; + const { folder, old } = await server.services.folder.updateFolder({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + projectId: req.body.workspaceId, + id: req.params.folderId, + path + }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.workspaceId, + event: { + type: EventType.UPDATE_FOLDER, + metadata: { + environment: req.body.environment, + folderId: folder.id, + folderPath: path, + newFolderName: folder.name, + oldFolderName: old.name + } + } + }); + return { folder }; + } + }); + + server.route({ + url: "/batch", + method: "PATCH", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Folders], + description: "Update folders by batch", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectSlug: z.string().trim().describe(FOLDERS.UPDATE.projectSlug), + folders: z + .object({ + id: z.string().describe(FOLDERS.UPDATE.folderId), + environment: z.string().trim().describe(FOLDERS.UPDATE.environment), + name: z + .string() + .trim() + .describe(FOLDERS.UPDATE.name) + .refine((name) => isValidFolderName(name), { + message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed." + }), + path: z + .string() + .trim() + .default("/") + .transform(prefixWithSlash) + .transform(removeTrailingSlash) + .describe(FOLDERS.UPDATE.path), + description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description) + }) + .array() + .min(1) + }), + response: { + 200: z.object({ + folders: SecretFoldersSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { newFolders, oldFolders, projectId } = await server.services.folder.updateManyFolders({ + ...req.body, + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await Promise.all( + req.body.folders.map(async (folder, index) => { + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.UPDATE_FOLDER, + metadata: { + environment: oldFolders[index].envId, + folderId: oldFolders[index].id, + folderPath: folder.path, + newFolderName: newFolders[index].name, + oldFolderName: oldFolders[index].name + } + } + }); + }) + ); + + return { folders: newFolders }; + } + }); + + // TODO(daniel): Expose this route in api reference and write docs for it. + server.route({ + method: "DELETE", + url: "/:folderIdOrName", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Folders], + description: "Delete a folder", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + folderIdOrName: z.string().describe(FOLDERS.DELETE.folderIdOrName) + }), + body: z.object({ + workspaceId: z.string().trim().describe(FOLDERS.DELETE.projectId), + environment: z.string().trim().describe(FOLDERS.DELETE.environment), + path: z + .string() + .trim() + .default("/") + .transform(prefixWithSlash) // Transformations get skipped if path is undefined + .transform(removeTrailingSlash) + .describe(FOLDERS.DELETE.path) + .optional(), + // keep this here as cli need directory + directory: z + .string() + .trim() + .default("/") + .transform(prefixWithSlash) // Transformations get skipped if directory is undefined + .transform(removeTrailingSlash) + .describe(FOLDERS.DELETE.directory) + .optional() + }), + response: { + 200: z.object({ + folder: SecretFoldersSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const path = req.body.path || req.body.directory || "/"; + const folder = await server.services.folder.deleteFolder({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + projectId: req.body.workspaceId, + idOrName: req.params.folderIdOrName, + path + }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.workspaceId, + event: { + type: EventType.DELETE_FOLDER, + metadata: { + environment: req.body.environment, + folderId: folder.id, + folderPath: path, + folderName: folder.name + } + } + }); + return { folder }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Folders], + description: "Get folders", + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + workspaceId: z.string().trim().describe(FOLDERS.LIST.projectId), + environment: z.string().trim().describe(FOLDERS.LIST.environment), + lastSecretModified: z.string().datetime().trim().optional().describe(FOLDERS.LIST.lastSecretModified), + path: z + .string() + .trim() + .transform(prefixWithSlash) // Transformations get skipped if path is undefined + .transform(removeTrailingSlash) + .describe(FOLDERS.LIST.path) + .optional(), + // backward compatibility with cli + directory: z + .string() + .trim() + .transform(prefixWithSlash) // Transformations get skipped if directory is undefined + .transform(removeTrailingSlash) + .describe(FOLDERS.LIST.directory) + .optional(), + recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive) + }), + response: { + 200: z.object({ + folders: SecretFoldersSchema.extend({ + relativePath: z.string().optional() + }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const path = req.query.path || req.query.directory || "/"; + const folders = await server.services.folder.getFolders({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query, + projectId: req.query.workspaceId, + path + }); + return { folders }; + } + }); + + server.route({ + method: "GET", + url: "/:id", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Folders], + description: "Get folder by id", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + id: z.string().trim().describe(FOLDERS.GET_BY_ID.folderId) + }), + response: { + 200: z.object({ + folder: SecretFoldersSchema.extend({ + environment: z.object({ + envId: z.string(), + envName: z.string(), + envSlug: z.string() + }), + path: z.string(), + projectId: z.string() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const folder = await server.services.folder.getFolderById({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + return { folder }; + } + }); +}; diff --git a/backend/src/server/routes/v1/deprecated-secret-import-router.ts b/backend/src/server/routes/v1/deprecated-secret-import-router.ts new file mode 100644 index 000000000..2fdbe4216 --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-secret-import-router.ts @@ -0,0 +1,472 @@ +import { z } from "zod"; + +import { SecretImportsSchema, SecretsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, SECRET_IMPORTS } from "@app/lib/api-docs"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { readLimit, secretsLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { secretRawSchema } from "../sanitizedSchemas"; + +export const registerDeprecatedSecretImportRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], + description: "Create secret imports", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + workspaceId: z.string().trim().describe(SECRET_IMPORTS.CREATE.projectId), + environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.environment), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.path), + import: z.object({ + environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.import.environment), + path: z.string().trim().transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.import.path) + }), + isReplication: z.boolean().default(false).describe(SECRET_IMPORTS.CREATE.isReplication) + }), + response: { + 200: z.object({ + message: z.string(), + secretImport: SecretImportsSchema.omit({ importEnv: true }).merge( + z.object({ + importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() }) + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretImport = await server.services.secretImport.createImport({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + projectId: req.body.workspaceId, + data: req.body.import + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.workspaceId, + event: { + type: EventType.CREATE_SECRET_IMPORT, + metadata: { + secretImportId: secretImport.id, + folderId: secretImport.folderId, + importFromSecretPath: secretImport.importPath, + importFromEnvironment: secretImport.importEnv.slug, + importToEnvironment: req.body.environment, + importToSecretPath: req.body.path + } + } + }); + return { message: "Successfully created secret import", secretImport }; + } + }); + + server.route({ + method: "PATCH", + url: "/:secretImportId", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], + description: "Update secret imports", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId) + }), + body: z.object({ + workspaceId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.projectId), + environment: z.string().trim().describe(SECRET_IMPORTS.UPDATE.environment), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.UPDATE.path), + import: z.object({ + environment: z.string().trim().optional().describe(SECRET_IMPORTS.UPDATE.import.environment), + path: z + .string() + .trim() + .optional() + .transform((val) => (val ? removeTrailingSlash(val) : val)) + .describe(SECRET_IMPORTS.UPDATE.import.path), + position: z.number().optional().describe(SECRET_IMPORTS.UPDATE.import.position) + }) + }), + response: { + 200: z.object({ + message: z.string(), + secretImport: SecretImportsSchema.omit({ importEnv: true }).merge( + z.object({ + importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() }) + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretImport = await server.services.secretImport.updateImport({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.secretImportId, + ...req.body, + projectId: req.body.workspaceId, + data: req.body.import + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.workspaceId, + event: { + type: EventType.UPDATE_SECRET_IMPORT, + metadata: { + secretImportId: secretImport.id, + folderId: secretImport.folderId, + position: secretImport.position, + importToEnvironment: req.body.environment, + importToSecretPath: req.body.path + } + } + }); + + return { message: "Successfully updated secret import", secretImport }; + } + }); + + server.route({ + method: "DELETE", + url: "/:secretImportId", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], + description: "Delete secret imports", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretImportId: z.string().trim().describe(SECRET_IMPORTS.DELETE.secretImportId) + }), + body: z.object({ + workspaceId: z.string().trim().describe(SECRET_IMPORTS.DELETE.projectId), + environment: z.string().trim().describe(SECRET_IMPORTS.DELETE.environment), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.DELETE.path) + }), + response: { + 200: z.object({ + message: z.string(), + secretImport: SecretImportsSchema.omit({ importEnv: true }).merge( + z.object({ + importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() }) + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretImport = await server.services.secretImport.deleteImport({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.secretImportId, + ...req.body, + projectId: req.body.workspaceId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.workspaceId, + event: { + type: EventType.DELETE_SECRET_IMPORT, + metadata: { + secretImportId: secretImport.id, + folderId: secretImport.folderId, + importFromEnvironment: secretImport.importEnv.slug, + importFromSecretPath: secretImport.importPath, + importToEnvironment: req.body.environment, + importToSecretPath: req.body.path + } + } + }); + return { message: "Successfully deleted secret import", secretImport }; + } + }); + + server.route({ + method: "POST", + url: "/:secretImportId/replication-resync", + config: { + rateLimit: secretsLimit + }, + schema: { + description: "Resync secret replication of secret imports", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId) + }), + body: z.object({ + workspaceId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.projectId), + environment: z.string().trim().describe(SECRET_IMPORTS.UPDATE.environment), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.UPDATE.path) + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { message } = await server.services.secretImport.resyncSecretImportReplication({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.secretImportId, + ...req.body, + projectId: req.body.workspaceId + }); + + return { message }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], + description: "Get secret imports", + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + workspaceId: z.string().trim().describe(SECRET_IMPORTS.LIST.projectId), + environment: z.string().trim().describe(SECRET_IMPORTS.LIST.environment), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.LIST.path) + }), + response: { + 200: z.object({ + message: z.string(), + secretImports: SecretImportsSchema.omit({ importEnv: true }) + .extend({ + importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() }) + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretImports = await server.services.secretImport.getImports({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query, + projectId: req.query.workspaceId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.query.workspaceId, + event: { + type: EventType.GET_SECRET_IMPORTS, + metadata: { + environment: req.query.environment, + folderId: secretImports?.[0]?.folderId, + numberOfImports: secretImports.length + } + } + }); + return { message: "Successfully fetched secret imports", secretImports }; + } + }); + + server.route({ + url: "/:secretImportId", + method: "GET", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], + description: "Get single secret import", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretImportId: z.string().trim().describe(SECRET_IMPORTS.GET.secretImportId) + }), + response: { + 200: z.object({ + secretImport: SecretImportsSchema.omit({ importEnv: true }).extend({ + environment: z.object({ + id: z.string(), + name: z.string(), + slug: z.string() + }), + projectId: z.string(), + importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() }), + secretPath: z.string() + }) + }) + } + }, + + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretImport = await server.services.secretImport.getImportById({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.secretImportId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: secretImport.projectId, + event: { + type: EventType.GET_SECRET_IMPORT, + metadata: { + secretImportId: secretImport.id, + folderId: secretImport.folderId + } + } + }); + + return { secretImport }; + } + }); + + server.route({ + url: "/secrets", + method: "GET", + config: { + rateLimit: secretsLimit + }, + schema: { + querystring: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + path: z.string().trim().default("/").transform(removeTrailingSlash) + }), + response: { + 200: z.object({ + secrets: z + .object({ + secretPath: z.string(), + environment: z.string(), + environmentInfo: z.object({ + id: z.string(), + name: z.string(), + slug: z.string() + }), + folderId: z.string().optional(), + secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const importedSecrets = await server.services.secretImport.getSecretsFromImports({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query, + projectId: req.query.workspaceId + }); + return { secrets: importedSecrets }; + } + }); + + server.route({ + url: "/secrets/raw", + method: "GET", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], + querystring: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + path: z.string().trim().default("/").transform(removeTrailingSlash) + }), + response: { + 200: z.object({ + secrets: z + .object({ + secretPath: z.string(), + environment: z.string(), + environmentInfo: z.object({ + id: z.string(), + name: z.string(), + slug: z.string() + }), + folderId: z.string().optional(), + secrets: secretRawSchema.array() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const importedSecrets = await server.services.secretImport.getRawSecretsFromImports({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query, + projectId: req.query.workspaceId + }); + return { secrets: importedSecrets }; + } + }); +}; diff --git a/backend/src/server/routes/v1/deprecated-secret-tag-router.ts b/backend/src/server/routes/v1/deprecated-secret-tag-router.ts new file mode 100644 index 000000000..d90475408 --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-secret-tag-router.ts @@ -0,0 +1,213 @@ +import { z } from "zod"; + +import { SecretTagsSchema } from "@app/db/schemas"; +import { ApiDocsTags, SECRET_TAGS } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerDeprecatedSecretTagRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/:projectId/tags", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Folders], + params: z.object({ + projectId: z.string().trim().describe(SECRET_TAGS.LIST.projectId) + }), + response: { + 200: z.object({ + workspaceTags: SecretTagsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspaceTags = await server.services.secretTag.getProjectTags({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.projectId + }); + return { workspaceTags }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/tags/:tagId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Folders], + params: z.object({ + projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.projectId), + tagId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.tagId) + }), + response: { + 200: z.object({ + // akhilmhdh: for terraform backward compatiability + workspaceTag: SecretTagsSchema.extend({ name: z.string() }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspaceTag = await server.services.secretTag.getTagById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.tagId + }); + return { workspaceTag }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/tags/slug/:tagSlug", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Folders], + params: z.object({ + projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.projectId), + tagSlug: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.tagSlug) + }), + response: { + 200: z.object({ + // akhilmhdh: for terraform backward compatiability + workspaceTag: SecretTagsSchema.extend({ name: z.string() }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspaceTag = await server.services.secretTag.getTagBySlug({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + slug: req.params.tagSlug, + projectId: req.params.projectId + }); + return { workspaceTag }; + } + }); + + server.route({ + method: "POST", + url: "/:projectId/tags", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Folders], + params: z.object({ + projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId) + }), + body: z.object({ + slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.CREATE.slug), + color: z.string().trim().describe(SECRET_TAGS.CREATE.color) + }), + response: { + 200: z.object({ + workspaceTag: SecretTagsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspaceTag = await server.services.secretTag.createTag({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.projectId, + ...req.body + }); + return { workspaceTag }; + } + }); + + server.route({ + method: "PATCH", + url: "/:projectId/tags/:tagId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Folders], + params: z.object({ + projectId: z.string().trim().describe(SECRET_TAGS.UPDATE.projectId), + tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId) + }), + body: z.object({ + slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.UPDATE.slug), + color: z.string().trim().describe(SECRET_TAGS.UPDATE.color) + }), + response: { + 200: z.object({ + workspaceTag: SecretTagsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspaceTag = await server.services.secretTag.updateTag({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + id: req.params.tagId + }); + return { workspaceTag }; + } + }); + + server.route({ + method: "DELETE", + url: "/:projectId/tags/:tagId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Folders], + params: z.object({ + projectId: z.string().trim().describe(SECRET_TAGS.DELETE.projectId), + tagId: z.string().trim().describe(SECRET_TAGS.DELETE.tagId) + }), + response: { + 200: z.object({ + workspaceTag: SecretTagsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspaceTag = await server.services.secretTag.deleteTag({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.tagId + }); + return { workspaceTag }; + } + }); +}; diff --git a/backend/src/server/routes/v2/group-project-router.ts b/backend/src/server/routes/v1/group-project-router.ts similarity index 100% rename from backend/src/server/routes/v2/group-project-router.ts rename to backend/src/server/routes/v1/group-project-router.ts diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts index 5d3612bf5..512f253e0 100644 --- a/backend/src/server/routes/v1/identity-ldap-auth-router.ts +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -8,7 +8,7 @@ import { Authenticator } from "@fastify/passport"; import fastifySession from "@fastify/session"; -import { FastifyRequest } from "fastify"; +import { FastifyReply, FastifyRequest } from "fastify"; import { IncomingMessage } from "http"; import LdapStrategy from "passport-ldapauth"; import { z } from "zod"; @@ -135,19 +135,26 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) }) } }, - preValidation: passport.authenticate("ldapauth", { - failWithError: true, - session: false - }) as any, + preValidation: [ + (req, res) => { + const passportAuth = (request: FastifyRequest, reply: FastifyReply) => + ( + passport.authenticate("ldapauth", { + failWithError: true, + session: false + }) as any + )(request, reply); - errorHandler: (error) => { - if (error.name === "AuthenticationError") { - throw new UnauthorizedError({ message: "Invalid credentials" }); + const { identityId, username } = req.body; + return server.services.identityLdapAuth.withLdapLockout( + { + identityId, + username + }, + () => passportAuth(req, res) + ); } - - throw error; - }, - + ], handler: async (req) => { if (!req.passportMachineIdentity?.identityId) { throw new UnauthorizedError({ message: "Invalid request. Missing identity ID or LDAP entry details." }); @@ -241,7 +248,21 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) .int() .min(0) .default(0) - .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit) + .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit), + lockoutEnabled: z.boolean().default(true).describe(LDAP_AUTH.ATTACH.lockoutEnabled), + lockoutThreshold: z.number().min(1).max(30).default(3).describe(LDAP_AUTH.ATTACH.lockoutThreshold), + lockoutDurationSeconds: z + .number() + .min(30) + .max(86400) + .default(300) + .describe(LDAP_AUTH.ATTACH.lockoutDurationSeconds), + lockoutCounterResetSeconds: z + .number() + .min(5) + .max(3600) + .default(30) + .describe(LDAP_AUTH.ATTACH.lockoutCounterResetSeconds) }) .refine( (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, @@ -291,7 +312,21 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) .int() .min(0) .default(0) - .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit) + .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit), + lockoutEnabled: z.boolean().default(true).describe(LDAP_AUTH.ATTACH.lockoutEnabled), + lockoutThreshold: z.number().min(1).max(30).default(3).describe(LDAP_AUTH.ATTACH.lockoutThreshold), + lockoutDurationSeconds: z + .number() + .min(30) + .max(86400) + .default(300) + .describe(LDAP_AUTH.ATTACH.lockoutDurationSeconds), + lockoutCounterResetSeconds: z + .number() + .min(5) + .max(3600) + .default(30) + .describe(LDAP_AUTH.ATTACH.lockoutCounterResetSeconds) }) .refine( (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, @@ -331,7 +366,11 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) accessTokenTTL: identityLdapAuth.accessTokenTTL, accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, allowedFields: req.body.allowedFields, - templateId: identityLdapAuth.templateId + templateId: identityLdapAuth.templateId, + lockoutEnabled: identityLdapAuth.lockoutEnabled, + lockoutThreshold: identityLdapAuth.lockoutThreshold, + lockoutDurationSeconds: identityLdapAuth.lockoutDurationSeconds, + lockoutCounterResetSeconds: identityLdapAuth.lockoutCounterResetSeconds } } }); @@ -395,7 +434,21 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) .max(315360000) .min(0) .optional() - .describe(LDAP_AUTH.UPDATE.accessTokenMaxTTL) + .describe(LDAP_AUTH.UPDATE.accessTokenMaxTTL), + lockoutEnabled: z.boolean().optional().describe(LDAP_AUTH.UPDATE.lockoutEnabled), + lockoutThreshold: z.number().min(1).max(30).optional().describe(LDAP_AUTH.UPDATE.lockoutThreshold), + lockoutDurationSeconds: z + .number() + .min(30) + .max(86400) + .optional() + .describe(LDAP_AUTH.UPDATE.lockoutDurationSeconds), + lockoutCounterResetSeconds: z + .number() + .min(5) + .max(3600) + .optional() + .describe(LDAP_AUTH.UPDATE.lockoutCounterResetSeconds) }) .refine( (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), @@ -434,7 +487,11 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[], allowedFields: req.body.allowedFields, - templateId: identityLdapAuth.templateId + templateId: identityLdapAuth.templateId, + lockoutEnabled: identityLdapAuth.lockoutEnabled, + lockoutThreshold: identityLdapAuth.lockoutThreshold, + lockoutDurationSeconds: identityLdapAuth.lockoutDurationSeconds, + lockoutCounterResetSeconds: identityLdapAuth.lockoutCounterResetSeconds } } }); @@ -553,4 +610,53 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) return { identityLdapAuth }; } }); + + server.route({ + method: "POST", + url: "/ldap-auth/identities/:identityId/clear-lockouts", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.LdapAuth], + description: "Clear LDAP Auth Lockouts for identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(LDAP_AUTH.CLEAR_CLIENT_LOCKOUTS.identityId) + }), + response: { + 200: z.object({ + deleted: z.number() + }) + } + }, + handler: async (req) => { + const clearLockoutsData = await server.services.identityLdapAuth.clearLdapAuthLockouts({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: clearLockoutsData.orgId, + event: { + type: EventType.CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS, + metadata: { + identityId: clearLockoutsData.identityId + } + } + }); + + return clearLockoutsData; + } + }); }; diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v1/identity-project-router.ts similarity index 100% rename from backend/src/server/routes/v2/identity-project-router.ts rename to backend/src/server/routes/v1/identity-project-router.ts diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 458efd194..4fb07aeac 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -13,8 +13,15 @@ import { registerCaRouter } from "./certificate-authority-router"; import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers"; import { registerCertRouter } from "./certificate-router"; import { registerCertificateTemplateRouter } from "./certificate-template-router"; +import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router"; +import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router"; +import { registerDeprecatedProjectRouter } from "./deprecated-project-router"; +import { registerDeprecatedSecretFolderRouter } from "./deprecated-secret-folder-router"; +import { registerDeprecatedSecretImportRouter } from "./deprecated-secret-import-router"; +import { registerDeprecatedSecretTagRouter } from "./deprecated-secret-tag-router"; import { registerEventRouter } from "./event-router"; import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router"; +import { registerGroupProjectRouter } from "./group-project-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; import { registerIdentityAliCloudAuthRouter } from "./identity-alicloud-auth-router"; import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router"; @@ -25,6 +32,7 @@ import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-rou import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router"; import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; +import { registerIdentityProjectRouter } from "./identity-project-router"; import { registerIdentityRouter } from "./identity-router"; import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router"; import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router"; @@ -33,6 +41,7 @@ import { registerIntegrationAuthRouter } from "./integration-auth-router"; import { registerIntegrationRouter } from "./integration-router"; import { registerInviteOrgRouter } from "./invite-org-router"; import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router"; +import { registerNotificationRouter } from "./notification-router"; import { registerOrgAdminRouter } from "./org-admin-router"; import { registerOrgRouter } from "./organization-router"; import { registerPasswordRouter } from "./password-router"; @@ -44,8 +53,6 @@ import { registerProjectKeyRouter } from "./project-key-router"; import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectRouter } from "./project-router"; import { SECRET_REMINDER_REGISTER_ROUTER_MAP } from "./reminder-routers"; -import { registerSecretFolderRouter } from "./secret-folder-router"; -import { registerSecretImportRouter } from "./secret-import-router"; import { registerSecretRequestsRouter } from "./secret-requests-router"; import { registerSecretSharingRouter } from "./secret-sharing-router"; import { registerSecretTagRouter } from "./secret-tag-router"; @@ -83,10 +90,11 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerAdminRouter, { prefix: "/admin" }); await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" }); await server.register(registerUserRouter, { prefix: "/user" }); + await server.register(registerNotificationRouter, { prefix: "/notifications" }); await server.register(registerInviteOrgRouter, { prefix: "/invite-org" }); await server.register(registerUserActionRouter, { prefix: "/user-action" }); - await server.register(registerSecretImportRouter, { prefix: "/secret-imports" }); - await server.register(registerSecretFolderRouter, { prefix: "/folders" }); + await server.register(registerDeprecatedSecretImportRouter, { prefix: "/secret-imports" }); + await server.register(registerDeprecatedSecretFolderRouter, { prefix: "/folders" }); await server.register( async (workflowIntegrationRouter) => { @@ -99,15 +107,28 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register( async (projectRouter) => { - await projectRouter.register(registerProjectRouter); - await projectRouter.register(registerProjectEnvRouter); + await projectRouter.register(registerDeprecatedProjectRouter); + await projectRouter.register(registerDeprecatedProjectEnvRouter); + // depreciated completed in use await projectRouter.register(registerProjectKeyRouter); - await projectRouter.register(registerProjectMembershipRouter); - await projectRouter.register(registerSecretTagRouter); + await projectRouter.register(registerDeprecatedProjectMembershipRouter); + await projectRouter.register(registerDeprecatedSecretTagRouter); }, { prefix: "/workspace" } ); + await server.register( + async (projectRouter) => { + await projectRouter.register(registerProjectRouter); + await projectRouter.register(registerProjectMembershipRouter); + await projectRouter.register(registerProjectEnvRouter); + await projectRouter.register(registerSecretTagRouter); + await projectRouter.register(registerGroupProjectRouter); + await projectRouter.register(registerIdentityProjectRouter); + }, + { prefix: "/projects" } + ); + await server.register( async (pkiRouter) => { await pkiRouter.register(registerCaRouter, { prefix: "/ca" }); diff --git a/backend/src/server/routes/v1/notification-router.ts b/backend/src/server/routes/v1/notification-router.ts new file mode 100644 index 000000000..5f72b88d6 --- /dev/null +++ b/backend/src/server/routes/v1/notification-router.ts @@ -0,0 +1,123 @@ +import { z } from "zod"; + +import { UserNotificationsSchema } from "@app/db/schemas/user-notifications"; +import { UnauthorizedError } from "@app/lib/errors"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerNotificationRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/user", + config: { + rateLimit: readLimit + }, + method: "GET", + schema: { + response: { + 200: z.object({ + notifications: UserNotificationsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + if (req.auth.authMode !== AuthMode.JWT) { + throw new UnauthorizedError({ message: "This endpoint can only be accessed by users" }); + } + + const notifications = await server.services.notification.listUserNotifications({ + userId: req.auth.userId, + orgId: req.auth.orgId + }); + + return { notifications }; + } + }); + + server.route({ + url: "/user/:notificationId", + config: { + rateLimit: writeLimit + }, + method: "DELETE", + schema: { + params: z.object({ + notificationId: z.string() + }), + response: { + 200: z.object({ + notification: UserNotificationsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + if (req.auth.authMode !== AuthMode.JWT) { + throw new UnauthorizedError({ message: "This endpoint can only be accessed by users" }); + } + + const notification = await server.services.notification.deleteUserNotification({ + notificationId: req.params.notificationId, + userId: req.auth.userId + }); + + return { notification }; + } + }); + + server.route({ + url: "/user/:notificationId", + config: { + rateLimit: writeLimit + }, + method: "PATCH", + schema: { + params: z.object({ + notificationId: z.string() + }), + body: z.object({ + isRead: z.boolean() + }), + response: { + 200: z.object({ + notification: UserNotificationsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + if (req.auth.authMode !== AuthMode.JWT) { + throw new UnauthorizedError({ message: "This endpoint can only be accessed by users" }); + } + + const notification = await server.services.notification.updateUserNotification({ + notificationId: req.params.notificationId, + userId: req.auth.userId, + ...req.body + }); + + return { notification }; + } + }); + + // Mark all user notifications as read + server.route({ + url: "/user/mark-as-read", + config: { + rateLimit: writeLimit + }, + method: "POST", + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + if (req.auth.authMode !== AuthMode.JWT) { + throw new UnauthorizedError({ message: "This endpoint can only be accessed by users" }); + } + + await server.services.notification.markUserNotificationsAsRead({ + userId: req.auth.userId, + orgId: req.auth.orgId + }); + } + }); +}; diff --git a/backend/src/server/routes/v1/project-env-router.ts b/backend/src/server/routes/v1/project-env-router.ts index 9a136e160..81b828b55 100644 --- a/backend/src/server/routes/v1/project-env-router.ts +++ b/backend/src/server/routes/v1/project-env-router.ts @@ -11,58 +11,7 @@ import { AuthMode } from "@app/services/auth/auth-type"; export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:workspaceId/environments/:envId", - config: { - rateLimit: readLimit - }, - schema: { - hide: false, - tags: [ApiDocsTags.Environments], - description: "Get Environment", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - // NOTE(daniel): workspaceId isn't used, but we need to keep it for backwards compatibility. The endpoint defined below, uses no project ID, and is takes a pure environment ID. - workspaceId: z.string().trim().describe(ENVIRONMENTS.GET.workspaceId), - envId: z.string().trim().describe(ENVIRONMENTS.GET.id) - }), - response: { - 200: z.object({ - environment: ProjectEnvironmentsSchema - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const environment = await server.services.projectEnv.getEnvironmentById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - id: req.params.envId - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: environment.projectId, - event: { - type: EventType.GET_ENVIRONMENT, - metadata: { - id: environment.id - } - } - }); - - return { environment }; - } - }); - - server.route({ - method: "GET", - url: "/environments/:envId", + url: "/:projectId/environments/:envId", config: { rateLimit: readLimit }, @@ -76,7 +25,8 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - envId: z.string().trim().describe(ENVIRONMENTS.GET.id) + envId: z.string().trim().describe(ENVIRONMENTS.GET.id), + projectId: z.string().trim().describe(ENVIRONMENTS.GET.projectId) }), response: { 200: z.object({ @@ -111,7 +61,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/:workspaceId/environments", + url: "/:projectId/environments", config: { rateLimit: writeLimit }, @@ -125,7 +75,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim().describe(ENVIRONMENTS.CREATE.workspaceId) + projectId: z.string().trim().describe(ENVIRONMENTS.CREATE.projectId) }), body: z.object({ name: z.string().trim().describe(ENVIRONMENTS.CREATE.name), @@ -135,7 +85,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ message: z.string(), - workspace: z.string(), + projectId: z.string(), environment: ProjectEnvironmentsSchema }) } @@ -147,7 +97,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, - projectId: req.params.workspaceId, + projectId: req.params.projectId, ...req.body }); @@ -164,7 +114,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { }); return { message: "Successfully created new environment", - workspace: req.params.workspaceId, + projectId: req.params.projectId, environment }; } @@ -172,7 +122,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", - url: "/:workspaceId/environments/:id", + url: "/:projectId/environments/:id", config: { rateLimit: writeLimit }, @@ -186,7 +136,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim().describe(ENVIRONMENTS.UPDATE.workspaceId), + projectId: z.string().trim().describe(ENVIRONMENTS.UPDATE.projectId), id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id) }), body: z.object({ @@ -197,7 +147,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ message: z.string(), - workspace: z.string(), + projectId: z.string(), environment: ProjectEnvironmentsSchema }) } @@ -209,7 +159,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, id: req.params.id, ...req.body }); @@ -232,7 +182,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { return { message: "Successfully updated environment", - workspace: req.params.workspaceId, + projectId: req.params.projectId, environment }; } @@ -240,7 +190,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", - url: "/:workspaceId/environments/:id", + url: "/:projectId/environments/:id", config: { rateLimit: writeLimit }, @@ -254,13 +204,13 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim().describe(ENVIRONMENTS.DELETE.workspaceId), + projectId: z.string().trim().describe(ENVIRONMENTS.DELETE.projectId), id: z.string().trim().describe(ENVIRONMENTS.DELETE.id) }), response: { 200: z.object({ message: z.string(), - workspace: z.string(), + projectId: z.string(), environment: ProjectEnvironmentsSchema }) } @@ -272,7 +222,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, id: req.params.id }); @@ -290,7 +240,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { return { message: "Successfully deleted environment", - workspace: req.params.workspaceId, + projectId: req.params.projectId, environment }; } diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index cd3734efc..f3cdfe701 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -1,7 +1,8 @@ import { z } from "zod"; import { - OrgMembershipsSchema, + OrgMembershipRole, + ProjectMembershipRole, ProjectMembershipsSchema, ProjectUserMembershipRolesSchema, UserEncryptionKeysSchema, @@ -18,7 +19,7 @@ import { ProjectUserMembershipTemporaryMode } from "@app/services/project-member export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:workspaceId/memberships", + url: "/:projectId/memberships", config: { rateLimit: readLimit }, @@ -32,7 +33,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider } ], params: z.object({ - workspaceId: z.string().trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIPS.workspaceId) + projectId: z.string().trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIPS.projectId) }), response: { 200: z.object({ @@ -71,7 +72,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + projectId: req.params.projectId }); return { memberships }; } @@ -79,7 +80,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider server.route({ method: "GET", - url: "/:workspaceId/memberships/:membershipId", + url: "/:projectId/memberships/:membershipId", config: { rateLimit: readLimit }, @@ -91,7 +92,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider } ], params: z.object({ - workspaceId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.workspaceId), + projectId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.projectId), membershipId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.membershipId) }), response: { @@ -129,7 +130,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, id: req.params.membershipId }); return { membership }; @@ -138,7 +139,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider server.route({ method: "POST", - url: "/:workspaceId/memberships/details", + url: "/:projectId/memberships/details", config: { rateLimit: readLimit }, @@ -152,7 +153,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider } ], params: z.object({ - workspaceId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.workspaceId) + projectId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.projectId) }), body: z.object({ username: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.username) @@ -191,7 +192,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, username: req.body.username }); return { membership }; @@ -200,61 +201,83 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider server.route({ method: "POST", - url: "/:workspaceId/memberships", + url: "/:projectId/memberships", config: { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], + description: "Invite members to project", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().describe(PROJECT_USERS.INVITE_MEMBER.projectId) }), body: z.object({ - members: z - .object({ - orgMembershipId: z.string().trim(), - workspaceEncryptedKey: z.string().trim(), - workspaceEncryptedNonce: z.string().trim() - }) + emails: z + .string() + .email() .array() - .min(1) + .default([]) + .describe(PROJECT_USERS.INVITE_MEMBER.emails) + .refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"), + usernames: z + .string() + .array() + .default([]) + .describe(PROJECT_USERS.INVITE_MEMBER.usernames) + .refine((val) => val.every((el) => el === el.toLowerCase()), "Username must be lowercase"), + roleSlugs: z.string().array().min(1).optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs) }), response: { 200: z.object({ - success: z.boolean(), - data: OrgMembershipsSchema.array() + memberships: ProjectMembershipsSchema.array() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const data = await server.services.projectMembership.addUsersToProject({ - actorId: req.permission.id, - actor: req.permission.type, + const usernamesAndEmails = [...req.body.emails, ...req.body.usernames]; + const { projectMemberships: memberships } = await server.services.org.inviteUserToOrganization({ actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, - members: req.body.members + actor: req.permission.type, + inviteeEmails: usernamesAndEmails, + orgId: req.permission.orgId, + organizationRoleSlug: OrgMembershipRole.NoAccess, + projects: [ + { + id: req.params.projectId, + projectRoleSlug: req.body.roleSlugs || [ProjectMembershipRole.Member] + } + ] }); await server.services.auditLog.createAuditLog({ - projectId: req.params.workspaceId, + projectId: req.params.projectId, ...req.auditLogInfo, event: { - type: EventType.ADD_BATCH_WORKSPACE_MEMBER, - metadata: data.map(({ userId }) => ({ + type: EventType.ADD_BATCH_PROJECT_MEMBER, + metadata: memberships.map(({ userId, id }) => ({ userId: userId || "", + membershipId: id, email: "" })) } }); - return { data, success: true }; + return { memberships }; } }); server.route({ method: "PATCH", - url: "/:workspaceId/memberships/:membershipId", + url: "/:projectId/memberships/:membershipId", config: { rateLimit: writeLimit }, @@ -268,7 +291,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider } ], params: z.object({ - workspaceId: z.string().trim().describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.workspaceId), + projectId: z.string().trim().describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.projectId), membershipId: z.string().trim().describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.membershipId) }), body: z.object({ @@ -305,31 +328,87 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, membershipId: req.params.membershipId, roles: req.body.roles }); - // await server.services.auditLog.createAuditLog({ - // ...req.auditLogInfo, - // projectId: req.params.workspaceId, - // event: { - // type: EventType.UPDATE_USER_WORKSPACE_ROLE, - // metadata: { - // userId: membership.userId, - // newRole: req.body.role, - // oldRole: membership.role, - // email: "" - // } - // } - // }); return { roles }; } }); server.route({ method: "DELETE", - url: "/:workspaceId/memberships/:membershipId", + url: "/:projectId/memberships", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], + description: "Remove members from project", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().describe(PROJECT_USERS.REMOVE_MEMBER.projectId) + }), + body: z.object({ + emails: z + .string() + .email() + .array() + .default([]) + .describe(PROJECT_USERS.REMOVE_MEMBER.emails) + .refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"), + usernames: z + .string() + .array() + .default([]) + .describe(PROJECT_USERS.REMOVE_MEMBER.usernames) + .refine((val) => val.every((el) => el === el.toLowerCase()), "Username must be lowercase") + }), + response: { + 200: z.object({ + memberships: ProjectMembershipsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const memberships = await server.services.projectMembership.deleteProjectMemberships({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.projectId, + emails: req.body.emails, + usernames: req.body.usernames + }); + + for (const membership of memberships) { + // eslint-disable-next-line no-await-in-loop + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.projectId, + event: { + type: EventType.REMOVE_PROJECT_MEMBER, + metadata: { + userId: membership.userId, + email: "" + } + } + }); + } + return { memberships }; + } + }); + + server.route({ + method: "DELETE", + url: "/:projectId/memberships/:membershipId", config: { rateLimit: writeLimit }, @@ -341,7 +420,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider } ], params: z.object({ - workspaceId: z.string().trim(), + projectId: z.string().trim(), membershipId: z.string().trim() }), response: { @@ -357,15 +436,15 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, membershipId: req.params.membershipId }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.params.workspaceId, + projectId: req.params.projectId, event: { - type: EventType.REMOVE_WORKSPACE_MEMBER, + type: EventType.REMOVE_PROJECT_MEMBER, metadata: { userId: membership.userId, email: "" @@ -378,13 +457,13 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider server.route({ method: "DELETE", - url: "/:workspaceId/leave", + url: "/:projectId/leave", config: { rateLimit: writeLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), response: { 200: z.object({ @@ -398,7 +477,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider const membership = await server.services.projectMembership.leaveProject({ actorId: req.permission.id, actor: req.permission.type, - projectId: req.params.workspaceId + projectId: req.params.projectId }); return { membership }; } diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 5e7dce76e..be8b46dd5 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -2,7 +2,10 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { + CertificatesSchema, IntegrationsSchema, + PkiAlertsSchema, + PkiCollectionsSchema, ProjectEnvironmentsSchema, ProjectMembershipsSchema, ProjectRolesSchema, @@ -16,18 +19,35 @@ import { } from "@app/db/schemas"; import { ProjectMicrosoftTeamsConfigsSchema } from "@app/db/schemas/project-microsoft-teams-configs"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types"; +import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema"; +import { sanitizedSshCertificate } from "@app/ee/services/ssh-certificate/ssh-certificate-schema"; +import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema"; +import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema"; +import { LoginMappingSource } from "@app/ee/services/ssh-host/ssh-host-types"; +import { sanitizedSshHostGroup } from "@app/ee/services/ssh-host-group/ssh-host-group-schema"; import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; import { re2Validator } from "@app/lib/zod"; import { readLimit, requestAccessLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; +import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums"; +import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema"; import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns"; +import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema"; import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types"; import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types"; -import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas"; +import { + integrationAuthPubSchema, + InternalCertificateAuthorityResponseSchema, + SanitizedProjectSchema +} from "../sanitizedSchemas"; import { sanitizedServiceTokenSchema } from "../v2/service-token-router"; const projectWithEnv = SanitizedProjectSchema.merge( @@ -40,41 +60,7 @@ const projectWithEnv = SanitizedProjectSchema.merge( export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:workspaceId/keys", - config: { - rateLimit: readLimit - }, - schema: { - params: z.object({ - workspaceId: z.string().trim() - }), - response: { - 200: z.object({ - publicKeys: z - .object({ - publicKey: z.string().nullable().optional(), - userId: z.string() - }) - .array() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const publicKeys = await server.services.projectKey.getProjectPublicKeys({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId - }); - return { publicKeys }; - } - }); - - server.route({ - method: "GET", - url: "/:workspaceId/users", + url: "/:projectId/users", config: { rateLimit: readLimit }, @@ -96,7 +82,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { .optional() }), params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), response: { 200: z.object({ @@ -142,7 +128,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorAuthMethod: req.permission.authMethod, includeGroupMembers: req.query.includeGroupMembers, - projectId: req.params.workspaceId, + projectId: req.params.projectId, actorOrgId: req.permission.orgId, roles }); @@ -151,6 +137,83 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Projects], + description: "Create a new project", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectName: z.string().trim().describe(PROJECTS.CREATE.projectName), + projectDescription: z.string().trim().optional().describe(PROJECTS.CREATE.projectDescription), + slug: slugSchema({ min: 5, max: 36 }).optional().describe(PROJECTS.CREATE.slug), + kmsKeyId: z.string().optional(), + template: slugSchema({ field: "Template Name", max: 64 }) + .optional() + .default(InfisicalProjectTemplate.Default) + .describe(PROJECTS.CREATE.template), + type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager), + shouldCreateDefaultEnvs: z.boolean().optional().default(true) + }), + response: { + 200: z.object({ + project: projectWithEnv + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const project = await server.services.project.createProject({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + projectName: req.body.projectName, + projectDescription: req.body.projectDescription, + slug: req.body.slug, + kmsKeyId: req.body.kmsKeyId, + template: req.body.template, + type: req.body.type, + createDefaultEnvs: req.body.shouldCreateDefaultEnvs + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.ProjectCreated, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + orgId: project.orgId, + name: project.name, + ...req.auditLogInfo + } + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: project.id, + event: { + type: EventType.CREATE_PROJECT, + metadata: { + ...req.body, + name: req.body.projectName + } + } + }); + + return { project }; + } + }); + server.route({ method: "GET", url: "/", @@ -158,6 +221,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], + description: "List projects", + security: [ + { + bearerAuth: [] + } + ], querystring: z.object({ includeRoles: z .enum(["true", "false"]) @@ -167,7 +238,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - workspaces: projectWithEnv + projects: projectWithEnv .extend({ roles: ProjectRolesSchema.array().optional() }) @@ -177,7 +248,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const workspaces = await server.services.project.getProjects({ + const projects = await server.services.project.getProjects({ includeRoles: req.query.includeRoles, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -185,13 +256,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, type: req.query.type }); - return { workspaces }; + return { projects }; } }); server.route({ method: "GET", - url: "/:workspaceId", + url: "/:projectId", config: { rateLimit: readLimit }, @@ -205,33 +276,73 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim().describe(PROJECTS.GET.workspaceId) + projectId: z.string().trim().describe(PROJECTS.GET.projectId) }), response: { 200: z.object({ - workspace: projectWithEnv.optional() + project: projectWithEnv.optional() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const workspace = await server.services.project.getAProject({ + const project = await server.services.project.getAProject({ filter: { type: ProjectFilterType.ID, - projectId: req.params.workspaceId + projectId: req.params.projectId }, actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId }); - return { workspace }; + return { project }; + } + }); + + server.route({ + method: "GET", + url: "/slug/:slug", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Projects], + description: "Get project details by slug", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + slug: slugSchema({ max: 36 }).describe("The slug of the project to get.") + }), + response: { + 200: projectWithEnv + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const project = await server.services.project.getAProject({ + filter: { + slug: req.params.slug, + orgId: req.permission.orgId, + type: ProjectFilterType.SLUG + }, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type + }); + + return project; } }); server.route({ method: "DELETE", - url: "/:workspaceId", + url: "/:projectId", config: { rateLimit: writeLimit }, @@ -245,20 +356,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim().describe(PROJECTS.DELETE.workspaceId) + projectId: z.string().trim().describe(PROJECTS.DELETE.projectId) }), response: { 200: z.object({ - workspace: SanitizedProjectSchema.optional() + project: SanitizedProjectSchema.optional() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const workspace = await server.services.project.deleteProject({ + const project = await server.services.project.deleteProject({ filter: { type: ProjectFilterType.ID, - projectId: req.params.workspaceId + projectId: req.params.projectId }, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -269,68 +380,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, event: { type: EventType.DELETE_PROJECT, - metadata: workspace + metadata: project } }); - return { workspace }; - } - }); - - server.route({ - url: "/:workspaceId/name", - method: "POST", - config: { - rateLimit: writeLimit - }, - schema: { - params: z.object({ - workspaceId: z.string().trim() - }), - body: z.object({ - name: z.string().trim() - }), - response: { - 200: z.object({ - message: z.string(), - workspace: SanitizedProjectSchema - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const workspace = await server.services.project.updateName({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, - name: req.body.name - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - orgId: req.permission.orgId, - projectId: req.params.workspaceId, - event: { - type: EventType.UPDATE_PROJECT, - metadata: req.body - } - }); - - return { - message: "Successfully changed workspace name", - workspace - }; + return { project }; } }); server.route({ method: "PATCH", - url: "/:workspaceId", + url: "/:projectId", config: { rateLimit: writeLimit }, @@ -344,7 +407,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim().describe(PROJECTS.UPDATE.workspaceId) + projectId: z.string().trim().describe(PROJECTS.UPDATE.projectId) }), body: z.object({ name: z @@ -373,35 +436,35 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { .describe(PROJECTS.UPDATE.slug), secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing), showSnapshotsLegacy: z.boolean().optional().describe(PROJECTS.UPDATE.showSnapshotsLegacy), - defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct), secretDetectionIgnoreValues: z .array(z.string()) .optional() - .describe(PROJECTS.UPDATE.secretDetectionIgnoreValues) + .describe(PROJECTS.UPDATE.secretDetectionIgnoreValues), + pitVersionLimit: z.number().min(1).max(100).optional() }), response: { 200: z.object({ - workspace: SanitizedProjectSchema + project: SanitizedProjectSchema }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const workspace = await server.services.project.updateProject({ + const project = await server.services.project.updateProject({ filter: { type: ProjectFilterType.ID, - projectId: req.params.workspaceId + projectId: req.params.projectId }, update: { name: req.body.name, description: req.body.description, autoCapitalization: req.body.autoCapitalization, - defaultProduct: req.body.defaultProduct, hasDeleteProtection: req.body.hasDeleteProtection, slug: req.body.slug, secretSharing: req.body.secretSharing, showSnapshotsLegacy: req.body.showSnapshotsLegacy, - secretDetectionIgnoreValues: req.body.secretDetectionIgnoreValues + secretDetectionIgnoreValues: req.body.secretDetectionIgnoreValues, + pitVersionLimit: req.body.pitVersionLimit }, actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, @@ -412,7 +475,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, event: { type: EventType.UPDATE_PROJECT, metadata: req.body @@ -420,164 +483,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }); return { - workspace - }; - } - }); - - server.route({ - method: "POST", - url: "/:workspaceId/auto-capitalization", - config: { - rateLimit: writeLimit - }, - schema: { - params: z.object({ - workspaceId: z.string().trim() - }), - body: z.object({ - autoCapitalization: z.boolean() - }), - response: { - 200: z.object({ - message: z.string(), - workspace: SanitizedProjectSchema - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const workspace = await server.services.project.toggleAutoCapitalization({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, - autoCapitalization: req.body.autoCapitalization - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - orgId: req.permission.orgId, - projectId: req.params.workspaceId, - event: { - type: EventType.UPDATE_PROJECT, - metadata: req.body - } - }); - - return { - message: "Successfully changed workspace settings", - workspace - }; - } - }); - - server.route({ - method: "POST", - url: "/:workspaceId/delete-protection", - config: { - rateLimit: writeLimit - }, - schema: { - params: z.object({ - workspaceId: z.string().trim() - }), - body: z.object({ - hasDeleteProtection: z.boolean() - }), - response: { - 200: z.object({ - message: z.string(), - workspace: SanitizedProjectSchema - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const workspace = await server.services.project.toggleDeleteProtection({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, - hasDeleteProtection: req.body.hasDeleteProtection - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - orgId: req.permission.orgId, - projectId: req.params.workspaceId, - event: { - type: EventType.UPDATE_PROJECT, - metadata: req.body - } - }); - - return { - message: "Successfully changed workspace settings", - workspace + project }; } }); server.route({ method: "PUT", - url: "/:workspaceSlug/version-limit", + url: "/:projectId/audit-logs-retention", config: { rateLimit: writeLimit }, schema: { params: z.object({ - workspaceSlug: z.string().trim() - }), - body: z.object({ - pitVersionLimit: z.number().min(1).max(100) - }), - response: { - 200: z.object({ - message: z.string(), - workspace: SanitizedProjectSchema - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const workspace = await server.services.project.updateVersionLimit({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - pitVersionLimit: req.body.pitVersionLimit, - workspaceSlug: req.params.workspaceSlug - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - orgId: req.permission.orgId, - projectId: workspace.id, - event: { - type: EventType.UPDATE_PROJECT, - metadata: req.body - } - }); - - return { - message: "Successfully changed workspace version limit", - workspace - }; - } - }); - - server.route({ - method: "PUT", - url: "/:workspaceSlug/audit-logs-retention", - config: { - rateLimit: writeLimit - }, - schema: { - params: z.object({ - workspaceSlug: z.string().trim() + projectId: z.string().trim() }), body: z.object({ auditLogsRetentionDays: z.number().min(0) @@ -585,25 +504,28 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ message: z.string(), - workspace: SanitizedProjectSchema + project: SanitizedProjectSchema }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const workspace = await server.services.project.updateAuditLogsRetention({ + const project = await server.services.project.updateAuditLogsRetention({ actorId: req.permission.id, actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - workspaceSlug: req.params.workspaceSlug, - auditLogsRetentionDays: req.body.auditLogsRetentionDays + auditLogsRetentionDays: req.body.auditLogsRetentionDays, + filter: { + projectId: req.params.projectId, + type: ProjectFilterType.ID + } }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: workspace.id, + projectId: project.id, event: { type: EventType.UPDATE_PROJECT, metadata: req.body @@ -612,14 +534,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return { message: "Successfully updated project's audit logs retention period", - workspace + project }; } }); server.route({ method: "GET", - url: "/:workspaceId/integrations", + url: "/:projectId/integrations", config: { rateLimit: readLimit }, @@ -633,7 +555,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION.workspaceId) + projectId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION.projectId) }), response: { 200: z.object({ @@ -656,7 +578,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + projectId: req.params.projectId }); return { integrations }; } @@ -664,21 +586,21 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:workspaceId/authorizations", + url: "/:projectId/authorizations", config: { rateLimit: readLimit }, schema: { hide: false, tags: [ApiDocsTags.Integrations], - description: "List integration auth objects for a workspace.", + description: "List integration auth objects for a project.", security: [ { bearerAuth: [] } ], params: z.object({ - workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION_AUTHORIZATION.workspaceId) + projectId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION_AUTHORIZATION.projectId) }), response: { 200: z.object({ @@ -693,7 +615,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + projectId: req.params.projectId }); return { authorizations }; } @@ -701,13 +623,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:workspaceId/service-token-data", + url: "/:projectId/service-token-data", config: { rateLimit: readLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), response: { 200: z.object({ @@ -722,7 +644,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + projectId: req.params.projectId }); return { serviceTokenData }; } @@ -730,13 +652,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:workspaceId/ssh-config", + url: "/:projectId/ssh-config", config: { rateLimit: readLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), response: { 200: ProjectSshConfigsSchema.pick({ @@ -756,7 +678,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + projectId: req.params.projectId }); await server.services.auditLog.createAuditLog({ @@ -777,13 +699,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", - url: "/:workspaceId/ssh-config", + url: "/:projectId/ssh-config", config: { rateLimit: writeLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), body: z.object({ defaultUserSshCaId: z.string().optional(), @@ -807,7 +729,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, ...req.body }); @@ -831,13 +753,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:workspaceId/workflow-integration-config/:integration", + url: "/:projectId/workflow-integration-config/:integration", config: { rateLimit: readLimit }, schema: { params: z.object({ - workspaceId: z.string().trim(), + projectId: z.string().trim(), integration: z.nativeEnum(WorkflowIntegration) }), response: { @@ -876,13 +798,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, integration: req.params.integration }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.params.workspaceId, + projectId: req.params.projectId, event: { type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG, metadata: { @@ -936,15 +858,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "PUT", - url: "/:workspaceId/workflow-integration", + url: "/:projectId/workflow-integration", config: { rateLimit: readLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), - body: z.discriminatedUnion("integration", [ z.object({ integration: z.literal(WorkflowIntegration.SLACK), @@ -999,13 +920,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + projectId: req.params.projectId, ...req.body }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.params.workspaceId, + projectId: req.params.projectId, event: { type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG, metadata: { @@ -1026,13 +947,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:workspaceId/environment-folder-tree", + url: "/:projectId/environment-folder-tree", config: { rateLimit: readLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), response: { 200: z.record( @@ -1043,7 +964,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const environmentsFolders = await server.services.folder.getProjectEnvironmentsFolders( - req.params.workspaceId, + req.params.projectId, req.permission ); @@ -1064,6 +985,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { type: z.nativeEnum(ProjectType).optional(), orderBy: z.nativeEnum(SearchProjectSortBy).optional().default(SearchProjectSortBy.NAME), orderDirection: z.nativeEnum(SortDirection).optional().default(SortDirection.ASC), + projectIds: z.string().trim().array().optional(), name: z .string() .trim() @@ -1092,13 +1014,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/:workspaceId/project-access", + url: "/:projectId/project-access", config: { rateLimit: requestAccessLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + projectId: z.string().trim() }), body: z.object({ comment: z @@ -1132,17 +1054,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { await server.services.project.requestProjectAccess({ permission: req.permission, comment: req.body.comment, - projectId: req.params.workspaceId + projectId: req.params.projectId }); if (req.auth.actor === ActorType.USER) { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.params.workspaceId, + projectId: req.params.projectId, event: { type: EventType.PROJECT_ACCESS_REQUEST, metadata: { - projectId: req.params.workspaceId, + projectId: req.params.projectId, requesterEmail: req.auth.user.email || req.auth.user.username, requesterId: req.auth.userId } @@ -1153,4 +1075,456 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return { message: "Project access request has been send to project admins" }; } }); + + /* Start upgrade of a project */ + server.route({ + method: "POST", + url: "/:projectId/upgrade", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim() + }), + body: z.object({ + userPrivateKey: z.string().trim() + }), + response: { + 200: z.void() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + await server.services.project.upgradeProject({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + projectId: req.params.projectId, + userPrivateKey: req.body.userPrivateKey + }); + } + }); + + /* Get upgrade status of project */ + server.route({ + url: "/:projectId/upgrade/status", + method: "GET", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim() + }), + response: { + 200: z.object({ + status: z.string().nullable() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const status = await server.services.project.getProjectUpgradeStatus({ + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.projectId, + actor: req.permission.type, + actorId: req.permission.id + }); + + return { status }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/cas", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + params: z.object({ + projectId: z.string().trim() + }), + querystring: z.object({ + status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional().describe(PROJECTS.LIST_CAS.status), + friendlyName: z.string().optional().describe(PROJECTS.LIST_CAS.friendlyName), + commonName: z.string().optional().describe(PROJECTS.LIST_CAS.commonName), + offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CAS.offset), + limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CAS.limit) + }), + response: { + 200: z.object({ + cas: z.array(InternalCertificateAuthorityResponseSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const cas = await server.services.project.listProjectCas({ + filter: { + projectId: req.params.projectId, + type: ProjectFilterType.ID + }, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + ...req.query + }); + return { cas }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/certificates", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + params: z.object({ + projectId: z.string().trim() + }), + querystring: z.object({ + friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName), + commonName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.commonName), + offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset), + limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CERTIFICATES.limit) + }), + response: { + 200: z.object({ + certificates: z.array(CertificatesSchema), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificates, totalCount } = await server.services.project.listProjectCertificates({ + filter: { + projectId: req.params.projectId, + type: ProjectFilterType.ID + }, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + ...req.query + }); + return { certificates, totalCount }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/pki-alerts", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiAlerting], + params: z.object({ + projectId: z.string().trim() + }), + response: { + 200: z.object({ + alerts: z.array(PkiAlertsSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { alerts } = await server.services.project.listProjectAlerts({ + projectId: req.params.projectId, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type + }); + + return { alerts }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/pki-collections", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], + params: z.object({ + projectId: z.string().trim() + }), + response: { + 200: z.object({ + collections: z.array(PkiCollectionsSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { pkiCollections } = await server.services.project.listProjectPkiCollections({ + projectId: req.params.projectId, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type + }); + + return { collections: pkiCollections }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/pki-subscribers", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.LIST_PKI_SUBSCRIBERS.projectId) + }), + response: { + 200: z.object({ + subscribers: z.array(sanitizedPkiSubscriber) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const subscribers = await server.services.project.listProjectPkiSubscribers({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + projectId: req.params.projectId + }); + + return { subscribers }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/certificate-templates", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], + params: z.object({ + projectId: z.string().trim() + }), + response: { + 200: z.object({ + certificateTemplates: sanitizedCertificateTemplate.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificateTemplates } = await server.services.project.listProjectCertificateTemplates({ + projectId: req.params.projectId, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type + }); + + return { certificateTemplates }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/ssh-certificates", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CAS.projectId) + }), + querystring: z.object({ + offset: z.coerce.number().default(0).describe(PROJECTS.LIST_SSH_CERTIFICATES.offset), + limit: z.coerce.number().default(25).describe(PROJECTS.LIST_SSH_CERTIFICATES.limit) + }), + response: { + 200: z.object({ + certificates: z.array(sanitizedSshCertificate), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificates, totalCount } = await server.services.project.listProjectSshCertificates({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + projectId: req.params.projectId, + offset: req.query.offset, + limit: req.query.limit + }); + + return { certificates, totalCount }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/ssh-certificate-templates", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateTemplates], + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CERTIFICATE_TEMPLATES.projectId) + }), + response: { + 200: z.object({ + certificateTemplates: z.array(sanitizedSshCertificateTemplate) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificateTemplates } = await server.services.project.listProjectSshCertificateTemplates({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + projectId: req.params.projectId + }); + + return { certificateTemplates }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/ssh-cas", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CAS.projectId) + }), + response: { + 200: z.object({ + cas: z.array(sanitizedSshCa) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const cas = await server.services.project.listProjectSshCas({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + projectId: req.params.projectId + }); + + return { cas }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/ssh-hosts", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SshHosts], + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOSTS.projectId) + }), + response: { + 200: z.object({ + hosts: z.array( + sanitizedSshHost.extend({ + loginMappings: loginMappingSchema + .extend({ + source: z.nativeEnum(LoginMappingSource) + }) + .array() + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const hosts = await server.services.project.listProjectSshHosts({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + projectId: req.params.projectId + }); + + return { hosts }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/ssh-host-groups", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SshHostGroups], + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOST_GROUPS.projectId) + }), + response: { + 200: z.object({ + groups: z.array( + sanitizedSshHostGroup.extend({ + loginMappings: loginMappingSchema.array(), + hostCount: z.number() + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const groups = await server.services.project.listProjectSshHostGroups({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + projectId: req.params.projectId + }); + + return { groups }; + } + }); }; diff --git a/backend/src/server/routes/v1/secret-tag-router.ts b/backend/src/server/routes/v1/secret-tag-router.ts index 01ba783fe..3c6c99eaf 100644 --- a/backend/src/server/routes/v1/secret-tag-router.ts +++ b/backend/src/server/routes/v1/secret-tag-router.ts @@ -22,20 +22,20 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - workspaceTags: SecretTagsSchema.array() + tags: SecretTagsSchema.array() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const workspaceTags = await server.services.secretTag.getProjectTags({ + const tags = await server.services.secretTag.getProjectTags({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.projectId }); - return { workspaceTags }; + return { tags }; } }); @@ -55,20 +55,20 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ // akhilmhdh: for terraform backward compatiability - workspaceTag: SecretTagsSchema.extend({ name: z.string() }) + tag: SecretTagsSchema.extend({ name: z.string() }) }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const workspaceTag = await server.services.secretTag.getTagById({ + const tag = await server.services.secretTag.getTagById({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.tagId }); - return { workspaceTag }; + return { tag }; } }); @@ -88,13 +88,13 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ // akhilmhdh: for terraform backward compatiability - workspaceTag: SecretTagsSchema.extend({ name: z.string() }) + tag: SecretTagsSchema.extend({ name: z.string() }) }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const workspaceTag = await server.services.secretTag.getTagBySlug({ + const tag = await server.services.secretTag.getTagBySlug({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -102,7 +102,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { slug: req.params.tagSlug, projectId: req.params.projectId }); - return { workspaceTag }; + return { tag }; } }); @@ -124,13 +124,13 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - workspaceTag: SecretTagsSchema + tag: SecretTagsSchema }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const workspaceTag = await server.services.secretTag.createTag({ + const tag = await server.services.secretTag.createTag({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -138,7 +138,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { projectId: req.params.projectId, ...req.body }); - return { workspaceTag }; + return { tag }; } }); @@ -161,13 +161,13 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - workspaceTag: SecretTagsSchema + tag: SecretTagsSchema }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const workspaceTag = await server.services.secretTag.updateTag({ + const tag = await server.services.secretTag.updateTag({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -175,7 +175,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { ...req.body, id: req.params.tagId }); - return { workspaceTag }; + return { tag }; } }); @@ -194,20 +194,20 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - workspaceTag: SecretTagsSchema + tag: SecretTagsSchema }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const workspaceTag = await server.services.secretTag.deleteTag({ + const tag = await server.services.secretTag.deleteTag({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.tagId }); - return { workspaceTag }; + return { tag }; } }); }; diff --git a/backend/src/server/routes/v1/webhook-router.ts b/backend/src/server/routes/v1/webhook-router.ts index 377af135c..386628d28 100644 --- a/backend/src/server/routes/v1/webhook-router.ts +++ b/backend/src/server/routes/v1/webhook-router.ts @@ -39,7 +39,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { body: z .object({ type: z.nativeEnum(WebhookType).default(WebhookType.GENERAL), - workspaceId: z.string().trim(), + projectId: z.string().trim(), environment: z.string().trim(), webhookUrl: z.string().url().trim(), webhookSecretKey: z.string().trim().optional(), @@ -67,13 +67,12 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.body.workspaceId, ...req.body }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.body.workspaceId, + projectId: req.body.projectId, event: { type: EventType.CREATE_WEBHOOK, metadata: { @@ -216,7 +215,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT]), schema: { querystring: z.object({ - workspaceId: z.string().trim(), + projectId: z.string().trim(), environment: z.string().trim().optional(), secretPath: z .string() @@ -238,7 +237,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.query, - projectId: req.query.workspaceId + projectId: req.query.projectId }); return { message: "Successfully fetched webhook", webhooks }; } diff --git a/backend/src/server/routes/v2/deprecated-group-project-router.ts b/backend/src/server/routes/v2/deprecated-group-project-router.ts new file mode 100644 index 000000000..f0e4ee705 --- /dev/null +++ b/backend/src/server/routes/v2/deprecated-group-project-router.ts @@ -0,0 +1,363 @@ +import { z } from "zod"; + +import { + GroupProjectMembershipsSchema, + GroupsSchema, + ProjectMembershipRole, + ProjectUserMembershipRolesSchema, + UsersSchema +} from "@app/db/schemas"; +import { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; +import { ApiDocsTags, GROUPS, PROJECTS } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; + +export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/:projectId/groups/:groupIdOrName", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], + description: "Add group to project", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectId), + groupIdOrName: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupIdOrName) + }), + body: z + .object({ + role: z + .string() + .trim() + .min(1) + .default(ProjectMembershipRole.NoAccess) + .describe(PROJECTS.ADD_GROUP_TO_PROJECT.role), + roles: z + .array( + z.union([ + z.object({ + role: z.string(), + isTemporary: z.literal(false).default(false) + }), + z.object({ + role: z.string(), + isTemporary: z.literal(true), + temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), + temporaryAccessStartTime: z.string().datetime() + }) + ]) + ) + .optional() + }) + .refine((data) => data.role || data.roles, { + message: "Either role or roles must be present", + path: ["role", "roles"] + }), + response: { + 200: z.object({ + groupMembership: GroupProjectMembershipsSchema + }) + } + }, + handler: async (req) => { + const groupMembership = await server.services.groupProject.addGroupToProject({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + roles: req.body.roles || [{ role: req.body.role }], + projectId: req.params.projectId, + groupIdOrName: req.params.groupIdOrName + }); + + return { groupMembership }; + } + }); + + server.route({ + method: "PATCH", + url: "/:projectId/groups/:groupId", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], + description: "Update group in project", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.projectId), + groupId: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.groupId) + }), + body: z.object({ + roles: z + .array( + z.union([ + z.object({ + role: z.string(), + isTemporary: z.literal(false).default(false) + }), + z.object({ + role: z.string(), + isTemporary: z.literal(true), + temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), + temporaryAccessStartTime: z.string().datetime() + }) + ]) + ) + .min(1) + .describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.roles) + }), + response: { + 200: z.object({ + roles: ProjectUserMembershipRolesSchema.array() + }) + } + }, + handler: async (req) => { + const roles = await server.services.groupProject.updateGroupInProject({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.projectId, + groupId: req.params.groupId, + roles: req.body.roles + }); + + return { roles }; + } + }); + + server.route({ + method: "DELETE", + url: "/:projectId/groups/:groupId", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], + description: "Remove group from project", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.projectId), + groupId: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.groupId) + }), + response: { + 200: z.object({ + groupMembership: GroupProjectMembershipsSchema + }) + } + }, + handler: async (req) => { + const groupMembership = await server.services.groupProject.removeGroupFromProject({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + groupId: req.params.groupId, + projectId: req.params.projectId + }); + + return { groupMembership }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/groups", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], + description: "Return list of groups in project", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.LIST_GROUPS_IN_PROJECT.projectId) + }), + response: { + 200: z.object({ + groupMemberships: z + .object({ + id: z.string(), + groupId: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ), + group: GroupsSchema.pick({ name: true, id: true, slug: true }) + }) + .array() + }) + } + }, + handler: async (req) => { + const groupMemberships = await server.services.groupProject.listGroupsInProject({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.projectId + }); + + return { groupMemberships }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/groups/:groupId", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], + description: "Return project group", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().trim(), + groupId: z.string().trim() + }), + response: { + 200: z.object({ + groupMembership: z.object({ + id: z.string(), + groupId: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ), + group: GroupsSchema.pick({ name: true, id: true, slug: true }) + }) + }) + } + }, + handler: async (req) => { + const groupMembership = await server.services.groupProject.getGroupInProject({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params + }); + + return { groupMembership }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/groups/:groupId/users", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], + description: "Return project group users", + params: z.object({ + projectId: z.string().trim().describe(GROUPS.LIST_USERS.projectId), + groupId: z.string().trim().describe(GROUPS.LIST_USERS.id) + }), + querystring: z.object({ + offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset), + limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit), + username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username), + search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search), + filter: z.nativeEnum(EFilterReturnedUsers).optional().describe(GROUPS.LIST_USERS.filterUsers) + }), + response: { + 200: z.object({ + users: UsersSchema.pick({ + email: true, + username: true, + firstName: true, + lastName: true, + id: true + }) + .merge( + z.object({ + isPartOfGroup: z.boolean(), + joinedGroupAt: z.date().nullable() + }) + ) + .array(), + totalCount: z.number() + }) + } + }, + handler: async (req) => { + const { users, totalCount } = await server.services.groupProject.listProjectGroupUsers({ + id: req.params.groupId, + projectId: req.params.projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + + return { users, totalCount }; + } + }); +}; diff --git a/backend/src/server/routes/v2/deprecated-identity-project-router.ts b/backend/src/server/routes/v2/deprecated-identity-project-router.ts new file mode 100644 index 000000000..c16c874ae --- /dev/null +++ b/backend/src/server/routes/v2/deprecated-identity-project-router.ts @@ -0,0 +1,418 @@ +import { z } from "zod"; + +import { + IdentitiesSchema, + IdentityProjectMembershipsSchema, + ProjectMembershipRole, + ProjectUserMembershipRolesSchema +} from "@app/db/schemas"; +import { ApiDocsTags, ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs"; +import { BadRequestError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { OrderByDirection } from "@app/lib/types"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { ProjectIdentityOrderBy } from "@app/services/identity-project/identity-project-types"; +import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; + +import { SanitizedProjectSchema } from "../sanitizedSchemas"; + +export const registerDeprecatedIdentityProjectRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/:projectId/identity-memberships/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], + description: "Create project identity membership", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().trim(), + identityId: z.string().trim() + }), + body: z.object({ + // @depreciated + role: z.string().trim().optional().default(ProjectMembershipRole.NoAccess), + roles: z + .array( + z.union([ + z.object({ + role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + isTemporary: z + .literal(false) + .default(false) + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role) + }), + z.object({ + role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + temporaryMode: z + .nativeEnum(ProjectUserMembershipTemporaryMode) + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role) + }) + ]) + ) + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.description) + .optional() + }), + response: { + 200: z.object({ + identityMembership: IdentityProjectMembershipsSchema + }) + } + }, + handler: async (req) => { + const { role, roles } = req.body; + if (!role && !roles) throw new BadRequestError({ message: "You must provide either role or roles field" }); + + const identityMembership = await server.services.identityProject.createProjectIdentity({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId, + projectId: req.params.projectId, + roles: roles || [{ role }] + }); + return { identityMembership }; + } + }); + + server.route({ + method: "PATCH", + url: "/:projectId/identity-memberships/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], + description: "Update project identity memberships", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().trim().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.projectId), + identityId: z.string().trim().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.identityId) + }), + body: z.object({ + roles: z + .array( + z.union([ + z.object({ + role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role), + isTemporary: z + .literal(false) + .default(false) + .describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary) + }), + z.object({ + role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role), + isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary), + temporaryMode: z + .nativeEnum(ProjectUserMembershipTemporaryMode) + .describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryMode), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryRange), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime) + }) + ]) + ) + .min(1) + .describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.description) + }), + response: { + 200: z.object({ + roles: ProjectUserMembershipRolesSchema.array() + }) + } + }, + handler: async (req) => { + const roles = await server.services.identityProject.updateProjectIdentity({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId, + projectId: req.params.projectId, + roles: req.body.roles + }); + return { roles }; + } + }); + + server.route({ + method: "DELETE", + url: "/:projectId/identity-memberships/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], + description: "Delete project identity memberships", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.projectId), + identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId) + }), + response: { + 200: z.object({ + identityMembership: IdentityProjectMembershipsSchema + }) + } + }, + handler: async (req) => { + const identityMembership = await server.services.identityProject.deleteProjectIdentity({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId, + projectId: req.params.projectId + }); + return { identityMembership }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/identity-memberships", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], + description: "Return project identity memberships", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.projectId) + }), + querystring: z.object({ + offset: z.coerce + .number() + .min(0) + .default(0) + .describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.offset) + .optional(), + limit: z.coerce + .number() + .min(1) + .max(20000) // TODO: temp limit until combobox added to add identity to project modal, reduce once added + .default(100) + .describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.limit) + .optional(), + orderBy: z + .nativeEnum(ProjectIdentityOrderBy) + .default(ProjectIdentityOrderBy.Name) + .describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderBy) + .optional(), + orderDirection: z + .nativeEnum(OrderByDirection) + .default(OrderByDirection.ASC) + .describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderDirection) + .optional(), + search: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.search).optional() + }), + response: { + 200: z.object({ + identityMemberships: z + .object({ + id: z.string(), + identityId: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ), + identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + authMethods: z.array(z.string()) + }), + project: SanitizedProjectSchema.pick({ name: true, id: true }) + }) + .array(), + totalCount: z.number() + }) + } + }, + handler: async (req) => { + const { identityMemberships, totalCount } = await server.services.identityProject.listProjectIdentities({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.projectId, + limit: req.query.limit, + offset: req.query.offset, + orderBy: req.query.orderBy, + orderDirection: req.query.orderDirection, + search: req.query.search + }); + + return { identityMemberships, totalCount }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/identity-memberships/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], + description: "Return project identity membership", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().trim().describe(PROJECT_IDENTITIES.GET_IDENTITY_MEMBERSHIP_BY_ID.projectId), + identityId: z.string().trim().describe(PROJECT_IDENTITIES.GET_IDENTITY_MEMBERSHIP_BY_ID.identityId) + }), + response: { + 200: z.object({ + identityMembership: z.object({ + id: z.string(), + identityId: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ), + identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + authMethods: z.array(z.string()) + }), + project: SanitizedProjectSchema.pick({ name: true, id: true }) + }) + }) + } + }, + handler: async (req) => { + const identityMembership = await server.services.identityProject.getProjectIdentityByIdentityId({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.projectId, + identityId: req.params.identityId + }); + return { identityMembership }; + } + }); + + server.route({ + method: "GET", + url: "/identity-memberships/:identityMembershipId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], + params: z.object({ + identityMembershipId: z.string().trim() + }), + response: { + 200: z.object({ + identityMembership: z.object({ + id: z.string(), + identityId: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ), + identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + authMethods: z.array(z.string()) + }), + project: SanitizedProjectSchema.pick({ name: true, id: true }) + }) + }) + } + }, + handler: async (req) => { + const identityMembership = await server.services.identityProject.getProjectIdentityByMembershipId({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityMembershipId: req.params.identityMembershipId + }); + return { identityMembership }; + } + }); +}; diff --git a/backend/src/server/routes/v2/project-membership-router.ts b/backend/src/server/routes/v2/deprecated-project-membership-router.ts similarity index 96% rename from backend/src/server/routes/v2/project-membership-router.ts rename to backend/src/server/routes/v2/deprecated-project-membership-router.ts index 76f1e9c5e..d88d2f996 100644 --- a/backend/src/server/routes/v2/project-membership-router.ts +++ b/backend/src/server/routes/v2/deprecated-project-membership-router.ts @@ -7,7 +7,7 @@ import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => { +export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/:projectId/memberships", @@ -71,7 +71,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider projectId: req.params.projectId, ...req.auditLogInfo, event: { - type: EventType.ADD_BATCH_WORKSPACE_MEMBER, + type: EventType.ADD_BATCH_PROJECT_MEMBER, metadata: memberships.map(({ userId, id }) => ({ userId: userId || "", membershipId: id, @@ -141,7 +141,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider ...req.auditLogInfo, projectId: req.params.projectId, event: { - type: EventType.REMOVE_WORKSPACE_MEMBER, + type: EventType.REMOVE_PROJECT_MEMBER, metadata: { userId: membership.userId, email: "" diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/deprecated-project-router.ts similarity index 92% rename from backend/src/server/routes/v2/project-router.ts rename to backend/src/server/routes/v2/deprecated-project-router.ts index 8b9091364..7c1045855 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/deprecated-project-router.ts @@ -35,7 +35,8 @@ const projectWithEnv = SanitizedProjectSchema.extend({ kmsSecretManagerKeyId: z.string().nullable().optional() }); -export const registerProjectRouter = async (server: FastifyZodProvider) => { +export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider) => { + // depreciated /* Get project key */ server.route({ method: "GET", @@ -46,7 +47,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { schema: { description: "Return encrypted project key", params: z.object({ - workspaceId: z.string().trim().describe(PROJECTS.GET_KEY.workspaceId) + workspaceId: z.string().trim().describe(PROJECTS.GET_KEY.projectId) }), response: { 200: ProjectKeysSchema.merge( @@ -72,7 +73,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { ...req.auditLogInfo, projectId: req.params.workspaceId, event: { - type: EventType.GET_WORKSPACE_KEY, + type: EventType.GET_PROJECT_KEY, metadata: { keyId: key?.id as string } @@ -83,68 +84,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); - /* Start upgrade of a project */ - server.route({ - method: "POST", - url: "/:projectId/upgrade", - config: { - rateLimit: writeLimit - }, - schema: { - params: z.object({ - projectId: z.string().trim() - }), - body: z.object({ - userPrivateKey: z.string().trim() - }), - response: { - 200: z.void() - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - await server.services.project.upgradeProject({ - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - projectId: req.params.projectId, - userPrivateKey: req.body.userPrivateKey - }); - } - }); - - /* Get upgrade status of project */ - server.route({ - url: "/:projectId/upgrade/status", - method: "GET", - config: { - rateLimit: readLimit - }, - schema: { - params: z.object({ - projectId: z.string().trim() - }), - response: { - 200: z.object({ - status: z.string().nullable() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const status = await server.services.project.getProjectUpgradeStatus({ - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId, - actor: req.permission.type, - actorId: req.permission.id - }); - - return { status }; - } - }); - /* Create new project */ server.route({ method: "POST", @@ -186,8 +125,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, - workspaceName: req.body.projectName, - workspaceDescription: req.body.projectDescription, + projectName: req.body.projectName, + projectDescription: req.body.projectDescription, slug: req.body.slug, kmsKeyId: req.body.kmsKeyId, template: req.body.template, @@ -224,6 +163,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }); /* Delete a project by slug */ + // moved to DELETE /v1/projects/slug/:slug server.route({ method: "DELETE", url: "/:slug", @@ -276,6 +216,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }); /* Get a project by slug */ + // moved to GET /v1/projects/slug/:slug server.route({ method: "GET", url: "/:slug", @@ -337,7 +278,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { 200: SanitizedProjectSchema } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const project = await server.services.project.updateProject({ diff --git a/backend/src/server/routes/v2/index.ts b/backend/src/server/routes/v2/index.ts index 93c422d15..aade29bb7 100644 --- a/backend/src/server/routes/v2/index.ts +++ b/backend/src/server/routes/v2/index.ts @@ -1,13 +1,15 @@ import { registerCaRouter } from "./certificate-authority-router"; -import { registerGroupProjectRouter } from "./group-project-router"; +import { registerDeprecatedGroupProjectRouter } from "./deprecated-group-project-router"; +import { registerDeprecatedIdentityProjectRouter } from "./deprecated-identity-project-router"; +import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router"; +import { registerDeprecatedProjectRouter } from "./deprecated-project-router"; import { registerIdentityOrgRouter } from "./identity-org-router"; -import { registerIdentityProjectRouter } from "./identity-project-router"; import { registerMfaRouter } from "./mfa-router"; import { registerOrgRouter } from "./organization-router"; import { registerPasswordRouter } from "./password-router"; import { registerPkiTemplatesRouter } from "./pki-templates-router"; -import { registerProjectMembershipRouter } from "./project-membership-router"; -import { registerProjectRouter } from "./project-router"; +import { registerSecretFolderRouter } from "./secret-folder-router"; +import { registerSecretImportRouter } from "./secret-import-router"; import { registerServiceTokenRouter } from "./service-token-router"; import { registerUserRouter } from "./user-router"; @@ -32,12 +34,17 @@ export const registerV2Routes = async (server: FastifyZodProvider) => { }, { prefix: "/organizations" } ); + + await server.register(registerSecretFolderRouter, { prefix: "/folders" }); + await server.register(registerSecretImportRouter, { prefix: "/secret-imports" }); + + // moved to v1/projects await server.register( async (projectServer) => { - await projectServer.register(registerProjectRouter); - await projectServer.register(registerIdentityProjectRouter); - await projectServer.register(registerGroupProjectRouter); - await projectServer.register(registerProjectMembershipRouter); + await projectServer.register(registerDeprecatedProjectRouter); + await projectServer.register(registerDeprecatedIdentityProjectRouter); + await projectServer.register(registerDeprecatedGroupProjectRouter); + await projectServer.register(registerDeprecatedProjectMembershipRouter); }, { prefix: "/workspace" } ); diff --git a/backend/src/server/routes/v1/secret-folder-router.ts b/backend/src/server/routes/v2/secret-folder-router.ts similarity index 80% rename from backend/src/server/routes/v1/secret-folder-router.ts rename to backend/src/server/routes/v2/secret-folder-router.ts index 871259147..0bf062452 100644 --- a/backend/src/server/routes/v1/secret-folder-router.ts +++ b/backend/src/server/routes/v2/secret-folder-router.ts @@ -28,7 +28,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => } ], body: z.object({ - workspaceId: z.string().trim().describe(FOLDERS.CREATE.workspaceId), + projectId: z.string().trim().describe(FOLDERS.CREATE.projectId), environment: z.string().trim().describe(FOLDERS.CREATE.environment), name: z .string() @@ -43,17 +43,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .default("/") .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.CREATE.path) - .optional(), - // backward compatibility with cli - directory: z - .string() - .trim() - .default("/") - .transform(prefixWithSlash) // Transformations get skipped if directory is undefined - .transform(removeTrailingSlash) - .describe(FOLDERS.CREATE.directory) - .optional(), + .describe(FOLDERS.CREATE.path), description: z.string().optional().nullable().describe(FOLDERS.CREATE.description) }), response: { @@ -66,27 +56,24 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const path = req.body.path || req.body.directory || "/"; const folder = await server.services.folder.createFolder({ actorId: req.permission.id, actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - projectId: req.body.workspaceId, - path, description: req.body.description }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.body.workspaceId, + projectId: req.body.projectId, event: { type: EventType.CREATE_FOLDER, metadata: { environment: req.body.environment, folderId: folder.id, folderName: folder.name, - folderPath: path, + folderPath: req.body.path, ...(req.body.description ? { description: req.body.description } : {}) } } @@ -115,7 +102,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => folderId: z.string().describe(FOLDERS.UPDATE.folderId) }), body: z.object({ - workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId), + projectId: z.string().trim().describe(FOLDERS.UPDATE.projectId), environment: z.string().trim().describe(FOLDERS.UPDATE.environment), name: z .string() @@ -130,17 +117,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .default("/") .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.UPDATE.path) - .optional(), - // backward compatibility with cli - directory: z - .string() - .trim() - .default("/") - .transform(prefixWithSlash) // Transformations get skipped if directory is undefined - .transform(removeTrailingSlash) - .describe(FOLDERS.UPDATE.directory) - .optional(), + .describe(FOLDERS.UPDATE.path), description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description) }), response: { @@ -153,26 +130,23 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const path = req.body.path || req.body.directory || "/"; const { folder, old } = await server.services.folder.updateFolder({ actorId: req.permission.id, actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - projectId: req.body.workspaceId, - id: req.params.folderId, - path + id: req.params.folderId }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.body.workspaceId, + projectId: req.body.projectId, event: { type: EventType.UPDATE_FOLDER, metadata: { environment: req.body.environment, folderId: folder.id, - folderPath: path, + folderPath: req.body.path, newFolderName: folder.name, oldFolderName: old.name } @@ -198,7 +172,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => } ], body: z.object({ - projectSlug: z.string().trim().describe(FOLDERS.UPDATE.projectSlug), + projectId: z.string().trim().describe(FOLDERS.UPDATE.projectId), folders: z .object({ id: z.string().describe(FOLDERS.UPDATE.folderId), @@ -281,7 +255,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => folderIdOrName: z.string().describe(FOLDERS.DELETE.folderIdOrName) }), body: z.object({ - workspaceId: z.string().trim().describe(FOLDERS.DELETE.workspaceId), + projectId: z.string().trim().describe(FOLDERS.DELETE.projectId), environment: z.string().trim().describe(FOLDERS.DELETE.environment), path: z .string() @@ -290,16 +264,6 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) .describe(FOLDERS.DELETE.path) - .optional(), - // keep this here as cli need directory - directory: z - .string() - .trim() - .default("/") - .transform(prefixWithSlash) // Transformations get skipped if directory is undefined - .transform(removeTrailingSlash) - .describe(FOLDERS.DELETE.directory) - .optional() }), response: { 200: z.object({ @@ -309,26 +273,23 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const path = req.body.path || req.body.directory || "/"; const folder = await server.services.folder.deleteFolder({ actorId: req.permission.id, actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - projectId: req.body.workspaceId, - idOrName: req.params.folderIdOrName, - path + idOrName: req.params.folderIdOrName }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.body.workspaceId, + projectId: req.body.projectId, event: { type: EventType.DELETE_FOLDER, metadata: { environment: req.body.environment, folderId: folder.id, - folderPath: path, + folderPath: req.body.path, folderName: folder.name } } @@ -353,7 +314,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => } ], querystring: z.object({ - workspaceId: z.string().trim().describe(FOLDERS.LIST.workspaceId), + projectId: z.string().trim().describe(FOLDERS.LIST.projectId), environment: z.string().trim().describe(FOLDERS.LIST.environment), lastSecretModified: z.string().datetime().trim().optional().describe(FOLDERS.LIST.lastSecretModified), path: z @@ -361,16 +322,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .trim() .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.LIST.path) - .optional(), - // backward compatibility with cli - directory: z - .string() - .trim() - .transform(prefixWithSlash) // Transformations get skipped if directory is undefined - .transform(removeTrailingSlash) - .describe(FOLDERS.LIST.directory) - .optional(), + .describe(FOLDERS.LIST.path), recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive) }), response: { @@ -383,15 +335,12 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const path = req.query.path || req.query.directory || "/"; const folders = await server.services.folder.getFolders({ actorId: req.permission.id, actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - ...req.query, - projectId: req.query.workspaceId, - path + ...req.query }); return { folders }; } diff --git a/backend/src/server/routes/v1/secret-import-router.ts b/backend/src/server/routes/v2/secret-import-router.ts similarity index 85% rename from backend/src/server/routes/v1/secret-import-router.ts rename to backend/src/server/routes/v2/secret-import-router.ts index fca11f8a0..d9802c4b3 100644 --- a/backend/src/server/routes/v1/secret-import-router.ts +++ b/backend/src/server/routes/v2/secret-import-router.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { SecretImportsSchema, SecretsSchema } from "@app/db/schemas"; +import { SecretImportsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, SECRET_IMPORTS } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; @@ -27,7 +27,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => } ], body: z.object({ - workspaceId: z.string().trim().describe(SECRET_IMPORTS.CREATE.workspaceId), + projectId: z.string().trim().describe(SECRET_IMPORTS.CREATE.projectId), environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.environment), path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.path), import: z.object({ @@ -55,13 +55,13 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - projectId: req.body.workspaceId, + projectId: req.body.projectId, data: req.body.import }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.body.workspaceId, + projectId: req.body.projectId, event: { type: EventType.CREATE_SECRET_IMPORT, metadata: { @@ -97,7 +97,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId) }), body: z.object({ - workspaceId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.workspaceId), + projectId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.projectId), environment: z.string().trim().describe(SECRET_IMPORTS.UPDATE.environment), path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.UPDATE.path), import: z.object({ @@ -131,13 +131,13 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => actorOrgId: req.permission.orgId, id: req.params.secretImportId, ...req.body, - projectId: req.body.workspaceId, + projectId: req.body.projectId, data: req.body.import }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.body.workspaceId, + projectId: req.body.projectId, event: { type: EventType.UPDATE_SECRET_IMPORT, metadata: { @@ -173,7 +173,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => secretImportId: z.string().trim().describe(SECRET_IMPORTS.DELETE.secretImportId) }), body: z.object({ - workspaceId: z.string().trim().describe(SECRET_IMPORTS.DELETE.workspaceId), + projectId: z.string().trim().describe(SECRET_IMPORTS.DELETE.projectId), environment: z.string().trim().describe(SECRET_IMPORTS.DELETE.environment), path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.DELETE.path) }), @@ -197,12 +197,12 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => actorOrgId: req.permission.orgId, id: req.params.secretImportId, ...req.body, - projectId: req.body.workspaceId + projectId: req.body.projectId }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.body.workspaceId, + projectId: req.body.projectId, event: { type: EventType.DELETE_SECRET_IMPORT, metadata: { @@ -236,7 +236,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId) }), body: z.object({ - workspaceId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.workspaceId), + projectId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.projectId), environment: z.string().trim().describe(SECRET_IMPORTS.UPDATE.environment), path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.UPDATE.path) }), @@ -255,7 +255,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => actorOrgId: req.permission.orgId, id: req.params.secretImportId, ...req.body, - projectId: req.body.workspaceId + projectId: req.body.projectId }); return { message }; @@ -278,7 +278,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => } ], querystring: z.object({ - workspaceId: z.string().trim().describe(SECRET_IMPORTS.LIST.workspaceId), + projectId: z.string().trim().describe(SECRET_IMPORTS.LIST.projectId), environment: z.string().trim().describe(SECRET_IMPORTS.LIST.environment), path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.LIST.path) }), @@ -301,12 +301,12 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.query, - projectId: req.query.workspaceId + projectId: req.query.projectId }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: req.query.workspaceId, + projectId: req.query.projectId, event: { type: EventType.GET_SECRET_IMPORTS, metadata: { @@ -386,55 +386,11 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => config: { rateLimit: secretsLimit }, - schema: { - querystring: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), - path: z.string().trim().default("/").transform(removeTrailingSlash) - }), - response: { - 200: z.object({ - secrets: z - .object({ - secretPath: z.string(), - environment: z.string(), - environmentInfo: z.object({ - id: z.string(), - name: z.string(), - slug: z.string() - }), - folderId: z.string().optional(), - secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() - }) - .array() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const importedSecrets = await server.services.secretImport.getSecretsFromImports({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.query, - projectId: req.query.workspaceId - }); - return { secrets: importedSecrets }; - } - }); - - server.route({ - url: "/secrets/raw", - method: "GET", - config: { - rateLimit: secretsLimit - }, schema: { hide: false, tags: [ApiDocsTags.SecretImports], querystring: z.object({ - workspaceId: z.string().trim(), + projectId: z.string().trim(), environment: z.string().trim(), path: z.string().trim().default("/").transform(removeTrailingSlash) }), @@ -463,8 +419,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - ...req.query, - projectId: req.query.workspaceId + ...req.query }); return { secrets: importedSecrets }; } diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts index f416fe8bb..397967fa2 100644 --- a/backend/src/server/routes/v2/user-router.ts +++ b/backend/src/server/routes/v2/user-router.ts @@ -129,6 +129,63 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "POST", + url: "/me/email-change/otp", + config: { + rateLimit: smtpRateLimit({ + keyGenerator: (req) => req.permission.id + }) + }, + schema: { + body: z.object({ + newEmail: z.string().email().trim() + }), + response: { + 200: z.object({ + success: z.boolean(), + message: z.string() + }) + } + }, + preHandler: verifyAuth([AuthMode.JWT], { requireOrg: false }), + handler: async (req) => { + const result = await server.services.user.requestEmailChangeOTP({ + userId: req.permission.id, + newEmail: req.body.newEmail + }); + return result; + } + }); + + server.route({ + method: "PATCH", + url: "/me/email", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + newEmail: z.string().email().trim(), + otpCode: z.string().trim().length(6) + }), + response: { + 200: z.object({ + user: UsersSchema + }) + } + }, + preHandler: verifyAuth([AuthMode.JWT], { requireOrg: false }), + handler: async (req) => { + const user = await server.services.user.updateUserEmail({ + userId: req.permission.id, + newEmail: req.body.newEmail, + otpCode: req.body.otpCode + }); + return { user }; + } + }); + server.route({ method: "GET", url: "/me/organizations", diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/deprecated-secret-router.ts similarity index 96% rename from backend/src/server/routes/v3/secret-router.ts rename to backend/src/server/routes/v3/deprecated-secret-router.ts index ce0d4f188..c037a3dee 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/deprecated-secret-router.ts @@ -39,7 +39,7 @@ const SecretReferenceNodeTree: z.ZodType = SecretReference children: z.lazy(() => SecretReferenceNodeTree.array()) }); -export const registerSecretRouter = async (server: FastifyZodProvider) => { +export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/tags/:secretName", @@ -230,7 +230,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }) .describe(RAW_SECRETS.LIST.metadataFilter), - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.LIST.projectId), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), @@ -298,7 +298,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspaceId = projectId; } - if (!workspaceId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" }); + if (!workspaceId || !environment) throw new BadRequestError({ message: "Missing project id or environment" }); const { secrets, imports } = await server.services.secret.getSecretsRaw({ actorId: req.permission.id, @@ -336,7 +336,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId, + projectId: workspaceId, environment, secretPath: req.query.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -405,7 +405,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretName: z.string().trim().describe(RAW_SECRETS.GET.secretName) }), querystring: z.object({ - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.GET.projectId), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.GET.environment), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), @@ -495,7 +495,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: 1, - workspaceId: secret.workspace, + projectId: secret.workspace, environment, secretPath: req.query.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -526,7 +526,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName) }), body: z.object({ - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.CREATE.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.CREATE.projectId), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.CREATE.projectSlug), environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), secretPath: z @@ -638,7 +638,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: projectId, + projectId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -669,7 +669,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName) }), body: z.object({ - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectId), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectSlug), environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), secretValue: z @@ -791,7 +791,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: projectId, + projectId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -821,7 +821,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName) }), body: z.object({ - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectId), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug), environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), secretPath: z @@ -909,7 +909,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: projectId, + projectId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1017,7 +1017,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: req.query.workspaceId, + projectId: req.query.workspaceId, environment: req.query.environment, secretPath: req.query.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1097,7 +1097,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: req.query.workspaceId, + projectId: req.query.workspaceId, environment: req.query.environment, secretPath: req.query.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1272,7 +1272,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: req.body.workspaceId, + projectId: req.body.workspaceId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1466,7 +1466,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: req.body.workspaceId, + projectId: req.body.workspaceId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1594,7 +1594,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: req.body.workspaceId, + projectId: req.body.workspaceId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1781,7 +1781,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: req.body.workspaceId, + projectId: req.body.workspaceId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1914,7 +1914,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: req.body.workspaceId, + projectId: req.body.workspaceId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -2039,7 +2039,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: req.body.workspaceId, + projectId: req.body.workspaceId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -2067,7 +2067,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { ], body: z.object({ projectSlug: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectSlug), - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectId), environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), secretPath: z .string() @@ -2166,7 +2166,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: secrets[0].workspace, + projectId: secrets[0].workspace, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -2194,7 +2194,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { ], body: z.object({ projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug), - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectId), environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), secretPath: z .string() @@ -2341,7 +2341,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: secrets[0].workspace, + projectId: secrets[0].workspace, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -2369,7 +2369,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { ], body: z.object({ projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug), - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectId), environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), secretPath: z .string() @@ -2459,7 +2459,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: secrets[0].workspace, + projectId: secrets[0].workspace, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -2469,95 +2469,4 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { return { secrets }; } }); - - server.route({ - method: "GET", - url: "/raw/:secretName/secret-reference-tree", - config: { - rateLimit: secretsLimit - }, - schema: { - hide: false, - tags: [ApiDocsTags.Secrets], - description: "Get secret reference tree", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - secretName: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.secretName) - }), - querystring: z.object({ - workspaceId: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.workspaceId), - environment: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.environment), - secretPath: z - .string() - .trim() - .default("/") - .transform(removeTrailingSlash) - .describe(RAW_SECRETS.GET_REFERENCE_TREE.secretPath) - }), - response: { - 200: z.object({ - tree: SecretReferenceNodeTree, - value: z.string().optional() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const { secretName } = req.params; - const { secretPath, environment, workspaceId } = req.query; - const { tree, value } = await server.services.secret.getSecretReferenceTree({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: workspaceId, - secretName, - secretPath, - environment - }); - - return { tree, value }; - } - }); - - server.route({ - method: "POST", - url: "/backfill-secret-references", - config: { - rateLimit: secretsLimit - }, - schema: { - description: "Backfill secret references", - security: [ - { - bearerAuth: [] - } - ], - body: z.object({ - projectId: z.string().trim().min(1) - }), - response: { - 200: z.object({ - message: z.string() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const { projectId } = req.body; - const message = await server.services.secret.backfillSecretReferences({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId - }); - - return message; - } - }); }; diff --git a/backend/src/server/routes/v3/external-migration-router.ts b/backend/src/server/routes/v3/external-migration-router.ts index 259a97ddb..a3b744485 100644 --- a/backend/src/server/routes/v3/external-migration-router.ts +++ b/backend/src/server/routes/v3/external-migration-router.ts @@ -2,10 +2,13 @@ import fastifyMultipart from "@fastify/multipart"; import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; -import { writeLimit } from "@app/server/config/rateLimiter"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { VaultMappingType } from "@app/services/external-migration/external-migration-types"; +import { + ExternalMigrationProviders, + VaultMappingType +} from "@app/services/external-migration/external-migration-types"; const MB25_IN_BYTES = 26214400; @@ -81,4 +84,33 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider }); } }); + + server.route({ + method: "GET", + url: "/custom-migration-enabled/:provider", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + provider: z.nativeEnum(ExternalMigrationProviders) + }), + response: { + 200: z.object({ + enabled: z.boolean() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const enabled = await server.services.migration.hasCustomVaultMigration({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + provider: req.params.provider + }); + return { enabled }; + } + }); }; diff --git a/backend/src/server/routes/v3/index.ts b/backend/src/server/routes/v3/index.ts index 1c31741a1..d7fa94d6b 100644 --- a/backend/src/server/routes/v3/index.ts +++ b/backend/src/server/routes/v3/index.ts @@ -1,7 +1,6 @@ +import { registerDeprecatedSecretRouter } from "./deprecated-secret-router"; import { registerExternalMigrationRouter } from "./external-migration-router"; import { registerLoginRouter } from "./login-router"; -import { registerSecretBlindIndexRouter } from "./secret-blind-index-router"; -import { registerSecretRouter } from "./secret-router"; import { registerSignupRouter } from "./signup-router"; import { registerUserRouter } from "./user-router"; @@ -9,7 +8,6 @@ export const registerV3Routes = async (server: FastifyZodProvider) => { await server.register(registerSignupRouter, { prefix: "/signup" }); await server.register(registerLoginRouter, { prefix: "/auth" }); await server.register(registerUserRouter, { prefix: "/users" }); - await server.register(registerSecretRouter, { prefix: "/secrets" }); - await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" }); + await server.register(registerDeprecatedSecretRouter, { prefix: "/secrets" }); await server.register(registerExternalMigrationRouter, { prefix: "/external-migration" }); }; diff --git a/backend/src/server/routes/v3/secret-blind-index-router.ts b/backend/src/server/routes/v3/secret-blind-index-router.ts deleted file mode 100644 index cfb27a58e..000000000 --- a/backend/src/server/routes/v3/secret-blind-index-router.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { z } from "zod"; - -import { SecretsSchema } from "@app/db/schemas"; -import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; -import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode } from "@app/services/auth/auth-type"; - -export const registerSecretBlindIndexRouter = async (server: FastifyZodProvider) => { - server.route({ - method: "GET", - url: "/:projectId/secrets/blind-index-status", - config: { - rateLimit: readLimit - }, - schema: { - params: z.object({ - projectId: z.string().trim() - }), - response: { - 200: z.boolean() - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const count = await server.services.secretBlindIndex.getSecretBlindIndexStatus({ - projectId: req.params.projectId, - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId - }); - return count === 0; - } - }); - - server.route({ - method: "GET", - url: "/:projectId/secrets", - config: { - rateLimit: readLimit - }, - schema: { - params: z.object({ - projectId: z.string().trim() - }), - response: { - 200: z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }) - .merge( - z.object({ - environment: z.string(), - workspace: z.string() - }) - ) - .array() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const secrets = await server.services.secretBlindIndex.getProjectSecrets({ - projectId: req.params.projectId, - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId - }); - return { secrets }; - } - }); - - server.route({ - method: "POST", - url: "/:projectId/secrets/names", - config: { - rateLimit: writeLimit - }, - schema: { - params: z.object({ - projectId: z.string().trim() - }), - body: z.object({ - secretsToUpdate: z - .object({ - secretName: z.string().trim(), - secretId: z.string().trim() - }) - .array() - }), - response: { - 200: z.object({ - message: z.string() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - await server.services.secretBlindIndex.updateProjectSecretName({ - projectId: req.params.projectId, - secretsToUpdate: req.body.secretsToUpdate, - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId - }); - return { message: "Successfully named workspace secrets" }; - } - }); -}; diff --git a/backend/src/server/routes/v4/index.ts b/backend/src/server/routes/v4/index.ts new file mode 100644 index 000000000..db2e69222 --- /dev/null +++ b/backend/src/server/routes/v4/index.ts @@ -0,0 +1,5 @@ +import { registerSecretRouter } from "./secret-router"; + +export const registerV4Routes = async (server: FastifyZodProvider) => { + await server.register(registerSecretRouter, { prefix: "/secrets" }); +}; diff --git a/backend/src/server/routes/v4/secret-router.ts b/backend/src/server/routes/v4/secret-router.ts new file mode 100644 index 000000000..ead96668c --- /dev/null +++ b/backend/src/server/routes/v4/secret-router.ts @@ -0,0 +1,1316 @@ +import picomatch from "picomatch"; +import { z } from "zod"; + +import { SecretApprovalRequestsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas"; +import { EventType, SecretApprovalEvent, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, RAW_SECRETS } from "@app/lib/api-docs"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { secretsLimit } from "@app/server/config/rateLimiter"; +import { BaseSecretNameSchema, SecretNameSchema } from "@app/server/lib/schemas"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; +import { getUserAgentType } from "@app/server/plugins/audit-log"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; +import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; +import { SecretProtectionType } from "@app/services/secret/secret-types"; +import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; + +import { SanitizedTagSchema, secretRawSchema } from "../sanitizedSchemas"; + +const SecretReferenceNode = z.object({ + key: z.string(), + value: z.string().optional(), + environment: z.string(), + secretPath: z.string() +}); + +const convertStringBoolean = (defaultValue: boolean = false) => { + return z + .enum(["true", "false"]) + .default(defaultValue ? "true" : "false") + .transform((value) => value === "true"); +}; + +type TSecretReferenceNode = z.infer & { children: TSecretReferenceNode[] }; + +const SecretReferenceNodeTree: z.ZodType = SecretReferenceNode.extend({ + children: z.lazy(() => SecretReferenceNodeTree.array()) +}); + +export const registerSecretRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "List secrets", + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + metadataFilter: z + .string() + .optional() + .transform((val) => { + if (!val) return undefined; + + const result: { key?: string; value?: string }[] = []; + const pairs = val.split("|"); + + for (const pair of pairs) { + const keyValuePair: { key?: string; value?: string } = {}; + const parts = pair.split(/[,=]/); + + for (let i = 0; i < parts.length; i += 2) { + const identifier = parts[i].trim().toLowerCase(); + const value = parts[i + 1]?.trim(); + + if (identifier === "key" && value) { + keyValuePair.key = value; + } else if (identifier === "value" && value) { + keyValuePair.value = value; + } + } + + if (keyValuePair.key && keyValuePair.value) { + result.push(keyValuePair); + } + } + + return result.length ? result : undefined; + }) + .superRefine((metadata, ctx) => { + if (metadata && !Array.isArray(metadata)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Invalid secretMetadata format. Correct format is key=value1,value=value2|key=value3,value=value4." + }); + } + + if (metadata) { + if (metadata.length > 10) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "You can only filter by up to 10 metadata fields" + }); + } + + for (const item of metadata) { + if (!item.key && !item.value) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Invalid secretMetadata format, key or value must be provided. Correct format is key=value1,value=value2|key=value3,value=value4." + }); + } + } + } + }) + .describe(RAW_SECRETS.LIST.metadataFilter), + projectId: z.string().trim().optional().describe(RAW_SECRETS.LIST.projectId), + environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), + secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), + viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.LIST.viewSecretValue), + expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.LIST.expand), + recursive: convertStringBoolean().describe(RAW_SECRETS.LIST.recursive), + include_imports: convertStringBoolean().describe(RAW_SECRETS.LIST.includeImports), + tagSlugs: z + .string() + .describe(RAW_SECRETS.LIST.tagSlugs) + .optional() + // split by comma and trim the strings + .transform((el) => (el ? el.split(",").map((i) => i.trim()) : [])) + }), + response: { + 200: z.object({ + secrets: secretRawSchema + .extend({ + secretPath: z.string().optional(), + secretValueHidden: z.boolean(), + secretMetadata: ResourceMetadataSchema.optional(), + tags: SanitizedTagSchema.array().optional() + }) + .array(), + imports: z + .object({ + secretPath: z.string(), + environment: z.string(), + folderId: z.string().optional(), + secrets: secretRawSchema + .omit({ createdAt: true, updatedAt: true }) + .extend({ + secretValueHidden: z.boolean(), + secretMetadata: ResourceMetadataSchema.optional() + }) + .array() + }) + .array() + .optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + // just for delivery hero usecase + let { secretPath, environment, projectId } = req.query; + if (req.auth.actor === ActorType.SERVICE) { + const scope = ServiceTokenScopes.parse(req.auth.serviceToken.scopes); + const isSingleScope = scope.length === 1; + if (isSingleScope && !picomatch.scan(scope[0].secretPath).isGlob) { + secretPath = scope[0].secretPath; + environment = scope[0].environment; + projectId = req.auth.serviceToken.projectId; + } + } + + if (!projectId || !environment) throw new BadRequestError({ message: "Missing project id or environment" }); + + const { secrets, imports } = await server.services.secret.getSecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + environment, + expandSecretReferences: req.query.expandSecretReferences, + actorAuthMethod: req.permission.authMethod, + projectId, + viewSecretValue: req.query.viewSecretValue, + path: secretPath, + metadataFilter: req.query.metadataFilter, + includeImports: req.query.include_imports, + recursive: req.query.recursive, + tagSlugs: req.query.tagSlugs + }); + + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.GET_SECRETS, + metadata: { + environment, + secretPath: req.query.secretPath, + numberOfSecrets: secrets.length + } + } + }); + + if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) { + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretPulled, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: secrets.length, + projectId, + environment, + secretPath: req.query.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + } + + return { secrets, imports }; + } + }); + + server.route({ + method: "GET", + url: "/id/:secretId", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + params: z.object({ + secretId: z.string() + }), + response: { + 200: z.object({ + secret: secretRawSchema.extend({ + secretPath: z.string(), + tags: SanitizedTagSchema.array().optional(), + secretMetadata: ResourceMetadataSchema.optional() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { secretId } = req.params; + const secret = await server.services.secret.getSecretByIdRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secretId + }); + + return { secret }; + } + }); + + server.route({ + method: "GET", + url: "/:secretName", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Get a secret by name", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretName: z.string().trim().describe(RAW_SECRETS.GET.secretName) + }), + querystring: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.GET.projectId), + environment: z.string().trim().optional().describe(RAW_SECRETS.GET.environment), + secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), + version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version), + type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type), + viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.GET.viewSecretValue), + expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.GET.expand), + include_imports: convertStringBoolean().describe(RAW_SECRETS.GET.includeImports) + }), + response: { + 200: z.object({ + secret: secretRawSchema.extend({ + secretValueHidden: z.boolean(), + secretPath: z.string(), + tags: SanitizedTagSchema.array().optional(), + secretMetadata: ResourceMetadataSchema.optional() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + let { secretPath, environment, projectId } = req.query; + if (req.auth.actor === ActorType.SERVICE) { + const scope = ServiceTokenScopes.parse(req.auth.serviceToken.scopes); + const isSingleScope = scope.length === 1; + if (isSingleScope && !picomatch.scan(scope[0].secretPath).isGlob) { + secretPath = scope[0].secretPath; + environment = scope[0].environment; + projectId = req.auth.serviceToken.projectId; + } + } + + if (!environment) throw new BadRequestError({ message: "Missing environment" }); + if (!projectId) { + throw new BadRequestError({ message: "You must provide workspaceId" }); + } + + const secret = await server.services.secret.getSecretByNameRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + expandSecretReferences: req.query.expandSecretReferences, + environment, + projectId, + viewSecretValue: req.query.viewSecretValue, + path: secretPath, + secretName: req.params.secretName, + type: req.query.type, + includeImports: req.query.include_imports, + version: req.query.version + }); + + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.GET_SECRET, + metadata: { + environment, + secretPath: req.query.secretPath, + secretId: secret.id, + secretKey: req.params.secretName, + secretVersion: secret.version, + secretMetadata: secret.secretMetadata + } + } + }); + + if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) { + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretPulled, + organizationId: req.permission.orgId, + distinctId: getTelemetryDistinctId(req), + properties: { + numberOfSecrets: 1, + projectId, + environment, + secretPath: req.query.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + } + return { secret }; + } + }); + + server.route({ + method: "POST", + url: "/:secretName", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Create secret", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName) + }), + body: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.CREATE.projectId), + environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.CREATE.secretPath), + secretValue: z + .string() + .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .describe(RAW_SECRETS.CREATE.secretValue), + secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment), + secretMetadata: ResourceMetadataSchema.optional(), + tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds), + skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding), + type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type), + secretReminderRepeatDays: z + .number() + .optional() + .nullable() + .describe(RAW_SECRETS.CREATE.secretReminderRepeatDays), + secretReminderNote: z + .string() + .max(1024, "Secret reminder note cannot exceed 1024 characters") + .optional() + .nullable() + .describe(RAW_SECRETS.CREATE.secretReminderNote) + }), + response: { + 200: z.union([ + z.object({ + secret: secretRawSchema + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretOperation = await server.services.secret.createSecretRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + environment: req.body.environment, + actorAuthMethod: req.permission.authMethod, + projectId: req.body.projectId, + secretPath: req.body.secretPath, + secretName: req.params.secretName, + type: req.body.type, + secretValue: req.body.secretValue, + skipMultilineEncoding: req.body.skipMultilineEncoding, + secretComment: req.body.secretComment, + secretMetadata: req.body.secretMetadata, + tagIds: req.body.tagIds, + secretReminderNote: req.body.secretReminderNote, + secretReminderRepeatDays: req.body.secretReminderRepeatDays + }); + if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath: req.body.secretPath, + environment: req.body.environment, + secretKey: req.params.secretName, + eventType: SecretApprovalEvent.Create + } + } + }); + + return { approval: secretOperation.approval }; + } + + const { secret } = secretOperation; + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.CREATE_SECRET, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secretId: secret.id, + secretKey: req.params.secretName, + secretVersion: secret.version, + secretMetadata: req.body.secretMetadata + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretCreated, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: 1, + projectId: req.body.projectId, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + + return { secret }; + } + }); + + server.route({ + method: "PATCH", + url: "/:secretName", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Update secret", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName) + }), + body: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.UPDATE.projectId), + environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), + secretValue: z + .string() + .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .optional() + .describe(RAW_SECRETS.UPDATE.secretValue), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.UPDATE.secretPath), + skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding), + type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type), + tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds), + metadata: z.record(z.string()).optional(), + secretMetadata: ResourceMetadataSchema.optional(), + secretReminderNote: z + .string() + .max(1024, "Secret reminder note cannot exceed 1024 characters") + .optional() + .nullable() + .describe(RAW_SECRETS.UPDATE.secretReminderNote), + secretReminderRepeatDays: z + .number() + .optional() + .nullable() + .describe(RAW_SECRETS.UPDATE.secretReminderRepeatDays), + secretReminderRecipients: z.string().array().optional().describe(RAW_SECRETS.UPDATE.secretReminderRecipients), + newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName), + secretComment: z.string().optional().describe(RAW_SECRETS.UPDATE.secretComment) + }), + response: { + 200: z.union([ + z.object({ + secret: secretRawSchema.extend({ + secretValueHidden: z.boolean() + }) + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretOperation = await server.services.secret.updateSecretRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + environment: req.body.environment, + projectId: req.body.projectId, + secretPath: req.body.secretPath, + secretName: req.params.secretName, + type: req.body.type, + secretValue: req.body.secretValue, + skipMultilineEncoding: req.body.skipMultilineEncoding, + tagIds: req.body.tagIds, + secretReminderRepeatDays: req.body.secretReminderRepeatDays, + secretReminderRecipients: req.body.secretReminderRecipients, + secretReminderNote: req.body.secretReminderNote, + metadata: req.body.metadata, + newSecretName: req.body.newSecretName, + secretComment: req.body.secretComment, + secretMetadata: req.body.secretMetadata + }); + + if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath: req.body.secretPath, + environment: req.body.environment, + secretKey: req.params.secretName, + eventType: SecretApprovalEvent.Update + } + } + }); + + return { approval: secretOperation.approval }; + } + const { secret } = secretOperation; + + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.UPDATE_SECRET, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secretId: secret.id, + secretKey: req.params.secretName, + secretVersion: secret.version, + secretMetadata: req.body.secretMetadata + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretUpdated, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: 1, + projectId: req.body.projectId, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + return { secret }; + } + }); + + server.route({ + method: "DELETE", + url: "/:secretName", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Delete secret", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName) + }), + body: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.DELETE.projectId), + environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.DELETE.secretPath), + type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.DELETE.type) + }), + response: { + 200: z.union([ + z.object({ + secret: secretRawSchema.extend({ + secretValueHidden: z.boolean() + }) + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretOperation = await server.services.secret.deleteSecretRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + environment: req.body.environment, + projectId: req.body.projectId, + secretPath: req.body.secretPath, + secretName: req.params.secretName, + type: req.body.type + }); + if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath: req.body.secretPath, + environment: req.body.environment, + secretKey: req.params.secretName, + eventType: SecretApprovalEvent.Delete + } + } + }); + + return { approval: secretOperation.approval }; + } + + const { secret } = secretOperation; + + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.DELETE_SECRET, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secretId: secret.id, + secretKey: req.params.secretName, + secretVersion: secret.version + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretDeleted, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: 1, + projectId: req.body.projectId, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + + return { secret }; + } + }); + + server.route({ + method: "POST", + url: "/move", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + body: z.object({ + projectSlug: z.string().trim(), + sourceEnvironment: z.string().trim(), + sourceSecretPath: z.string().trim().default("/").transform(removeTrailingSlash), + destinationEnvironment: z.string().trim(), + destinationSecretPath: z.string().trim().default("/").transform(removeTrailingSlash), + secretIds: z.string().array(), + shouldOverwrite: z.boolean().default(false) + }), + response: { + 200: z.object({ + isSourceUpdated: z.boolean(), + isDestinationUpdated: z.boolean() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { projectId, isSourceUpdated, isDestinationUpdated } = await server.services.secret.moveSecrets({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.MOVE_SECRETS, + metadata: { + sourceEnvironment: req.body.sourceEnvironment, + sourceSecretPath: req.body.sourceSecretPath, + destinationEnvironment: req.body.destinationEnvironment, + destinationSecretPath: req.body.destinationSecretPath, + secretIds: req.body.secretIds + } + } + }); + + return { + isSourceUpdated, + isDestinationUpdated + }; + } + }); + + server.route({ + method: "POST", + url: "/batch", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Create many secrets", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.UPDATE.projectId), + environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.CREATE.secretPath), + secrets: z + .object({ + secretKey: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName), + secretValue: z + .string() + .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .describe(RAW_SECRETS.CREATE.secretValue), + secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment), + skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding), + metadata: z.record(z.string()).optional(), + secretMetadata: ResourceMetadataSchema.optional(), + tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds) + }) + .array() + .min(1) + }), + response: { + 200: z.union([ + z.object({ + secrets: secretRawSchema.array() + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { environment, secretPath, secrets: inputSecrets } = req.body; + + const secretOperation = await server.services.secret.createManySecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secretPath, + environment, + projectId: req.body.projectId, + secrets: inputSecrets + }); + if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath, + environment, + secrets: inputSecrets.map((secret) => ({ + secretKey: secret.secretKey + })), + eventType: SecretApprovalEvent.CreateMany + } + } + }); + return { approval: secretOperation.approval }; + } + const { secrets } = secretOperation; + + const secretMetadataMap = new Map( + inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata]) + ); + + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.CREATE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: secrets.map((secret) => ({ + secretId: secret.id, + secretKey: secret.secretKey, + secretVersion: secret.version, + secretMetadata: secretMetadataMap.get(secret.secretKey) + })) + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretCreated, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: secrets.length, + projectId: req.body.projectId, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + return { secrets }; + } + }); + + server.route({ + method: "PATCH", + url: "/batch", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Update many secrets", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.DELETE.projectId), + environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.UPDATE.secretPath), + mode: z + .nativeEnum(SecretUpdateMode) + .optional() + .default(SecretUpdateMode.FailOnNotFound) + .describe(RAW_SECRETS.UPDATE.mode), + secrets: z + .object({ + secretKey: SecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName), + secretValue: z + .string() + .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .optional() + .describe(RAW_SECRETS.UPDATE.secretValue), + secretPath: z + .string() + .trim() + .transform(removeTrailingSlash) + .optional() + .describe(RAW_SECRETS.UPDATE.secretPath), + secretComment: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.secretComment), + skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding), + newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName), + tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds), + secretReminderNote: z + .string() + .max(1024, "Secret reminder note cannot exceed 1024 characters") + .optional() + .nullable() + .describe(RAW_SECRETS.UPDATE.secretReminderNote), + secretMetadata: ResourceMetadataSchema.optional(), + secretReminderRepeatDays: z + .number() + .optional() + .nullable() + .describe(RAW_SECRETS.UPDATE.secretReminderRepeatDays) + }) + .array() + .min(1) + }), + response: { + 200: z.union([ + z.object({ + secrets: secretRawSchema.extend({ secretValueHidden: z.boolean() }).array() + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { environment, secretPath, secrets: inputSecrets } = req.body; + const secretOperation = await server.services.secret.updateManySecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secretPath, + environment, + projectId: req.body.projectId, + secrets: inputSecrets, + mode: req.body.mode + }); + if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath, + environment, + secrets: inputSecrets.map((secret) => ({ + secretKey: secret.secretKey, + secretPath: secret.secretPath + })), + eventType: SecretApprovalEvent.UpdateMany + } + } + }); + return { approval: secretOperation.approval }; + } + const { secrets } = secretOperation; + + const secretMetadataMap = new Map( + inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata]) + ); + + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.UPDATE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: secrets + .filter((el) => el.version > 1) + .map((secret) => ({ + secretId: secret.id, + secretPath: secret.secretPath, + secretKey: secret.secretKey, + secretVersion: secret.version, + secretMetadata: secretMetadataMap.get(secret.secretKey) + })) + } + } + }); + const createdSecrets = secrets.filter((el) => el.version === 1); + if (createdSecrets.length) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.CREATE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: createdSecrets.map((secret) => ({ + secretId: secret.id, + secretPath: secret.secretPath, + secretKey: secret.secretKey, + secretVersion: secret.version, + secretMetadata: secretMetadataMap.get(secret.secretKey) + })) + } + } + }); + } + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretUpdated, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: secrets.length, + projectId: req.body.projectId, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + return { secrets }; + } + }); + + server.route({ + method: "DELETE", + url: "/batch", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Delete many secrets", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.DELETE.projectId), + environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.DELETE.secretPath), + secrets: z + .object({ + secretKey: z.string().describe(RAW_SECRETS.DELETE.secretName), + type: z.nativeEnum(SecretType).default(SecretType.Shared) + }) + .array() + .min(1) + }), + response: { + 200: z.union([ + z.object({ + secrets: secretRawSchema + .extend({ + secretValueHidden: z.boolean() + }) + .array() + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { environment, secretPath, secrets: inputSecrets } = req.body; + const secretOperation = await server.services.secret.deleteManySecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + environment, + secretPath, + projectId: req.body.projectId, + secrets: inputSecrets + }); + if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath, + environment, + secrets: inputSecrets.map((secret) => ({ + secretKey: secret.secretKey + })), + eventType: SecretApprovalEvent.DeleteMany + } + } + }); + + return { approval: secretOperation.approval }; + } + const { secrets } = secretOperation; + + await server.services.auditLog.createAuditLog({ + projectId: secrets[0].workspace, + ...req.auditLogInfo, + event: { + type: EventType.DELETE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: secrets.map((secret) => ({ + secretId: secret.id, + secretKey: secret.secretKey, + secretVersion: secret.version + })) + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretDeleted, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: secrets.length, + projectId: secrets[0].workspace, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + return { secrets }; + } + }); + + server.route({ + method: "GET", + url: "/:secretName/secret-reference-tree", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Get secret reference tree", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretName: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.secretName) + }), + querystring: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.projectId), + environment: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.GET_REFERENCE_TREE.secretPath) + }), + response: { + 200: z.object({ + tree: SecretReferenceNodeTree, + value: z.string().optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { secretName } = req.params; + const { secretPath, environment, projectId } = req.query; + const { tree, value } = await server.services.secret.getSecretReferenceTree({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId, + secretName, + secretPath, + environment + }); + + return { tree, value }; + } + }); + + server.route({ + method: "POST", + url: "/backfill-secret-references", + config: { + rateLimit: secretsLimit + }, + schema: { + description: "Backfill secret references", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectId: z.string().trim().min(1) + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { projectId } = req.body; + const message = await server.services.secret.backfillSecretReferences({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId + }); + + return message; + } + }); +}; diff --git a/backend/src/services/app-connection/app-connection-dal.ts b/backend/src/services/app-connection/app-connection-dal.ts index f74f7cf06..10b6a6274 100644 --- a/backend/src/services/app-connection/app-connection-dal.ts +++ b/backend/src/services/app-connection/app-connection-dal.ts @@ -1,11 +1,115 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { TableName, TAppConnections } from "@app/db/schemas"; +import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; +import { transformUsageToProjects } from "@app/services/app-connection/app-connection-fns"; export type TAppConnectionDALFactory = ReturnType; +type AppConnectionFindFilter = Parameters>[0]; + export const appConnectionDALFactory = (db: TDbClient) => { const appConnectionOrm = ormify(db, TableName.AppConnection); - return { ...appConnectionOrm }; + const findWithProjectDetails = async (filter: AppConnectionFindFilter, tx?: Knex) => { + const query = (tx || db.replicaNode())(TableName.AppConnection) + .leftJoin(TableName.Project, `${TableName.AppConnection}.projectId`, `${TableName.Project}.id`) + .select(selectAllTableCols(TableName.AppConnection)) + .select( + // project + db.ref("name").withSchema(TableName.Project).as("projectName"), + db.ref("type").withSchema(TableName.Project).as("projectType"), + db.ref("slug").withSchema(TableName.Project).as("projectSlug") + ); + + if (filter) { + /* eslint-disable @typescript-eslint/no-misused-promises */ + void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.AppConnection, filter))); + } + + const connections = await query; + + return connections.map(({ projectName, projectSlug, projectType, projectId, ...connection }) => ({ + ...connection, + projectId, + project: projectId + ? { + name: projectName, + type: projectType, + slug: projectSlug, + id: projectId + } + : null + })); + }; + + const findAppConnectionUsageById = async (connectionId: string, tx?: Knex) => { + const secretSyncs = await (tx || db.replicaNode())(TableName.SecretSync) + .where(`${TableName.SecretSync}.connectionId`, connectionId) + .join(TableName.Project, `${TableName.SecretSync}.projectId`, `${TableName.Project}.id`) + .select( + db.ref("name").withSchema(TableName.SecretSync), + db.ref("id").withSchema(TableName.SecretSync), + db.ref("projectId").withSchema(TableName.SecretSync), + db.ref("name").as("projectName").withSchema(TableName.Project), + db.ref("slug").as("projectSlug").withSchema(TableName.Project), + db.ref("type").as("projectType").withSchema(TableName.Project) + ); + + const secretRotations = await (tx || db.replicaNode())(TableName.SecretRotationV2) + .where(`${TableName.SecretRotationV2}.connectionId`, connectionId) + .join(TableName.SecretFolder, `${TableName.SecretRotationV2}.folderId`, `${TableName.SecretFolder}.id`) + .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) + .join(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) + .select( + db.ref("name").withSchema(TableName.SecretRotationV2), + db.ref("id").withSchema(TableName.SecretRotationV2), + db.ref("id").as("projectId").withSchema(TableName.Project), + db.ref("name").as("projectName").withSchema(TableName.Project), + db.ref("slug").as("projectSlug").withSchema(TableName.Project), + db.ref("type").as("projectType").withSchema(TableName.Project) + ); + + const externalCas = await (tx || db.replicaNode())(TableName.ExternalCertificateAuthority) + .where(`${TableName.ExternalCertificateAuthority}.appConnectionId`, connectionId) + .orWhere(`${TableName.ExternalCertificateAuthority}.dnsAppConnectionId`, connectionId) + .join( + TableName.CertificateAuthority, + `${TableName.ExternalCertificateAuthority}.caId`, + `${TableName.CertificateAuthority}.id` + ) + .join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`) + .select( + db.ref("name").withSchema(TableName.CertificateAuthority), + db.ref("id").withSchema(TableName.ExternalCertificateAuthority), + db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority), + db.ref("dnsAppConnectionId").withSchema(TableName.ExternalCertificateAuthority), + db.ref("id").as("projectId").withSchema(TableName.Project), + db.ref("name").as("projectName").withSchema(TableName.Project), + db.ref("slug").as("projectSlug").withSchema(TableName.Project), + db.ref("type").as("projectType").withSchema(TableName.Project) + ); + + const dataSources = await (tx || db.replicaNode())(TableName.SecretScanningDataSource) + .where(`${TableName.SecretScanningDataSource}.connectionId`, connectionId) + .join(TableName.Project, `${TableName.SecretScanningDataSource}.projectId`, `${TableName.Project}.id`) + .select( + db.ref("name").withSchema(TableName.SecretScanningDataSource), + db.ref("id").withSchema(TableName.SecretScanningDataSource), + db.ref("id").as("projectId").withSchema(TableName.Project), + db.ref("name").as("projectName").withSchema(TableName.Project), + db.ref("slug").as("projectSlug").withSchema(TableName.Project), + db.ref("type").as("projectType").withSchema(TableName.Project) + ); + + return transformUsageToProjects({ + secretSyncs, + secretRotations, + dataSources, + externalCas + }); + }; + + return { ...appConnectionOrm, findAppConnectionUsageById, findWithProjectDetails }; }; diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 94de51f1e..f88b5a357 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -1,3 +1,4 @@ +import { ProjectType } from "@app/db/schemas"; import { TAppConnections } from "@app/db/schemas/app-connections"; import { getOCIConnectionListItem, @@ -6,7 +7,10 @@ import { } from "@app/ee/services/app-connections/oci"; import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee/services/app-connections/oracledb"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { SECRET_ROTATION_CONNECTION_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps"; +import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-maps"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; import { APP_CONNECTION_NAME_MAP, APP_CONNECTION_PLAN_MAP } from "@app/services/app-connection/app-connection-maps"; @@ -15,6 +19,7 @@ import { validateSqlConnectionCredentials } from "@app/services/app-connection/shared/sql"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { getOnePassConnectionListItem, @@ -132,7 +137,22 @@ import { } from "./windmill"; import { getZabbixConnectionListItem, validateZabbixConnectionCredentials, ZabbixConnectionMethod } from "./zabbix"; -export const listAppConnectionOptions = () => { +const SECRET_SYNC_APP_CONNECTION_MAP = Object.fromEntries( + Object.entries(SECRET_SYNC_CONNECTION_MAP).map(([key, value]) => [value, key]) +); + +const SECRET_ROTATION_APP_CONNECTION_MAP = Object.fromEntries( + Object.entries(SECRET_ROTATION_CONNECTION_MAP).map(([key, value]) => [value, key]) +); + +const SECRET_SCANNING_APP_CONNECTION_MAP = Object.fromEntries( + Object.entries(SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP).map(([key, value]) => [value, key]) +); + +// scott: ideally this would be derived from a utilized map like the above +const PKI_APP_CONNECTIONS = [AppConnection.AWS, AppConnection.Cloudflare, AppConnection.AzureADCS]; + +export const listAppConnectionOptions = (projectType?: ProjectType) => { return [ getAwsConnectionListItem(), getGitHubConnectionListItem(), @@ -172,22 +192,51 @@ export const listAppConnectionOptions = () => { getDigitalOceanConnectionListItem(), getNetlifyConnectionListItem(), getOktaConnectionListItem() - ].sort((a, b) => a.name.localeCompare(b.name)); + ] + .filter((option) => { + switch (projectType) { + case ProjectType.SecretManager: + return ( + Boolean(SECRET_SYNC_APP_CONNECTION_MAP[option.app]) || + Boolean(SECRET_ROTATION_APP_CONNECTION_MAP[option.app]) + ); + case ProjectType.SecretScanning: + return Boolean(SECRET_SCANNING_APP_CONNECTION_MAP[option.app]); + case ProjectType.CertificateManager: + return PKI_APP_CONNECTIONS.includes(option.app); + case ProjectType.KMS: + return false; + case ProjectType.SSH: + return false; + default: + return true; + } + }) + .sort((a, b) => a.name.localeCompare(b.name)); }; export const encryptAppConnectionCredentials = async ({ orgId, credentials, - kmsService + kmsService, + projectId }: { orgId: string; credentials: TAppConnection["credentials"]; kmsService: TAppConnectionServiceFactoryDep["kmsService"]; + projectId: string | null | undefined; }) => { - const { encryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId - }); + const { encryptor } = await kmsService.createCipherPairWithDataKey( + projectId + ? { + type: KmsDataKey.SecretManager, + projectId + } + : { + type: KmsDataKey.Organization, + orgId + } + ); const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({ plainText: Buffer.from(JSON.stringify(credentials)) @@ -199,16 +248,22 @@ export const encryptAppConnectionCredentials = async ({ export const decryptAppConnectionCredentials = async ({ orgId, encryptedCredentials, - kmsService + kmsService, + projectId }: { orgId: string; encryptedCredentials: Buffer; kmsService: TAppConnectionServiceFactoryDep["kmsService"]; + projectId: string | null | undefined; }) => { - const { decryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId - }); + const { decryptor } = await kmsService.createCipherPairWithDataKey( + projectId + ? { type: KmsDataKey.SecretManager, projectId } + : { + type: KmsDataKey.Organization, + orgId + } + ); const decryptedPlainTextBlob = decryptor({ cipherTextBlob: encryptedCredentials @@ -219,7 +274,8 @@ export const decryptAppConnectionCredentials = async ({ export const validateAppConnectionCredentials = async ( appConnection: TAppConnectionConfig, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ): Promise => { const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record = { [AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator, @@ -264,7 +320,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator }; - return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService); + return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service); }; export const getAppConnectionMethodName = (method: TAppConnection["method"]) => { @@ -341,6 +397,7 @@ export const decryptAppConnection = async ( credentials: await decryptAppConnectionCredentials({ encryptedCredentials: appConnection.encryptedCredentials, orgId: appConnection.orgId, + projectId: appConnection.projectId, kmsService }), credentialsHash: crypto.nativeCrypto.createHash("sha256").update(appConnection.encryptedCredentials).digest("hex") @@ -411,3 +468,73 @@ export const enterpriseAppCheck = async ( }); } }; + +type Resource = { + name: string; + id: string; + projectId: string; + projectName: string; + projectSlug: string; + projectType: string; +}; + +type UsageData = { + secretSyncs: Resource[]; + secretRotations: Resource[]; + dataSources: Resource[]; + externalCas: Resource[]; +}; + +type ResourceSummary = { + name: string; + id: string; +}; + +type ProjectWithResources = { + id: string; + name: string; + slug: string; + type: ProjectType; + resources: { + secretSyncs: ResourceSummary[]; + secretRotations: ResourceSummary[]; + dataSources: ResourceSummary[]; + externalCas: (ResourceSummary & { appConnectionId?: string; dnsAppConnectionId?: string })[]; + }; +}; + +export const transformUsageToProjects = (data: UsageData): ProjectWithResources[] => { + const projectMap = new Map(); + + Object.entries(data).forEach(([resourceType, resources]) => { + resources.forEach((resource) => { + const { projectId, projectName, projectSlug, projectType, name, id, ...rest } = resource; + + const projectKey = projectId; + + if (!projectMap.has(projectKey)) { + projectMap.set(projectKey, { + id: projectId, + name: projectName, + slug: projectSlug, + type: projectType as ProjectType, + resources: { + secretSyncs: [], + secretRotations: [], + dataSources: [], + externalCas: [] + } + }); + } + + const project = projectMap.get(projectKey)!; + project.resources[resourceType as keyof ProjectWithResources["resources"]].push({ + name, + id, + ...rest + }); + }); + }); + + return Array.from(projectMap.values()); +}; diff --git a/backend/src/services/app-connection/app-connection-schemas.ts b/backend/src/services/app-connection/app-connection-schemas.ts index d0dcb1a54..3f6e3914a 100644 --- a/backend/src/services/app-connection/app-connection-schemas.ts +++ b/backend/src/services/app-connection/app-connection-schemas.ts @@ -13,7 +13,15 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({ app: true, method: true }).extend({ - credentialsHash: z.string().optional() + credentialsHash: z.string().optional(), + project: z + .object({ + name: z.string(), + id: z.string(), + type: z.string(), + slug: z.string() + }) + .nullish() }); export const GenericCreateAppConnectionFieldsSchema = ( @@ -28,6 +36,7 @@ export const GenericCreateAppConnectionFieldsSchema = ( .max(256, "Description cannot exceed 256 characters") .nullish() .describe(AppConnections.CREATE(app).description), + projectId: z.string().optional().describe(AppConnections.CREATE(app).projectId), isPlatformManagedCredentials: supportsPlatformManagedCredentials ? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials) : z diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 869364853..19d66b9fd 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -1,10 +1,13 @@ import { ForbiddenError, subject } from "@casl/ability"; +import { ActionProjectType, TAppConnections } from "@app/db/schemas"; import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci"; import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service"; import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2DALFactory } from "@app/ee/services/gateway-v2/gateway-v2-dal"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionAppConnectionActions, @@ -12,6 +15,10 @@ import { OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + ProjectPermissionAppConnectionActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { crypto } from "@app/lib/crypto/cryptography"; import { DatabaseErrorCode } from "@app/lib/error-codes"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; @@ -25,9 +32,8 @@ import { TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM, validateAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; -import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service"; -import { githubRadarConnectionService } from "@app/services/app-connection/github-radar/github-radar-connection-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; import { ValidateOnePassConnectionCredentialsSchema } from "./1password"; import { onePassConnectionService } from "./1password/1password-connection-service"; @@ -39,10 +45,13 @@ import { TAppConnectionConfig, TAppConnectionRaw, TCreateAppConnectionDTO, + TGetAppConnectionByNameDTO, TUpdateAppConnectionDTO, - TValidateAppConnectionCredentialsSchema + TValidateAppConnectionCredentialsSchema, + TValidateAppConnectionUsageByIdDTO } from "./app-connection-types"; import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0"; +import { auth0ConnectionService } from "./auth0/auth0-connection-service"; import { ValidateAwsConnectionCredentialsSchema } from "./aws"; import { awsConnectionService } from "./aws/aws-connection-service"; import { ValidateAzureADCSConnectionCredentialsSchema } from "./azure-adcs/azure-adcs-connection-schemas"; @@ -71,6 +80,7 @@ import { gcpConnectionService } from "./gcp/gcp-connection-service"; import { ValidateGitHubConnectionCredentialsSchema } from "./github"; import { githubConnectionService } from "./github/github-connection-service"; import { ValidateGitHubRadarConnectionCredentialsSchema } from "./github-radar"; +import { githubRadarConnectionService } from "./github-radar/github-radar-connection-service"; import { ValidateGitLabConnectionCredentialsSchema } from "./gitlab"; import { gitlabConnectionService } from "./gitlab/gitlab-connection-service"; import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault"; @@ -106,11 +116,14 @@ import { zabbixConnectionService } from "./zabbix/zabbix-connection-service"; export type TAppConnectionServiceFactoryDep = { appConnectionDAL: TAppConnectionDALFactory; - permissionService: Pick; + permissionService: Pick; kmsService: Pick; licenseService: Pick; gatewayService: Pick; + gatewayV2Service: Pick; gatewayDAL: Pick; + gatewayV2DAL: Pick; + projectDAL: Pick; }; export type TAppConnectionServiceFactory = ReturnType; @@ -162,29 +175,66 @@ export const appConnectionServiceFactory = ({ kmsService, licenseService, gatewayService, - gatewayDAL + gatewayV2Service, + gatewayDAL, + gatewayV2DAL, + projectDAL }: TAppConnectionServiceFactoryDep) => { - const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const listAppConnections = async (actor: OrgServiceActor, app?: AppConnection, projectId?: string) => { + let appConnections: TAppConnections[]; - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionAppConnectionActions.Read, - OrgPermissionSubjects.AppConnections - ); + if (projectId) { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.Any + }); - const appConnections = await appConnectionDAL.find( - app - ? { orgId: actor.orgId, app } - : { - orgId: actor.orgId - } - ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionAppConnectionActions.Read, + ProjectPermissionSub.AppConnections + ); + + appConnections = ( + await appConnectionDAL.findWithProjectDetails({ + projectId, + ...(app ? { app } : {}) + }) + ).filter((appConnection) => + permission.can( + ProjectPermissionAppConnectionActions.Read, + subject(ProjectPermissionSub.AppConnections, { connectionId: appConnection.id }) + ) + ); + } else { + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAppConnectionActions.Read, + OrgPermissionSubjects.AppConnections + ); + + appConnections = ( + await appConnectionDAL.findWithProjectDetails({ + orgId: actor.orgId, + ...(app ? { app } : {}) + }) + ).filter((appConnection) => + permission.can( + OrgPermissionAppConnectionActions.Read, + subject(OrgPermissionSubjects.AppConnections, { connectionId: appConnection.id }) + ) + ); + } return Promise.all( appConnections @@ -198,18 +248,34 @@ export const appConnectionServiceFactory = ({ if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - appConnection.orgId - ); + if (appConnection.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: appConnection.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.Any + }); - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionAppConnectionActions.Read, - OrgPermissionSubjects.AppConnections - ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionAppConnectionActions.Read, + subject(ProjectPermissionSub.AppConnections, { connectionId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + appConnection.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAppConnectionActions.Read, + subject(OrgPermissionSubjects.AppConnections, { connectionId }) + ); + } if (appConnection.app !== app) throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` }); @@ -217,24 +283,49 @@ export const appConnectionServiceFactory = ({ return decryptAppConnection(appConnection, kmsService); }; - const findAppConnectionByName = async (app: AppConnection, connectionName: string, actor: OrgServiceActor) => { - const appConnection = await appConnectionDAL.findOne({ name: connectionName, orgId: actor.orgId }); + const findAppConnectionByName = async ( + app: AppConnection, + { connectionName, projectId }: TGetAppConnectionByNameDTO, + actor: OrgServiceActor + ) => { + const appConnection = await appConnectionDAL.findOne({ + name: connectionName, + ...(projectId ? { projectId } : { orgId: actor.orgId, projectId: null }) + }); if (!appConnection) - throw new NotFoundError({ message: `Could not find App Connection with name ${connectionName}` }); + throw new NotFoundError({ + message: `Could not find App Connection with name ${connectionName} in ${projectId ? "project" : "organization"} scope.` + }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - appConnection.orgId - ); + if (appConnection.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: appConnection.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.Any + }); - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionAppConnectionActions.Read, - OrgPermissionSubjects.AppConnections - ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionAppConnectionActions.Read, + subject(ProjectPermissionSub.AppConnections, { connectionId: appConnection.id }) + ); + } else { + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + appConnection.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAppConnectionActions.Read, + subject(OrgPermissionSubjects.AppConnections, { connectionId: appConnection.id }) + ); + } if (appConnection.app !== app) throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for App "${app}"` }); @@ -243,10 +334,10 @@ export const appConnectionServiceFactory = ({ }; const createAppConnection = async ( - { method, app, credentials, gatewayId, ...params }: TCreateAppConnectionDTO, + { method, app, credentials, gatewayId, projectId, ...params }: TCreateAppConnectionDTO, actor: OrgServiceActor ) => { - const { permission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission( actor.type, actor.id, actor.orgId, @@ -254,19 +345,40 @@ export const appConnectionServiceFactory = ({ actor.orgId ); - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionAppConnectionActions.Create, - OrgPermissionSubjects.AppConnections - ); + if (projectId) { + const project = await projectDAL.findProjectById(projectId); + + if (!project) throw new BadRequestError({ message: `Could not find project with ID ${projectId}` }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionAppConnectionActions.Create, + ProjectPermissionSub.AppConnections + ); + } else { + ForbiddenError.from(orgPermission).throwUnlessCan( + OrgPermissionAppConnectionActions.Create, + OrgPermissionSubjects.AppConnections + ); + } if (gatewayId) { - ForbiddenError.from(permission).throwUnlessCan( + ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway ); const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId }); - if (!gateway) { + const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: actor.orgId }); + if (!gateway && !gatewayV2) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found for org` }); @@ -288,7 +400,8 @@ export const appConnectionServiceFactory = ({ orgId: actor.orgId, gatewayId } as TAppConnectionConfig, - gatewayService + gatewayService, + gatewayV2Service ); try { @@ -296,7 +409,8 @@ export const appConnectionServiceFactory = ({ const encryptedCredentials = await encryptAppConnectionCredentials({ credentials: connectionCredentials, orgId: actor.orgId, - kmsService + kmsService, + projectId }); return appConnectionDAL.create({ @@ -305,6 +419,7 @@ export const appConnectionServiceFactory = ({ method, app, gatewayId, + projectId, ...params }); }; @@ -321,7 +436,8 @@ export const appConnectionServiceFactory = ({ gatewayId } as TAppConnectionConfig, (platformCredentials) => createConnection(platformCredentials), - gatewayService + gatewayService, + gatewayV2Service ); } else { connection = await createConnection(validatedCredentials); @@ -356,7 +472,7 @@ export const appConnectionServiceFactory = ({ "Failed to update app connection due to plan restriction. Upgrade plan to access enterprise app connections." ); - const { permission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission( actor.type, actor.id, actor.orgId, @@ -364,20 +480,37 @@ export const appConnectionServiceFactory = ({ appConnection.orgId ); - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionAppConnectionActions.Edit, - OrgPermissionSubjects.AppConnections - ); + if (appConnection.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: appConnection.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.Any + }); - if (gatewayId !== appConnection.gatewayId) { ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionAppConnectionActions.Edit, + subject(ProjectPermissionSub.AppConnections, { connectionId }) + ); + } else { + ForbiddenError.from(orgPermission).throwUnlessCan( + OrgPermissionAppConnectionActions.Edit, + subject(OrgPermissionSubjects.AppConnections, { connectionId }) + ); + } + + if (gatewayId !== undefined && gatewayId !== appConnection.gatewayId) { + ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway ); if (gatewayId) { const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId }); - if (!gateway) { + const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: actor.orgId }); + if (!gateway && !gatewayV2) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found for org` }); @@ -417,7 +550,8 @@ export const appConnectionServiceFactory = ({ method, gatewayId } as TAppConnectionConfig, - gatewayService + gatewayService, + gatewayV2Service ); if (!updatedCredentials) @@ -430,7 +564,8 @@ export const appConnectionServiceFactory = ({ ? await encryptAppConnectionCredentials({ credentials: connectionCredentials, orgId: actor.orgId, - kmsService + kmsService, + projectId: appConnection.projectId }) : undefined; @@ -458,7 +593,8 @@ export const appConnectionServiceFactory = ({ gatewayId } as TAppConnectionConfig, (platformCredentials) => updateConnection(platformCredentials), - gatewayService + gatewayService, + gatewayV2Service ); } else { updatedConnection = await updateConnection(updatedCredentials); @@ -479,18 +615,34 @@ export const appConnectionServiceFactory = ({ if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - appConnection.orgId - ); + if (appConnection.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: appConnection.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.Any + }); - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionAppConnectionActions.Delete, - OrgPermissionSubjects.AppConnections - ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionAppConnectionActions.Delete, + subject(ProjectPermissionSub.AppConnections, { connectionId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + appConnection.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAppConnectionActions.Delete, + subject(OrgPermissionSubjects.AppConnections, { connectionId }) + ); + } if (appConnection.app !== app) throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` }); @@ -532,18 +684,34 @@ export const appConnectionServiceFactory = ({ "Failed to connect app due to plan restriction. Upgrade plan to access enterprise app connections." ); - const { permission: orgPermission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + if (appConnection.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: appConnection.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.Any + }); - ForbiddenError.from(orgPermission).throwUnlessCan( - OrgPermissionAppConnectionActions.Connect, - subject(OrgPermissionSubjects.AppConnections, { connectionId: appConnection.id }) - ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionAppConnectionActions.Connect, + subject(ProjectPermissionSub.AppConnections, { connectionId }) + ); + } else { + const { permission: orgPermission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + appConnection.orgId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(orgPermission).throwUnlessCan( + OrgPermissionAppConnectionActions.Connect, + subject(OrgPermissionSubjects.AppConnections, { connectionId }) + ); + } if (appConnection.app !== app) throw new BadRequestError({ @@ -557,7 +725,23 @@ export const appConnectionServiceFactory = ({ return connection as T; }; - const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor) => { + const validateAppConnectionUsageById = async ( + app: AppConnection, + { connectionId, projectId }: TValidateAppConnectionUsageByIdDTO, + actor: OrgServiceActor + ) => { + const appConnection = await connectAppConnectionById(app, connectionId, actor); + + if (appConnection.projectId && appConnection.projectId !== projectId) { + throw new BadRequestError({ + message: `You cannot connect project App Connection with ID "${appConnection.id}" from project with ID "${appConnection.projectId}" to project with ID "${projectId}"` + }); + } + + return appConnection; + }; + + const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor, projectId?: string) => { const { permission: orgPermission } = await permissionService.getOrgPermission( actor.type, actor.id, @@ -566,29 +750,90 @@ export const appConnectionServiceFactory = ({ actor.orgId ); - const appConnections = await appConnectionDAL.find({ app, orgId: actor.orgId }); + let availableProjectConnections: TAppConnections[] = []; - const availableConnections = appConnections.filter((connection) => + if (projectId) { + const project = await projectDAL.findProjectById(projectId); + + if (!project) throw new BadRequestError({ message: `Could not find project with ID ${projectId}` }); + + const { permission: projectPermission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(projectPermission).throwUnlessCan( + ProjectPermissionAppConnectionActions.Connect, + ProjectPermissionSub.AppConnections + ); + + const projectAppConnections = await appConnectionDAL.find({ app, projectId }); + + availableProjectConnections = projectAppConnections.filter((connection) => + projectPermission.can( + ProjectPermissionAppConnectionActions.Connect, + subject(ProjectPermissionSub.AppConnections, { connectionId: connection.id }) + ) + ); + } + + const orgAppConnections = await appConnectionDAL.find({ app, orgId: actor.orgId, projectId: null }); + + const availableOrgConnections = orgAppConnections.filter((connection) => orgPermission.can( OrgPermissionAppConnectionActions.Connect, subject(OrgPermissionSubjects.AppConnections, { connectionId: connection.id }) ) ); - return availableConnections as Omit[]; + return [...availableOrgConnections, ...availableProjectConnections].sort((a, b) => + a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + ) as Omit[]; + }; + + const findAppConnectionUsageById = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => { + const appConnection = await appConnectionDAL.findById(connectionId); + + if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); + + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + appConnection.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAppConnectionActions.Read, + OrgPermissionSubjects.AppConnections + ); + + if (appConnection.app !== app) + throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` }); + + const projectUsage = await appConnectionDAL.findAppConnectionUsageById(connectionId); + + return projectUsage; }; return { listAppConnectionOptions, - listAppConnectionsByOrg, + listAppConnections, findAppConnectionById, findAppConnectionByName, createAppConnection, updateAppConnection, deleteAppConnection, connectAppConnectionById, + validateAppConnectionUsageById, listAvailableAppConnectionsForUser, - github: githubConnectionService(connectAppConnectionById, gatewayService), + findAppConnectionUsageById, + github: githubConnectionService(connectAppConnectionById, gatewayService, gatewayV2Service), githubRadar: githubRadarConnectionService(connectAppConnectionById), gcp: gcpConnectionService(connectAppConnectionById), databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index 43a141697..600438fc9 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -10,6 +10,7 @@ import { TValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; @@ -315,13 +316,23 @@ export type TSqlConnectionInput = export type TCreateAppConnectionDTO = Pick< TAppConnectionInput, - "credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" | "gatewayId" + "credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" | "gatewayId" | "projectId" >; -export type TUpdateAppConnectionDTO = Partial> & { +export type TUpdateAppConnectionDTO = Partial> & { connectionId: string; }; +export type TGetAppConnectionByNameDTO = { + connectionName: string; + projectId?: string; +}; + +export type TValidateAppConnectionUsageByIdDTO = { + connectionId: string; + projectId: string; +}; + export type TAppConnectionConfig = | TAwsConnectionConfig | TGitHubConnectionConfig @@ -411,13 +422,15 @@ export type TListAwsConnectionIamUsers = { export type TAppConnectionCredentialsValidator = ( appConnection: TAppConnectionConfig, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => Promise; export type TAppConnectionTransitionCredentialsToPlatform = ( appConnection: TAppConnectionConfig, callback: (credentials: TAppConnection["credentials"]) => Promise, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => Promise; export type TAppConnectionBaseConfig = { diff --git a/backend/src/services/app-connection/auth0/auth0-connection-fns.ts b/backend/src/services/app-connection/auth0/auth0-connection-fns.ts index de4faf683..944b1f69a 100644 --- a/backend/src/services/app-connection/auth0/auth0-connection-fns.ts +++ b/backend/src/services/app-connection/auth0/auth0-connection-fns.ts @@ -51,7 +51,7 @@ const authorizeAuth0Connection = async ({ }; export const getAuth0ConnectionAccessToken = async ( - { id, orgId, credentials }: TAuth0Connection, + { id, orgId, credentials, projectId }: TAuth0Connection, appConnectionDAL: Pick, kmsService: Pick ) => { @@ -72,7 +72,8 @@ export const getAuth0ConnectionAccessToken = async ( const encryptedCredentials = await encryptAppConnectionCredentials({ credentials: updatedCredentials, orgId, - kmsService + kmsService, + projectId }); await appConnectionDAL.updateById(id, { encryptedCredentials }); diff --git a/backend/src/services/app-connection/azure-adcs/azure-adcs-connection-fns.ts b/backend/src/services/app-connection/azure-adcs/azure-adcs-connection-fns.ts index 552bd89f5..5e86f6740 100644 --- a/backend/src/services/app-connection/azure-adcs/azure-adcs-connection-fns.ts +++ b/backend/src/services/app-connection/azure-adcs/azure-adcs-connection-fns.ts @@ -352,7 +352,8 @@ export const getAzureADCSConnectionCredentials = async ( const credentials = (await decryptAppConnectionCredentials({ orgId: appConnection.orgId, kmsService, - encryptedCredentials: appConnection.encryptedCredentials + encryptedCredentials: appConnection.encryptedCredentials, + projectId: appConnection.projectId })) as { username: string; password: string; diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts index 2614cfd12..22cec0ae7 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts @@ -57,6 +57,7 @@ export const getAzureConnectionAccessToken = async ( const credentials = (await decryptAppConnectionCredentials({ orgId: appConnection.orgId, kmsService, + projectId: appConnection.projectId, encryptedCredentials: appConnection.encryptedCredentials })) as TAzureClientSecretsConnectionCredentials; @@ -93,6 +94,7 @@ export const getAzureConnectionAccessToken = async ( const encryptedCredentials = await encryptAppConnectionCredentials({ credentials: updatedCredentials, orgId: appConnection.orgId, + projectId: appConnection.projectId, kmsService }); @@ -102,6 +104,7 @@ export const getAzureConnectionAccessToken = async ( case AzureClientSecretsConnectionMethod.ClientSecret: const accessTokenCredentials = (await decryptAppConnectionCredentials({ orgId: appConnection.orgId, + projectId: appConnection.projectId, kmsService, encryptedCredentials: appConnection.encryptedCredentials })) as TAzureClientSecretsConnectionClientSecretCredentials; @@ -129,6 +132,7 @@ export const getAzureConnectionAccessToken = async ( const encryptedClientCredentials = await encryptAppConnectionCredentials({ credentials: updatedClientCredentials, orgId: appConnection.orgId, + projectId: appConnection.projectId, kmsService }); diff --git a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts index a3a9f10bd..0bd2188ac 100644 --- a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts +++ b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts @@ -70,7 +70,8 @@ export const getAzureDevopsConnection = async ( const oauthCredentials = (await decryptAppConnectionCredentials({ orgId: appConnection.orgId, kmsService, - encryptedCredentials: appConnection.encryptedCredentials + encryptedCredentials: appConnection.encryptedCredentials, + projectId: appConnection.projectId })) as TAzureDevOpsConnectionCredentials; if (!("refreshToken" in oauthCredentials)) { @@ -100,7 +101,8 @@ export const getAzureDevopsConnection = async ( const encryptedOAuthCredentials = await encryptAppConnectionCredentials({ credentials: updatedOAuthCredentials, orgId: appConnection.orgId, - kmsService + kmsService, + projectId: appConnection.projectId }); await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials }); @@ -111,7 +113,8 @@ export const getAzureDevopsConnection = async ( const accessTokenCredentials = (await decryptAppConnectionCredentials({ orgId: appConnection.orgId, kmsService, - encryptedCredentials: appConnection.encryptedCredentials + encryptedCredentials: appConnection.encryptedCredentials, + projectId: appConnection.projectId })) as { accessToken: string }; if (!("accessToken" in accessTokenCredentials)) { @@ -124,7 +127,8 @@ export const getAzureDevopsConnection = async ( const clientSecretCredentials = (await decryptAppConnectionCredentials({ orgId: appConnection.orgId, kmsService, - encryptedCredentials: appConnection.encryptedCredentials + encryptedCredentials: appConnection.encryptedCredentials, + projectId: appConnection.projectId })) as TAzureDevOpsConnectionClientSecretCredentials; const { accessToken, expiresAt, clientId, clientSecret, tenantId: clientTenantId } = clientSecretCredentials; @@ -153,7 +157,8 @@ export const getAzureDevopsConnection = async ( const encryptedClientCredentials = await encryptAppConnectionCredentials({ credentials: updatedClientCredentials, orgId: appConnection.orgId, - kmsService + kmsService, + projectId: appConnection.projectId }); await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials }); diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts index d6a260050..cd3583800 100644 --- a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts @@ -58,7 +58,8 @@ export const getAzureConnectionAccessToken = async ( const oauthCredentials = (await decryptAppConnectionCredentials({ orgId: appConnection.orgId, kmsService, - encryptedCredentials: appConnection.encryptedCredentials + encryptedCredentials: appConnection.encryptedCredentials, + projectId: appConnection.projectId })) as TAzureKeyVaultConnectionCredentials; const { data } = await request.post( @@ -82,7 +83,8 @@ export const getAzureConnectionAccessToken = async ( const encryptedOAuthCredentials = await encryptAppConnectionCredentials({ credentials: updatedOAuthCredentials, orgId: appConnection.orgId, - kmsService + kmsService, + projectId: appConnection.projectId }); await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials }); @@ -95,7 +97,8 @@ export const getAzureConnectionAccessToken = async ( const clientSecretCredentials = (await decryptAppConnectionCredentials({ orgId: appConnection.orgId, kmsService, - encryptedCredentials: appConnection.encryptedCredentials + encryptedCredentials: appConnection.encryptedCredentials, + projectId: appConnection.projectId })) as TAzureKeyVaultConnectionClientSecretCredentials; const { accessToken, expiresAt, clientId, clientSecret, tenantId } = clientSecretCredentials; @@ -124,7 +127,8 @@ export const getAzureConnectionAccessToken = async ( const encryptedClientCredentials = await encryptAppConnectionCredentials({ credentials: updatedClientCredentials, orgId: appConnection.orgId, - kmsService + kmsService, + projectId: appConnection.projectId }); await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials }); diff --git a/backend/src/services/app-connection/camunda/camunda-connection-fns.ts b/backend/src/services/app-connection/camunda/camunda-connection-fns.ts index 91a033c0e..b764da336 100644 --- a/backend/src/services/app-connection/camunda/camunda-connection-fns.ts +++ b/backend/src/services/app-connection/camunda/camunda-connection-fns.ts @@ -40,7 +40,7 @@ const authorizeCamundaConnection = async ({ }; export const getCamundaConnectionAccessToken = async ( - { id, orgId, credentials }: TCamundaConnection, + { id, orgId, credentials, projectId }: TCamundaConnection, appConnectionDAL: Pick, kmsService: Pick ) => { @@ -61,7 +61,8 @@ export const getCamundaConnectionAccessToken = async ( const encryptedCredentials = await encryptAppConnectionCredentials({ credentials: updatedCredentials, orgId, - kmsService + kmsService, + projectId }); await appConnectionDAL.updateById(id, { encryptedCredentials }); diff --git a/backend/src/services/app-connection/databricks/databricks-connection-fns.ts b/backend/src/services/app-connection/databricks/databricks-connection-fns.ts index 8912ad936..a9128ec51 100644 --- a/backend/src/services/app-connection/databricks/databricks-connection-fns.ts +++ b/backend/src/services/app-connection/databricks/databricks-connection-fns.ts @@ -47,7 +47,7 @@ const authorizeDatabricksConnection = async ({ }; export const getDatabricksConnectionAccessToken = async ( - { id, orgId, credentials }: TDatabricksConnection, + { id, orgId, credentials, projectId }: TDatabricksConnection, appConnectionDAL: Pick, kmsService: Pick ) => { @@ -68,7 +68,8 @@ export const getDatabricksConnectionAccessToken = async ( const encryptedCredentials = await encryptAppConnectionCredentials({ credentials: updatedCredentials, orgId, - kmsService + kmsService, + projectId }); await appConnectionDAL.updateById(id, { encryptedCredentials }); diff --git a/backend/src/services/app-connection/github/github-connection-fns.ts b/backend/src/services/app-connection/github/github-connection-fns.ts index 05e6fdda3..5cbf5c60d 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -4,11 +4,13 @@ import RE2 from "re2"; import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { getConfig } from "@app/lib/config/env"; import { request as httpRequest } from "@app/lib/config/request"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors"; import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { logger } from "@app/lib/logger"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; @@ -49,6 +51,7 @@ export const getGitHubInstanceApiUrl = async (config: { export const requestWithGitHubGateway = async ( appConnection: { gatewayId?: string | null }, gatewayService: Pick, + gatewayV2Service: Pick, requestConfig: AxiosRequestConfig ): Promise> => { const { gatewayId } = appConnection; @@ -63,6 +66,52 @@ export const requestWithGitHubGateway = async ( await blockLocalAndPrivateIpAddresses(url.toString()); const [targetHost] = await verifyHostInputValidity(url.host, true); + const gatewayConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ + gatewayId, + targetHost, + targetPort: 443 + }); + + if (gatewayConnectionDetails) { + return withGatewayV2Proxy( + async (proxyPort) => { + const httpsAgent = new https.Agent({ + servername: targetHost + }); + + url.protocol = "https:"; + url.host = `localhost:${proxyPort}`; + + const finalRequestConfig: AxiosRequestConfig = { + ...requestConfig, + url: url.toString(), + httpsAgent, + headers: { + ...requestConfig.headers, + Host: targetHost + } + }; + + try { + return await httpRequest.request(finalRequestConfig); + } catch (error) { + const axiosError = error as AxiosError; + logger.error( + { message: axiosError.message, data: axiosError.response?.data }, + "Error during GitHub gateway request:" + ); + throw error; + } + }, + { + protocol: GatewayProxyProtocol.Tcp, + relayHost: gatewayConnectionDetails.relayHost, + gateway: gatewayConnectionDetails.gateway, + relay: gatewayConnectionDetails.relay + } + ); + } + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); @@ -115,7 +164,8 @@ export const requestWithGitHubGateway = async ( export const getGitHubAppAuthToken = async ( appConnection: TGitHubConnection, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { const appCfg = getConfig(); const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID; @@ -151,6 +201,7 @@ export const getGitHubAppAuthToken = async ( const response = await requestWithGitHubGateway<{ token: string; expires_at: string }>( appConnection, gatewayService, + gatewayV2Service, { url: `https://${apiBaseUrl}/app/installations/${installationId}/access_tokens`, method: "POST", @@ -191,6 +242,7 @@ function extractNextPageUrl(linkHeader: string | undefined): string | null { export const makePaginatedGitHubRequest = async ( appConnection: TGitHubConnection, gatewayService: Pick, + gatewayV2Service: Pick, path: string, dataMapper?: (data: R) => T[] ): Promise => { @@ -199,7 +251,7 @@ export const makePaginatedGitHubRequest = async ( const token = method === GitHubConnectionMethod.OAuth ? credentials.accessToken - : await getGitHubAppAuthToken(appConnection, gatewayService); + : await getGitHubAppAuthToken(appConnection, gatewayService, gatewayV2Service); const baseUrl = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`; const initialUrlObj = new URL(baseUrl); @@ -209,15 +261,20 @@ export const makePaginatedGitHubRequest = async ( const maxIterations = 1000; // Make initial request to get link header - const firstResponse: AxiosResponse = await requestWithGitHubGateway(appConnection, gatewayService, { - url: initialUrlObj.toString(), - method: "GET", - headers: { - Accept: "application/vnd.github+json", - Authorization: `Bearer ${token}`, - "X-GitHub-Api-Version": "2022-11-28" + const firstResponse: AxiosResponse = await requestWithGitHubGateway( + appConnection, + gatewayService, + gatewayV2Service, + { + url: initialUrlObj.toString(), + method: "GET", + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${token}`, + "X-GitHub-Api-Version": "2022-11-28" + } } - }); + ); const firstPageItems = dataMapper ? dataMapper(firstResponse.data) : (firstResponse.data as unknown as T[]); results = results.concat(firstPageItems); @@ -237,7 +294,7 @@ export const makePaginatedGitHubRequest = async ( pageUrlObj.searchParams.set("page", pageNum.toString()); pageRequests.push( - requestWithGitHubGateway(appConnection, gatewayService, { + requestWithGitHubGateway(appConnection, gatewayService, gatewayV2Service, { url: pageUrlObj.toString(), method: "GET", headers: { @@ -261,15 +318,20 @@ export const makePaginatedGitHubRequest = async ( while (url && i < maxIterations) { // eslint-disable-next-line no-await-in-loop - const response: AxiosResponse = await requestWithGitHubGateway(appConnection, gatewayService, { - url, - method: "GET", - headers: { - Accept: "application/vnd.github+json", - Authorization: `Bearer ${token}`, - "X-GitHub-Api-Version": "2022-11-28" + const response: AxiosResponse = await requestWithGitHubGateway( + appConnection, + gatewayService, + gatewayV2Service, + { + url, + method: "GET", + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${token}`, + "X-GitHub-Api-Version": "2022-11-28" + } } - }); + ); const items = dataMapper ? dataMapper(response.data) : (response.data as unknown as T[]); results = results.concat(items); @@ -308,30 +370,39 @@ type GitHubEnvironment = { export const getGitHubRepositories = async ( appConnection: TGitHubConnection, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { if (appConnection.method === GitHubConnectionMethod.App) { return makePaginatedGitHubRequest( appConnection, gatewayService, + gatewayV2Service, "/installation/repositories", (data) => data.repositories ); } - const repos = await makePaginatedGitHubRequest(appConnection, gatewayService, "/user/repos"); + const repos = await makePaginatedGitHubRequest( + appConnection, + gatewayService, + gatewayV2Service, + "/user/repos" + ); + return repos.filter((repo) => repo.permissions?.admin); }; export const getGitHubOrganizations = async ( appConnection: TGitHubConnection, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { if (appConnection.method === GitHubConnectionMethod.App) { const installationRepositories = await makePaginatedGitHubRequest< GitHubRepository, { repositories: GitHubRepository[] } - >(appConnection, gatewayService, "/installation/repositories", (data) => data.repositories); + >(appConnection, gatewayService, gatewayV2Service, "/installation/repositories", (data) => data.repositories); const organizationMap: Record = {}; installationRepositories.forEach((repo) => { @@ -343,12 +414,13 @@ export const getGitHubOrganizations = async ( return Object.values(organizationMap); } - return makePaginatedGitHubRequest(appConnection, gatewayService, "/user/orgs"); + return makePaginatedGitHubRequest(appConnection, gatewayService, gatewayV2Service, "/user/orgs"); }; export const getGitHubEnvironments = async ( appConnection: TGitHubConnection, gatewayService: Pick, + gatewayV2Service: Pick, owner: string, repo: string ) => { @@ -356,6 +428,7 @@ export const getGitHubEnvironments = async ( return await makePaginatedGitHubRequest( appConnection, gatewayService, + gatewayV2Service, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/environments`, (data) => data.environments ); @@ -383,7 +456,8 @@ export function isGithubErrorResponse(data: GithubTokenRespData): data is Github export const validateGitHubConnectionCredentials = async ( config: TGitHubConnectionConfig, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { const { credentials, method } = config; const { @@ -419,7 +493,7 @@ export const validateGitHubConnectionCredentials = async ( const host = credentials.host || "github.com"; try { - tokenResp = await requestWithGitHubGateway(config, gatewayService, { + tokenResp = await requestWithGitHubGateway(config, gatewayService, gatewayV2Service, { url: `https://${host}/login/oauth/access_token`, method: "POST", data: { @@ -471,7 +545,7 @@ export const validateGitHubConnectionCredentials = async ( id: number; }; }[]; - }>(config, gatewayService, { + }>(config, gatewayService, gatewayV2Service, { url: `https://${await getGitHubInstanceApiUrl(config)}/user/installations`, headers: { Accept: "application/json", diff --git a/backend/src/services/app-connection/github/github-connection-service.ts b/backend/src/services/app-connection/github/github-connection-service.ts index f1198ddfa..8292d94e0 100644 --- a/backend/src/services/app-connection/github/github-connection-service.ts +++ b/backend/src/services/app-connection/github/github-connection-service.ts @@ -1,4 +1,5 @@ import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { @@ -22,12 +23,13 @@ type TListGitHubEnvironmentsDTO = { export const githubConnectionService = ( getAppConnection: TGetAppConnectionFunc, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { const listRepositories = async (connectionId: string, actor: OrgServiceActor) => { const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor); - const repositories = await getGitHubRepositories(appConnection, gatewayService); + const repositories = await getGitHubRepositories(appConnection, gatewayService, gatewayV2Service); return repositories; }; @@ -35,7 +37,7 @@ export const githubConnectionService = ( const listOrganizations = async (connectionId: string, actor: OrgServiceActor) => { const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor); - const organizations = await getGitHubOrganizations(appConnection, gatewayService); + const organizations = await getGitHubOrganizations(appConnection, gatewayService, gatewayV2Service); return organizations; }; @@ -46,7 +48,7 @@ export const githubConnectionService = ( ) => { const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor); - const environments = await getGitHubEnvironments(appConnection, gatewayService, owner, repo); + const environments = await getGitHubEnvironments(appConnection, gatewayService, gatewayV2Service, owner, repo); return environments; }; diff --git a/backend/src/services/app-connection/gitlab/gitlab-connection-fns.ts b/backend/src/services/app-connection/gitlab/gitlab-connection-fns.ts index cb4e27e94..9499d6bf2 100644 --- a/backend/src/services/app-connection/gitlab/gitlab-connection-fns.ts +++ b/backend/src/services/app-connection/gitlab/gitlab-connection-fns.ts @@ -64,6 +64,7 @@ export const refreshGitLabToken = async ( refreshToken: string, appId: string, orgId: string, + projectId: string | undefined | null, appConnectionDAL: Pick, kmsService: Pick, instanceUrl?: string @@ -105,7 +106,8 @@ export const refreshGitLabToken = async ( expiresAt }, orgId, - kmsService + kmsService, + projectId }); await appConnectionDAL.updateById(appId, { encryptedCredentials }); @@ -238,6 +240,7 @@ export const getGitLabConnectionClient = async ( appConnection.credentials.refreshToken, appConnection.id, appConnection.orgId, + appConnection.projectId, appConnectionDAL, kmsService, appConnection.credentials.instanceUrl @@ -273,6 +276,7 @@ export const listGitLabProjects = async ({ appConnection.credentials.refreshToken, appConnection.id, appConnection.orgId, + appConnection.projectId, appConnectionDAL, kmsService, appConnection.credentials.instanceUrl @@ -341,6 +345,7 @@ export const listGitLabGroups = async ({ appConnection.credentials.refreshToken, appConnection.id, appConnection.orgId, + appConnection.projectId, appConnectionDAL, kmsService, appConnection.credentials.instanceUrl diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts index 46a59bcec..3a79e2f8e 100644 --- a/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts @@ -3,6 +3,7 @@ import https from "https"; import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; @@ -144,7 +145,9 @@ export const getHCVaultAccessToken = async ( export const validateHCVaultConnectionCredentials = async ( connection: THCVaultConnection, - gatewayService: Pick + gatewayService: Pick, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _gatewayV2Service: Pick ) => { const instanceUrl = await getHCVaultInstanceUrl(connection); diff --git a/backend/src/services/app-connection/heroku/heroku-connection-fns.ts b/backend/src/services/app-connection/heroku/heroku-connection-fns.ts index 5a8533c83..adbc5cd2b 100644 --- a/backend/src/services/app-connection/heroku/heroku-connection-fns.ts +++ b/backend/src/services/app-connection/heroku/heroku-connection-fns.ts @@ -36,6 +36,7 @@ export const refreshHerokuToken = async ( refreshToken: string, appId: string, orgId: string, + projectId: string | null | undefined, appConnectionDAL: Pick, kmsService: Pick ): Promise => { @@ -64,7 +65,8 @@ export const refreshHerokuToken = async ( expiresAt: new Date(Date.now() + data.expires_in * 1000 - 60000) }, orgId, - kmsService + kmsService, + projectId }); await appConnectionDAL.updateById(appId, { encryptedCredentials }); @@ -186,6 +188,7 @@ export const listHerokuApps = async ({ appConnection.credentials.refreshToken, appConnection.id, appConnection.orgId, + appConnection.projectId, appConnectionDAL, kmsService ); diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts index b9425d7be..ca50bae8a 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts @@ -2,12 +2,14 @@ import knex, { Knex } from "knex"; import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TSqlCredentialsRotationGeneratedCredentials, TSqlCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { TAppConnectionRaw, TSqlConnection } from "@app/services/app-connection/app-connection-types"; @@ -56,7 +58,7 @@ const getConnectionConfig = ({ ? { rejectUnauthorized: sslRejectUnauthorized, ca: sslCertificate, - servername: host + serverName: host } : false }; @@ -90,7 +92,7 @@ export const getSqlConnectionClient = async (appConnection: Pick( config: TSqlConnectionConfig, gatewayService: Pick, + gatewayV2Service: Pick, operation: (client: Knex) => Promise ): Promise => { const { credentials, app, gatewayId } = config; - if (gatewayId && gatewayService) { + if (gatewayId && gatewayService && gatewayV2Service) { const [targetHost] = await verifyHostInputValidity(credentials.host, true); + const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ + gatewayId, + targetHost, + targetPort: credentials.port + }); + + if (platformConnectionDetails) { + return withGatewayV2Proxy( + async (proxyPort) => { + const client = knex({ + client: SQL_CONNECTION_CLIENT_MAP[app], + connection: { + database: credentials.database, + port: proxyPort, + host: "localhost", + user: credentials.username, + password: credentials.password, + connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, + ...getConnectionConfig({ app, credentials }) + } + }); + try { + return await operation(client); + } finally { + await client.destroy(); + } + }, + { + protocol: GatewayProxyProtocol.Tcp, + relayHost: platformConnectionDetails.relayHost, + gateway: platformConnectionDetails.gateway, + relay: platformConnectionDetails.relay + } + ); + } + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); @@ -135,7 +174,7 @@ export const executeWithPotentialGateway = async ( }, { protocol: GatewayProxyProtocol.Tcp, - targetHost, + targetHost: app === AppConnection.Postgres ? targetHost : credentials.host, targetPort: credentials.port, relayHost, relayPort: Number(relayPort), @@ -161,10 +200,11 @@ export const executeWithPotentialGateway = async ( export const validateSqlConnectionCredentials = async ( config: TSqlConnectionConfig, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { try { - await executeWithPotentialGateway(config, gatewayService, async (client) => { + await executeWithPotentialGateway(config, gatewayService, gatewayV2Service, async (client) => { await client.raw(config.app === AppConnection.OracleDB ? `SELECT 1 FROM DUAL` : `Select 1`); }); return config.credentials; @@ -191,14 +231,15 @@ export const SQL_CONNECTION_ALTER_LOGIN_STATEMENT: Record< export const transferSqlConnectionCredentialsToPlatform = async ( config: TSqlConnectionConfig, callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { const { credentials, app } = config; const newPassword = alphaNumericNanoId(32); try { - return await executeWithPotentialGateway(config, gatewayService, (client) => { + return await executeWithPotentialGateway(config, gatewayService, gatewayV2Service, (client) => { return client.transaction(async (tx) => { await tx.raw( ...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword }) diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index c309b3998..613aa0766 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -36,6 +36,12 @@ export const getTokenConfig = (tokenType: TokenType) => { const expiresAt = new Date(new Date().getTime() + 86400000); return { token, triesLeft, expiresAt }; } + case TokenType.TOKEN_EMAIL_CHANGE_OTP: { + const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1)); + const triesLeft = 1; + const expiresAt = new Date(new Date().getTime() + 600000); + return { token, triesLeft, expiresAt }; + } case TokenType.TOKEN_EMAIL_MFA: { // generate random 6-digit code const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1)); @@ -75,7 +81,7 @@ export const getTokenConfig = (tokenType: TokenType) => { }; export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => { - const createTokenForUser = async ({ type, userId, orgId, aliasId }: TCreateTokenForUserDTO) => { + const createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => { const { token, ...tkCfg } = getTokenConfig(type); const appCfg = getConfig(); const tokenHash = await crypto.hashing().createHash(token, appCfg.SALT_ROUNDS); @@ -89,7 +95,8 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu userId, orgId, triesLeft: tkCfg?.triesLeft, - aliasId + aliasId, + payload }, tx ); diff --git a/backend/src/services/auth-token/auth-token-types.ts b/backend/src/services/auth-token/auth-token-types.ts index 7deb719a9..3255fbbbc 100644 --- a/backend/src/services/auth-token/auth-token-types.ts +++ b/backend/src/services/auth-token/auth-token-types.ts @@ -3,6 +3,7 @@ import { ProjectMembershipRole } from "@app/db/schemas"; export enum TokenType { TOKEN_EMAIL_CONFIRMATION = "emailConfirmation", TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified + TOKEN_EMAIL_CHANGE_OTP = "emailChangeOtp", TOKEN_EMAIL_MFA = "emailMfa", TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation", TOKEN_EMAIL_PASSWORD_RESET = "passwordReset", @@ -15,6 +16,7 @@ export type TCreateTokenForUserDTO = { userId: string; orgId?: string; aliasId?: string; + payload?: string; }; export type TCreateOrgInviteTokenDTO = { diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts index 830378ca8..b725e5584 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -42,10 +42,10 @@ import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/ type TAcmeCertificateAuthorityFnsDeps = { appConnectionDAL: Pick; - appConnectionService: Pick; + appConnectionService: Pick; certificateAuthorityDAL: Pick< TCertificateAuthorityDALFactory, - "create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" + "create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById" >; externalCertificateAuthorityDAL: Pick; certificateDAL: Pick; @@ -152,7 +152,11 @@ export const AcmeCertificateAuthorityFns = ({ } // validates permission to connect - await appConnectionService.connectAppConnectionById(appConnection.app as AppConnection, dnsAppConnectionId, actor); + await appConnectionService.validateAppConnectionUsageById( + appConnection.app as AppConnection, + { connectionId: dnsAppConnectionId, projectId }, + actor + ); const caEntity = await certificateAuthorityDAL.transaction(async (tx) => { try { @@ -242,10 +246,16 @@ export const AcmeCertificateAuthorityFns = ({ }); } + const ca = await certificateAuthorityDAL.findById(id); + + if (!ca) { + throw new NotFoundError({ message: `Could not find Certificate Authority with ID "${id}"` }); + } + // validates permission to connect - await appConnectionService.connectAppConnectionById( + await appConnectionService.validateAppConnectionUsageById( appConnection.app as AppConnection, - dnsAppConnectionId, + { connectionId: dnsAppConnectionId, projectId: ca.projectId }, actor ); diff --git a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts index 0e2619a27..25c5590eb 100644 --- a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts @@ -41,10 +41,10 @@ import { type TAzureAdCsCertificateAuthorityFnsDeps = { appConnectionDAL: Pick; - appConnectionService: Pick; + appConnectionService: Pick; certificateAuthorityDAL: Pick< TCertificateAuthorityDALFactory, - "create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" + "create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById" >; externalCertificateAuthorityDAL: Pick; certificateDAL: Pick; @@ -621,9 +621,9 @@ export const AzureAdCsCertificateAuthorityFns = ({ }); } - await appConnectionService.connectAppConnectionById( + await appConnectionService.validateAppConnectionUsageById( appConnection.app as AppConnection, - azureAdcsConnectionId, + { connectionId: azureAdcsConnectionId, projectId }, actor ); @@ -705,9 +705,15 @@ export const AzureAdCsCertificateAuthorityFns = ({ }); } - await appConnectionService.connectAppConnectionById( + const ca = await certificateAuthorityDAL.findById(id); + + if (!ca) { + throw new NotFoundError({ message: `Could not find Certificate Authority with ID "${id}"` }); + } + + await appConnectionService.validateAppConnectionUsageById( appConnection.app as AppConnection, - azureAdcsConnectionId, + { connectionId: azureAdcsConnectionId, projectId: ca.projectId }, actor ); diff --git a/backend/src/services/certificate-authority/certificate-authority-queue.ts b/backend/src/services/certificate-authority/certificate-authority-queue.ts index 0e015da03..21f7b71e6 100644 --- a/backend/src/services/certificate-authority/certificate-authority-queue.ts +++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts @@ -35,7 +35,7 @@ import { type TCertificateAuthorityQueueFactoryDep = { certificateAuthorityDAL: TCertificateAuthorityDALFactory; appConnectionDAL: Pick; - appConnectionService: Pick; + appConnectionService: Pick; externalCertificateAuthorityDAL: Pick; keyStore: Pick; certificateAuthorityCrlDAL: TCertificateAuthorityCrlDALFactory; diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 6cf55fc52..02c5a488a 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -43,7 +43,7 @@ import { TCreateInternalCertificateAuthorityDTO } from "./internal/internal-cert type TCertificateAuthorityServiceFactoryDep = { appConnectionDAL: Pick; - appConnectionService: Pick; + appConnectionService: Pick; certificateAuthorityDAL: Pick< TCertificateAuthorityDALFactory, | "transaction" diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index 9fed186bc..ab89ea996 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -1190,7 +1190,9 @@ export const internalCertificateAuthorityServiceFactory = ({ }); } - collectionId = certificateTemplate.pkiCollectionId as string; + if (!collectionId) { + collectionId = certificateTemplate.pkiCollectionId as string; + } ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId); } diff --git a/backend/src/services/certificate/certificate-fns.ts b/backend/src/services/certificate/certificate-fns.ts index eee220ce9..b2bc4df41 100644 --- a/backend/src/services/certificate/certificate-fns.ts +++ b/backend/src/services/certificate/certificate-fns.ts @@ -52,6 +52,9 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[]) .join("\n") .trim(); +export const prependCertToPemChain = (cert: x509.X509Certificate, pemChain: string) => + `${cert.toString("pem")}\n${pemChain}`; + export const splitPemChain = (pemText: string) => { const re2Pattern = new RE2("-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----", "g"); diff --git a/backend/src/services/external-migration/external-migration-fns/import.ts b/backend/src/services/external-migration/external-migration-fns/import.ts index 5728bf1c0..62888c5bf 100644 --- a/backend/src/services/external-migration/external-migration-fns/import.ts +++ b/backend/src/services/external-migration/external-migration-fns/import.ts @@ -55,7 +55,7 @@ export const importDataIntoInfisicalFn = async ({ actorId, actorOrgId, actorAuthMethod, - workspaceName: project.name, + projectName: project.name, createDefaultEnvs: false, tx }) diff --git a/backend/src/services/external-migration/external-migration-fns/vault.ts b/backend/src/services/external-migration/external-migration-fns/vault.ts index f5f57aa1b..ca6d2b0b0 100644 --- a/backend/src/services/external-migration/external-migration-fns/vault.ts +++ b/backend/src/services/external-migration/external-migration-fns/vault.ts @@ -408,19 +408,123 @@ export const transformToInfisicalFormatNamespaceToProjects = ( }; }; +export const transformToInfisicalFormatKeyVaultToProjectsCustomC1 = (vaultData: VaultData[]): InfisicalImportData => { + const projects: Array<{ name: string; id: string }> = []; + const environments: Array<{ name: string; id: string; projectId: string; envParentId?: string }> = []; + const folders: Array<{ id: string; name: string; environmentId: string; parentFolderId?: string }> = []; + const secrets: Array<{ id: string; name: string; environmentId: string; value: string; folderId?: string }> = []; + + // track created entities to avoid duplicates + const projectMap = new Map(); // team name -> projectId + const environmentMap = new Map(); // team-name:envName -> environmentId + const folderMap = new Map(); // team-name:envName:folderPath -> folderId + + for (const data of vaultData) { + const { path, secretData } = data; + + const pathParts = path.split("/").filter(Boolean); + if (pathParts.length < 2) { + // eslint-disable-next-line no-continue + continue; + } + + // first level: environment (dev, prod, staging, etc.) + const environmentName = pathParts[0]; + // second level: team name (team1, team2, etc.) + const teamName = pathParts[1]; + // remaining parts: folder structure + const folderParts = pathParts.slice(2); + + // create project (team) if if doesn't exist + if (!projectMap.has(teamName)) { + const projectId = uuidv4(); + projectMap.set(teamName, projectId); + projects.push({ + name: teamName, + id: projectId + }); + } + const projectId = projectMap.get(teamName)!; + + // create environment (dev, prod, etc.) for team + const envKey = `${teamName}:${environmentName}`; + if (!environmentMap.has(envKey)) { + const environmentId = uuidv4(); + environmentMap.set(envKey, environmentId); + environments.push({ + name: environmentName, + id: environmentId, + projectId + }); + } + const environmentId = environmentMap.get(envKey)!; + + // create folder structure for path segments + let currentFolderId: string | undefined; + let currentPath = ""; + + for (const folderName of folderParts) { + currentPath = currentPath ? `${currentPath}/${folderName}` : folderName; + const folderKey = `${teamName}:${environmentName}:${currentPath}`; + + if (!folderMap.has(folderKey)) { + const folderId = uuidv4(); + folderMap.set(folderKey, folderId); + folders.push({ + id: folderId, + name: folderName, + environmentId, + parentFolderId: currentFolderId || environmentId + }); + currentFolderId = folderId; + } else { + currentFolderId = folderMap.get(folderKey)!; + } + } + + for (const [key, value] of Object.entries(secretData)) { + secrets.push({ + id: uuidv4(), + name: key, + environmentId, + value: String(value), + folderId: currentFolderId + }); + } + } + + return { + projects, + environments, + folders, + secrets + }; +}; + +// refer to internal doc for more details on which ID's belong to which orgs. +// when its a custom migration, then it doesn't matter which mapping type is used (as of now). +export const vaultMigrationTransformMappings: Record< + string, + (vaultData: VaultData[], mappingType: VaultMappingType) => InfisicalImportData +> = { + "68c57ab3-cea5-41fc-ae38-e156b10c14d2": transformToInfisicalFormatKeyVaultToProjectsCustomC1 +} as const; + export const importVaultDataFn = async ( { vaultAccessToken, vaultNamespace, vaultUrl, mappingType, - gatewayId + gatewayId, + orgId }: { vaultAccessToken: string; vaultNamespace?: string; vaultUrl: string; mappingType: VaultMappingType; gatewayId?: string; + orgId: string; }, { gatewayService }: { gatewayService: Pick } ) => { @@ -432,6 +536,25 @@ export const importVaultDataFn = async ( }); } + let transformFn: (vaultData: VaultData[], mappingType: VaultMappingType) => InfisicalImportData; + + if (mappingType === VaultMappingType.Custom) { + transformFn = vaultMigrationTransformMappings[orgId]; + + if (!transformFn) { + throw new BadRequestError({ + message: "Please contact our sales team to enable custom vault migrations." + }); + } + } else { + transformFn = transformToInfisicalFormatNamespaceToProjects; + } + + logger.info( + { orgId, mappingType }, + `[importVaultDataFn]: Running ${orgId in vaultMigrationTransformMappings ? "custom" : "default"} transform` + ); + const vaultApi = vaultFactory(gatewayService); const vaultData = await vaultApi.collectVaultData({ @@ -441,7 +564,5 @@ export const importVaultDataFn = async ( gatewayId }); - const infisicalData = transformToInfisicalFormatNamespaceToProjects(vaultData, mappingType); - - return infisicalData; + return transformFn(vaultData, mappingType); }; diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index f1047d0ee..73fac00b9 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -5,9 +5,20 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { TUserDALFactory } from "../user/user-dal"; -import { decryptEnvKeyDataFn, importVaultDataFn, parseEnvKeyDataFn } from "./external-migration-fns"; +import { + decryptEnvKeyDataFn, + importVaultDataFn, + parseEnvKeyDataFn, + vaultMigrationTransformMappings +} from "./external-migration-fns"; import { TExternalMigrationQueueFactory } from "./external-migration-queue"; -import { ExternalPlatforms, TImportEnvKeyDataDTO, TImportVaultDataDTO } from "./external-migration-types"; +import { + ExternalMigrationProviders, + ExternalPlatforms, + THasCustomVaultMigrationDTO, + TImportEnvKeyDataDTO, + TImportVaultDataDTO +} from "./external-migration-types"; type TExternalMigrationServiceFactoryDep = { permissionService: TPermissionServiceFactory; @@ -101,7 +112,8 @@ export const externalMigrationServiceFactory = ({ vaultNamespace, vaultUrl, mappingType, - gatewayId + gatewayId, + orgId: actorOrgId }, { gatewayService @@ -127,8 +139,37 @@ export const externalMigrationServiceFactory = ({ }); }; + const hasCustomVaultMigration = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + provider + }: THasCustomVaultMigrationDTO) => { + const { membership } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + if (membership.role !== OrgMembershipRole.Admin) { + throw new ForbiddenRequestError({ message: "Only admins can check custom migration status" }); + } + + if (provider !== ExternalMigrationProviders.Vault) { + throw new BadRequestError({ + message: "Invalid provider. Vault is the only supported provider for custom migrations." + }); + } + + return actorOrgId in vaultMigrationTransformMappings; + }; + return { importEnvKeyData, - importVaultData + importVaultData, + hasCustomVaultMigration }; }; diff --git a/backend/src/services/external-migration/external-migration-types.ts b/backend/src/services/external-migration/external-migration-types.ts index ac8ff44e2..804444172 100644 --- a/backend/src/services/external-migration/external-migration-types.ts +++ b/backend/src/services/external-migration/external-migration-types.ts @@ -4,7 +4,8 @@ import { ActorAuthMethod, ActorType } from "../auth/auth-type"; export enum VaultMappingType { Namespace = "namespace", - KeyVault = "key-vault" + KeyVault = "key-vault", + Custom = "custom" } export type InfisicalImportData = { @@ -26,6 +27,10 @@ export type TImportEnvKeyDataDTO = { encryptedJson: { nonce: string; data: string }; } & Omit; +export type THasCustomVaultMigrationDTO = { + provider: ExternalMigrationProviders; +} & Omit; + export type TImportVaultDataDTO = { vaultAccessToken: string; vaultNamespace?: string; @@ -111,3 +116,8 @@ export enum ExternalPlatforms { EnvKey = "EnvKey", Vault = "Vault" } + +export enum ExternalMigrationProviders { + Vault = "vault", + EnvKey = "env-key" +} diff --git a/backend/src/services/folder-commit/folder-commit-service.test.ts b/backend/src/services/folder-commit/folder-commit-service.test.ts index 28d603829..0a73d6e0c 100644 --- a/backend/src/services/folder-commit/folder-commit-service.test.ts +++ b/backend/src/services/folder-commit/folder-commit-service.test.ts @@ -661,7 +661,7 @@ describe("folderCommitServiceFactory", () => { // Assert expect(mockFolderCommitDAL.create).toHaveBeenCalled(); - expect(mockSecretV2BridgeDAL.invalidateSecretCacheByProjectId).toHaveBeenCalledWith(projectId); + expect(mockSecretV2BridgeDAL.invalidateSecretCacheByProjectId).toHaveBeenCalledWith(projectId, {}); // Check that we got the right counts expect(result.totalChanges).toEqual(2); diff --git a/backend/src/services/folder-commit/folder-commit-service.ts b/backend/src/services/folder-commit/folder-commit-service.ts index 470edbbba..e4c151ff1 100644 --- a/backend/src/services/folder-commit/folder-commit-service.ts +++ b/backend/src/services/folder-commit/folder-commit-service.ts @@ -1386,7 +1386,7 @@ export const folderCommitServiceFactory = ({ ); // Invalidate cache to reflect the changes - await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId, tx); return { secretChangesCount: secretChanges.length, diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 9584b122a..bc231c6d6 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -6,6 +6,8 @@ import RE2 from "re2"; import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2DALFactory } from "@app/ee/services/gateway-v2/gateway-v2-dal"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionGatewayActions, @@ -21,6 +23,7 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; @@ -54,11 +57,15 @@ type TIdentityKubernetesAuthServiceFactoryDep = { licenseService: Pick; kmsService: Pick; gatewayService: TGatewayServiceFactory; + gatewayV2Service: TGatewayV2ServiceFactory; gatewayDAL: Pick; + gatewayV2DAL: Pick; }; export type TIdentityKubernetesAuthServiceFactory = ReturnType; +const GATEWAY_AUTH_DEFAULT_HOST = "https://kubernetes.default.svc.cluster.local"; + export const identityKubernetesAuthServiceFactory = ({ identityKubernetesAuthDAL, identityOrgMembershipDAL, @@ -66,7 +73,9 @@ export const identityKubernetesAuthServiceFactory = ({ permissionService, licenseService, gatewayService, + gatewayV2Service, gatewayDAL, + gatewayV2DAL, kmsService }: TIdentityKubernetesAuthServiceFactoryDep) => { const $gatewayProxyWrapper = async ( @@ -79,6 +88,42 @@ export const identityKubernetesAuthServiceFactory = ({ }, gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise ): Promise => { + const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ + gatewayId: inputs.gatewayId, + targetHost: inputs.targetHost ?? GATEWAY_AUTH_DEFAULT_HOST, + targetPort: inputs.targetPort ?? 443 + }); + + if (gatewayV2ConnectionDetails) { + let httpsAgent: https.Agent | undefined; + if (!inputs.reviewTokenThroughGateway) { + httpsAgent = new https.Agent({ + ca: inputs.caCert, + rejectUnauthorized: Boolean(inputs.caCert) + }); + } + + const callbackResult = await withGatewayV2Proxy( + async (port) => { + const res = await gatewayCallback( + inputs.reviewTokenThroughGateway ? "http://localhost" : "https://localhost", + port, + httpsAgent + ); + return res; + }, + { + protocol: inputs.reviewTokenThroughGateway ? GatewayProxyProtocol.Http : GatewayProxyProtocol.Tcp, + relayHost: gatewayV2ConnectionDetails.relayHost, + gateway: gatewayV2ConnectionDetails.gateway, + relay: gatewayV2ConnectionDetails.relay, + httpsAgent + } + ); + + return callbackResult; + } + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); @@ -277,7 +322,7 @@ export const identityKubernetesAuthServiceFactory = ({ let data: TCreateTokenReviewResponse | undefined; if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) { - if (!identityKubernetesAuth.gatewayId) { + if (!identityKubernetesAuth.gatewayId && !identityKubernetesAuth.gatewayV2Id) { throw new BadRequestError({ message: "Gateway ID is required when token review mode is set to Gateway" }); @@ -285,7 +330,7 @@ export const identityKubernetesAuthServiceFactory = ({ data = await $gatewayProxyWrapper( { - gatewayId: identityKubernetesAuth.gatewayId, + gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string, reviewTokenThroughGateway: true }, tokenReviewCallbackThroughGateway @@ -304,17 +349,18 @@ export const identityKubernetesAuthServiceFactory = ({ const [k8sHost, k8sPort] = kubernetesHost.split(":"); - data = identityKubernetesAuth.gatewayId - ? await $gatewayProxyWrapper( - { - gatewayId: identityKubernetesAuth.gatewayId, - targetHost: k8sHost, - targetPort: k8sPort ? Number(k8sPort) : 443, - reviewTokenThroughGateway: false - }, - tokenReviewCallbackRaw - ) - : await tokenReviewCallbackRaw(); + data = + identityKubernetesAuth.gatewayId || identityKubernetesAuth.gatewayV2Id + ? await $gatewayProxyWrapper( + { + gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string, + targetHost: k8sHost, + targetPort: k8sPort ? Number(k8sPort) : 443, + reviewTokenThroughGateway: false + }, + tokenReviewCallbackRaw + ) + : await tokenReviewCallbackRaw(); } else { throw new BadRequestError({ message: `Invalid token review mode: ${identityKubernetesAuth.tokenReviewMode}` @@ -490,14 +536,20 @@ export const identityKubernetesAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); + let isGatewayV1 = true; if (gatewayId) { const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); - if (!gateway) { + const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); + if (!gateway && !gatewayV2) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found` }); } + if (!gateway) { + isGatewayV1 = false; + } + const { permission: orgPermission } = await permissionService.getOrgPermission( actor, actorId, @@ -528,7 +580,8 @@ export const identityKubernetesAuthServiceFactory = ({ accessTokenMaxTTL, accessTokenTTL, accessTokenNumUsesLimit, - gatewayId, + gatewayId: isGatewayV1 ? gatewayId : null, + gatewayV2Id: isGatewayV1 ? null : gatewayId, accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), encryptedKubernetesTokenReviewerJwt: tokenReviewerJwt ? encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob @@ -608,14 +661,21 @@ export const identityKubernetesAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); + let isGatewayV1 = true; if (gatewayId) { const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); - if (!gateway) { + const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); + + if (!gateway && !gatewayV2) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found` }); } + if (!gateway) { + isGatewayV1 = false; + } + const { permission: orgPermission } = await permissionService.getOrgPermission( actor, actorId, @@ -629,13 +689,18 @@ export const identityKubernetesAuthServiceFactory = ({ ); } + const shouldUpdateGatewayId = Boolean(gatewayId); + const gatewayIdValue = isGatewayV1 ? gatewayId : null; + const gatewayV2IdValue = isGatewayV1 ? null : gatewayId; + const updateQuery: TIdentityKubernetesAuthsUpdate = { kubernetesHost, tokenReviewMode, allowedNamespaces, allowedNames, allowedAudience, - gatewayId, + gatewayId: shouldUpdateGatewayId ? gatewayIdValue : undefined, + gatewayV2Id: shouldUpdateGatewayId ? gatewayV2IdValue : undefined, accessTokenMaxTTL, accessTokenTTL, accessTokenNumUsesLimit, @@ -730,7 +795,13 @@ export const identityKubernetesAuthServiceFactory = ({ }).toString(); } - return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId }; + return { + ...identityKubernetesAuth, + caCert, + tokenReviewerJwt, + orgId: identityMembershipOrg.orgId, + gatewayId: identityKubernetesAuth.gatewayId ?? identityKubernetesAuth.gatewayV2Id + }; }; const revokeIdentityKubernetesAuth = async ({ diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index 47188e26d..46ec2f98a 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { ForbiddenError } from "@casl/ability"; +import slugify from "@sindresorhus/slugify"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TIdentityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template"; @@ -15,10 +16,18 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; +import { + BadRequestError, + NotFoundError, + PermissionBoundaryError, + RateLimitError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; +import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; @@ -32,6 +41,8 @@ import { TIdentityLdapAuthDALFactory } from "./identity-ldap-auth-dal"; import { AllowedFieldsSchema, TAttachLdapAuthDTO, + TCheckLdapAuthLockoutDTO, + TClearLdapAuthLockoutsDTO, TGetLdapAuthDTO, TLoginLdapAuthDTO, TRevokeLdapAuthDTO, @@ -50,10 +61,19 @@ type TIdentityLdapAuthServiceFactoryDep = { kmsService: TKmsServiceFactory; identityDAL: TIdentityDALFactory; identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory; + keyStore: Pick< + TKeyStoreFactory, + "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock" + >; }; export type TIdentityLdapAuthServiceFactory = ReturnType; +type LockoutObject = { + lockedOut: boolean; + failedAttempts: number; +}; + export const identityLdapAuthServiceFactory = ({ identityAccessTokenDAL, identityDAL, @@ -62,7 +82,8 @@ export const identityLdapAuthServiceFactory = ({ licenseService, permissionService, kmsService, - identityAuthTemplateDAL + identityAuthTemplateDAL, + keyStore }: TIdentityLdapAuthServiceFactoryDep) => { const getLdapConfig = async (identityId: string) => { const identity = await identityDAL.findOne({ id: identityId }); @@ -126,13 +147,17 @@ export const identityLdapAuthServiceFactory = ({ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) { - throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + throw new UnauthorizedError({ + message: "Invalid credentials" + }); } const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId }); if (!identityLdapAuth) { - throw new NotFoundError({ message: `Failed to find LDAP auth for identity with ID ${identityId}` }); + throw new UnauthorizedError({ + message: "Invalid credentials" + }); } const plan = await licenseService.getPlan(identityMembershipOrg.orgId); @@ -204,7 +229,11 @@ export const identityLdapAuthServiceFactory = ({ actor, actorOrgId, isActorSuperAdmin, - allowedFields + allowedFields, + lockoutEnabled, + lockoutThreshold, + lockoutDurationSeconds, + lockoutCounterResetSeconds }: TAttachLdapAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); @@ -337,7 +366,11 @@ export const identityLdapAuthServiceFactory = ({ accessTokenNumUsesLimit, accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined, - templateId + templateId, + lockoutEnabled, + lockoutThreshold, + lockoutDurationSeconds, + lockoutCounterResetSeconds }, tx ); @@ -363,7 +396,11 @@ export const identityLdapAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + lockoutEnabled, + lockoutThreshold, + lockoutDurationSeconds, + lockoutCounterResetSeconds }: TUpdateLdapAuthDTO) => { const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -511,7 +548,11 @@ export const identityLdapAuthServiceFactory = ({ accessTokenNumUsesLimit, accessTokenTrustedIps: reformattedAccessTokenTrustedIps ? JSON.stringify(reformattedAccessTokenTrustedIps) - : undefined + : undefined, + lockoutEnabled, + lockoutThreshold, + lockoutDurationSeconds, + lockoutCounterResetSeconds }); return { ...updatedLdapAuth, orgId: identityMembershipOrg.orgId }; @@ -611,12 +652,123 @@ export const identityLdapAuthServiceFactory = ({ return revokedIdentityLdapAuth; }; + const withLdapLockout = async ( + { identityId, username }: TCheckLdapAuthLockoutDTO, + authFn: () => Promise + ): Promise => { + const usernameSlug = slugify(username.trim().toLowerCase()); + + const LOCKOUT_KEY = `lockout:identity:${identityId}:${IdentityAuthMethod.LDAP_AUTH}:${usernameSlug}`; + + let lock: Awaited>; + try { + lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 3000, { + retryCount: 3, + retryDelay: 1500, + retryJitter: 100 + }); + } catch (e) { + logger.info( + `identity login failed to acquire lock [identityId=${identityId}] [authMethod=${IdentityAuthMethod.LDAP_AUTH}]` + ); + throw new RateLimitError({ message: "Failed to acquire lock: rate limit exceeded" }); + } + + try { + const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY); + if (lockoutRaw) { + const lockout = JSON.parse(lockoutRaw) as LockoutObject; + if (lockout.lockedOut) { + throw new UnauthorizedError({ + message: "This identity auth method is temporarily locked, please try again later" + }); + } + } + + const result = await authFn(); + + await keyStore.deleteItem(LOCKOUT_KEY); + + return result; + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + if ((error as any).status === 401) { + const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId }); + if (!identityLdapAuth) { + throw new UnauthorizedError({ message: "Invalid credentials" }); + } + + if (identityLdapAuth.lockoutEnabled) { + let lockout: LockoutObject = { + lockedOut: false, + failedAttempts: 0 + }; + + const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY); + if (lockoutRaw) { + lockout = JSON.parse(lockoutRaw) as LockoutObject; + } + + lockout.failedAttempts += 1; + if (lockout.failedAttempts >= identityLdapAuth.lockoutThreshold) { + lockout.lockedOut = true; + } + + await keyStore.setItemWithExpiry( + LOCKOUT_KEY, + lockout.lockedOut ? identityLdapAuth.lockoutDurationSeconds : identityLdapAuth.lockoutCounterResetSeconds, + JSON.stringify(lockout) + ); + } + + throw new UnauthorizedError({ message: "Invalid credentials" }); + } + throw error; + } finally { + await lock.release(); + } + }; + + const clearLdapAuthLockouts = async ({ + identityId, + actorId, + actor, + actorOrgId, + actorAuthMethod + }: TClearLdapAuthLockoutsDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have ldap auth" + }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const deleted = await keyStore.deleteItems({ + pattern: `lockout:identity:${identityId}:${IdentityAuthMethod.LDAP_AUTH}:*` + }); + + return { deleted, identityId, orgId: identityMembershipOrg.orgId }; + }; + return { attachLdapAuth, getLdapConfig, updateLdapAuth, login, revokeIdentityLdapAuth, - getLdapAuth + getLdapAuth, + withLdapLockout, + clearLdapAuthLockouts }; }; diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts index 8629763bb..a4aea7573 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts @@ -27,6 +27,10 @@ export type TAttachLdapAuthDTO = { accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; isActorSuperAdmin?: boolean; + lockoutEnabled: boolean; + lockoutThreshold: number; + lockoutDurationSeconds: number; + lockoutCounterResetSeconds: number; } & Omit; export type TUpdateLdapAuthDTO = { @@ -43,6 +47,10 @@ export type TUpdateLdapAuthDTO = { accessTokenMaxTTL?: number; accessTokenNumUsesLimit?: number; accessTokenTrustedIps?: { ipAddress: string }[]; + lockoutEnabled?: boolean; + lockoutThreshold?: number; + lockoutDurationSeconds?: number; + lockoutCounterResetSeconds?: number; } & Omit; export type TGetLdapAuthDTO = { @@ -56,3 +64,12 @@ export type TLoginLdapAuthDTO = { export type TRevokeLdapAuthDTO = { identityId: string; } & Omit; + +export type TClearLdapAuthLockoutsDTO = { + identityId: string; +} & Omit; + +export type TCheckLdapAuthLockoutDTO = { + identityId: string; + username: string; +}; diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 597747683..8aec16371 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -84,18 +84,20 @@ export const identityUaServiceFactory = ({ const LOCKOUT_KEY = `lockout:identity:${identityUa.identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:${clientId}`; - let lock: Awaited>; - try { - lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 500, { - retryCount: 3, - retryDelay: 300, - retryJitter: 100 - }); - } catch (e) { - logger.info( - `identity login failed to acquire lock [identityId=${identityUa.identityId}] [authMethod=${IdentityAuthMethod.UNIVERSAL_AUTH}]` - ); - throw new RateLimitError({ message: "Rate limit exceeded" }); + let lock: Awaited> | undefined; + if (identityUa.lockoutEnabled) { + try { + lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 500, { + retryCount: 3, + retryDelay: 300, + retryJitter: 100 + }); + } catch (e) { + logger.info( + `identity login failed to acquire lock [identityId=${identityUa.identityId}] [authMethod=${IdentityAuthMethod.UNIVERSAL_AUTH}]` + ); + throw new RateLimitError({ message: "Failed to acquire lock: rate limit exceeded" }); + } } try { @@ -257,7 +259,7 @@ export const identityUaServiceFactory = ({ ...accessTokenTTLParams }; } finally { - await lock.release(); + if (lock) await lock.release(); } }; diff --git a/backend/src/services/identity/identity-dal.ts b/backend/src/services/identity/identity-dal.ts index 363412493..7bc797600 100644 --- a/backend/src/services/identity/identity-dal.ts +++ b/backend/src/services/identity/identity-dal.ts @@ -25,7 +25,7 @@ export const identityDALFactory = (db: TDbClient) => { } as const; const tableName = authMethodToTableName[authMethod]; if (!tableName) return; - const data = await db(tableName).where({ identityId }).first(); + const data = await db.replicaNode()(tableName).where({ identityId }).first(); if (!data) return; return data.accessTokenTrustedIps; }; diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 969d00331..f216d6483 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -33,7 +33,7 @@ type TIdentityServiceFactoryDep = { identityProjectDAL: Pick; permissionService: Pick; licenseService: Pick; - keyStore: Pick; + keyStore: Pick; }; export type TIdentityServiceFactory = ReturnType; @@ -261,12 +261,18 @@ export const identityServiceFactory = ({ const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${id}:*`); const activeLockoutAuthMethods = new Set(); - activeLockouts.forEach((key) => { + for await (const key of activeLockouts) { const parts = key.split(":"); if (parts.length > 3) { - activeLockoutAuthMethods.add(parts[3]); + const lockoutRaw = await keyStore.getItem(key); + if (lockoutRaw) { + const lockout = JSON.parse(lockoutRaw) as { lockedOut: boolean }; + if (lockout.lockedOut) { + activeLockoutAuthMethods.add(parts[3]); + } + } } - }); + } return { ...identity, diff --git a/backend/src/services/integration-auth/integration-auth-dal.ts b/backend/src/services/integration-auth/integration-auth-dal.ts index 7a56afcbb..d3ccf610b 100644 --- a/backend/src/services/integration-auth/integration-auth-dal.ts +++ b/backend/src/services/integration-auth/integration-auth-dal.ts @@ -30,7 +30,7 @@ export const integrationAuthDALFactory = (db: TDbClient) => { const getByOrg = async (orgId: string, tx?: Knex) => { try { - const integrationAuths = await (tx || db)(TableName.IntegrationAuth) + const integrationAuths = await (tx || db.replicaNode())(TableName.IntegrationAuth) .join(TableName.Project, `${TableName.Project}.id`, `${TableName.IntegrationAuth}.projectId`) .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) .where(`${TableName.Organization}.id`, "=", orgId) diff --git a/backend/src/services/integration-auth/integration-delete-secret.ts b/backend/src/services/integration-auth/integration-delete-secret.ts index 2efec7a47..eaad08df8 100644 --- a/backend/src/services/integration-auth/integration-delete-secret.ts +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -39,7 +39,7 @@ const getIntegrationSecretsV2 = async ( }, secretV2BridgeDAL: Pick, folderDAL: Pick, - secretImportDAL: Pick + secretImportDAL: Pick ) => { const content: Record = {}; if (dto.depth > MAX_SYNC_SECRET_DEPTH) { @@ -300,7 +300,7 @@ export const deleteIntegrationSecrets = async ({ projectBotService: Pick; secretV2BridgeDAL: Pick; folderDAL: Pick; - secretImportDAL: Pick; + secretImportDAL: Pick; secretDAL: Pick; kmsService: Pick; }) => { diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 2ef8615eb..463ca12e3 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -40,7 +40,7 @@ type TIntegrationServiceFactoryDep = { projectBotService: TProjectBotServiceFactory; secretQueueService: Pick; secretV2BridgeDAL: Pick; - secretImportDAL: Pick; + secretImportDAL: Pick; kmsService: Pick; secretDAL: Pick; }; diff --git a/backend/src/services/kms/kms-root-config-dal.ts b/backend/src/services/kms/kms-root-config-dal.ts index 31826b79d..1b9b9a230 100644 --- a/backend/src/services/kms/kms-root-config-dal.ts +++ b/backend/src/services/kms/kms-root-config-dal.ts @@ -12,7 +12,7 @@ export const kmsRootConfigDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const result = await (tx || db)(TableName.KmsServerRootConfig) + const result = await (tx || db?.replicaNode?.() || db)(TableName.KmsServerRootConfig) .where({ id } as never) .first("*"); return result; diff --git a/backend/src/services/notification/notification-queue.ts b/backend/src/services/notification/notification-queue.ts new file mode 100644 index 000000000..e5d89c83b --- /dev/null +++ b/backend/src/services/notification/notification-queue.ts @@ -0,0 +1,39 @@ +import { QueueJobs, TQueueServiceFactory } from "@app/queue"; + +import { TCreateUserNotificationDTO } from "./notification-types"; +import { TUserNotificationDALFactory } from "./user-notification-dal"; + +type TNotificationQueueServiceFactoryDep = { + userNotificationDAL: Pick; + queueService: TQueueServiceFactory; +}; + +export type TNotificationQueueServiceFactory = { + pushUserNotifications: (data: TCreateUserNotificationDTO[]) => Promise; +}; + +export const notificationQueueServiceFactory = async ({ + userNotificationDAL, + queueService +}: TNotificationQueueServiceFactoryDep): Promise => { + const pushUserNotifications = async (data: TCreateUserNotificationDTO[]) => { + await queueService.queuePg(QueueJobs.UserNotification, { notifications: data }); + }; + + await queueService.startPg( + QueueJobs.UserNotification, + async ([job]) => { + const { notifications } = job.data as { notifications: TCreateUserNotificationDTO[] }; + await userNotificationDAL.batchInsert(notifications); + }, + { + batchSize: 1, + workerCount: 2, + pollingIntervalSeconds: 1 + } + ); + + return { + pushUserNotifications + }; +}; diff --git a/backend/src/services/notification/notification-service.ts b/backend/src/services/notification/notification-service.ts new file mode 100644 index 000000000..ed50c3f1c --- /dev/null +++ b/backend/src/services/notification/notification-service.ts @@ -0,0 +1,81 @@ +import { NotFoundError, UnauthorizedError } from "@app/lib/errors"; + +import { TNotificationQueueServiceFactory } from "./notification-queue"; +import { TCreateUserNotificationDTO } from "./notification-types"; +import { TUserNotificationDALFactory } from "./user-notification-dal"; + +type TNotificationServiceFactoryDep = { + notificationQueue: TNotificationQueueServiceFactory; + userNotificationDAL: TUserNotificationDALFactory; +}; + +export type TNotificationServiceFactory = ReturnType; + +export const notificationServiceFactory = ({ + notificationQueue, + userNotificationDAL +}: TNotificationServiceFactoryDep) => { + const listUserNotifications = async ({ userId, orgId }: { userId: string; orgId: string }) => { + const now = new Date(); + const threeMonthsAgo = new Date(); + threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); + + const notifications = await userNotificationDAL.find({ + userId, + orgId, + startDate: threeMonthsAgo.toISOString(), + endDate: now.toISOString() + }); + + return notifications; + }; + + const createUserNotifications = async (data: TCreateUserNotificationDTO[]) => { + return notificationQueue.pushUserNotifications(data); + }; + + const deleteUserNotification = async ({ userId, notificationId }: { userId: string; notificationId: string }) => { + if (!userId) throw new UnauthorizedError({ message: "Invalid userId" }); + + const deletedNotifications = await userNotificationDAL.delete({ id: notificationId, userId }); + + if (deletedNotifications.length <= 0) throw new NotFoundError({ message: "Notification not found" }); + + return deletedNotifications[0]; + }; + + const markUserNotificationsAsRead = async ({ userId, orgId }: { userId: string; orgId: string }) => { + await userNotificationDAL.markAllNotificationsAsRead(userId, orgId); + }; + + const updateUserNotification = async ({ + userId, + notificationId, + isRead + }: { + userId: string; + notificationId: string; + isRead: boolean; + }) => { + const [updatedNotification] = await userNotificationDAL.update( + { + id: notificationId, + userId + }, + { + isRead + } + ); + + if (!updatedNotification) throw new NotFoundError({ message: "Notification not found" }); + return updatedNotification; + }; + + return { + listUserNotifications, + createUserNotifications, + deleteUserNotification, + markUserNotificationsAsRead, + updateUserNotification + }; +}; diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts new file mode 100644 index 000000000..30bc87244 --- /dev/null +++ b/backend/src/services/notification/notification-types.ts @@ -0,0 +1,15 @@ +export enum NotificationType { + ACCESS_APPROVAL_REQUEST = "access-approval-request", + ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated" +} + +export interface TCreateUserNotificationDTO { + userId: string; + // Adding an orgId will make the notification only show up when a user is in a certain org. Otherwise, it shows up in all orgs. + // Keep in mind that org-scoped links for a notification will break if orgId is missing and the user is in the wrong org + orgId?: string; + type: NotificationType; + title: string; + body?: string; + link?: string; +} diff --git a/backend/src/services/notification/user-notification-dal.ts b/backend/src/services/notification/user-notification-dal.ts new file mode 100644 index 000000000..6dcb2e3d0 --- /dev/null +++ b/backend/src/services/notification/user-notification-dal.ts @@ -0,0 +1,130 @@ +import knex from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { logger } from "@app/lib/logger"; +import { QueueName } from "@app/queue"; + +export type TUserNotificationDALFactory = ReturnType; + +const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes +const PRUNE_BATCH_SIZE = 10000; +const MAX_RETRY_ON_FAILURE = 3; + +export const userNotificationDALFactory = (db: TDbClient) => { + const notificationOrm = ormify(db, TableName.UserNotifications); + + const find = async ( + { + userId, + orgId, + startDate, + endDate, + limit = 1000, + offset = 0 + }: { + userId: string; + orgId: string; + startDate: string; + endDate: string; + limit?: number; + offset?: number; + }, + tx?: knex.Knex + ) => { + try { + const docs = await (tx || db.replicaNode())(TableName.UserNotifications) + .where(`${TableName.UserNotifications}.userId`, userId) + .andWhere((qb) => { + void qb + .where(`${TableName.UserNotifications}.orgId`, orgId) + .orWhereNull(`${TableName.UserNotifications}.orgId`); + }) + .whereRaw(`"${TableName.UserNotifications}"."createdAt" >= ?::timestamptz`, [startDate]) + .andWhereRaw(`"${TableName.UserNotifications}"."createdAt" < ?::timestamptz`, [endDate]) + .select(selectAllTableCols(TableName.UserNotifications)) + .limit(limit) + .offset(offset) + .orderBy(`${TableName.UserNotifications}.createdAt`, "desc") + .timeout(1000 * 120); // 2 minutes timeout + + return docs; + } catch (error) { + if (error instanceof knex.KnexTimeoutError) { + throw new GatewayTimeoutError({ + error, + message: "Failed to fetch notifications due to timeout." + }); + } + + throw new DatabaseError({ error }); + } + }; + + // delete all notifications older than 3 months + const pruneNotifications = async () => { + const threeMonthsAgo = new Date(); + threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); + let deletedNotificationIds: { id: string }[] = []; + let numberOfRetryOnFailure = 0; + + logger.info(`${QueueName.DailyResourceCleanUp}: prune notifications started`); + do { + try { + // eslint-disable-next-line no-await-in-loop + deletedNotificationIds = await db.transaction(async (trx) => { + await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`); + + const findExpiredNotificationSubQuery = trx(TableName.UserNotifications) + .where("createdAt", "<", threeMonthsAgo) + .orderBy(`${TableName.UserNotifications}.createdAt`, "desc") + .select("id") + .limit(PRUNE_BATCH_SIZE); + + // eslint-disable-next-line no-await-in-loop + const results = await trx(TableName.UserNotifications) + .whereIn("id", findExpiredNotificationSubQuery) + .del() + .returning("id"); + + return results; + }); + + numberOfRetryOnFailure = 0; + } catch (error) { + numberOfRetryOnFailure += 1; + deletedNotificationIds = []; + logger.error(error, "Failed to delete notification on pruning. Retrying..."); + } finally { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + } + } while ( + deletedNotificationIds.length > 0 || + (numberOfRetryOnFailure > 0 && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE) + ); + + if (numberOfRetryOnFailure >= MAX_RETRY_ON_FAILURE) { + logger.error( + `${QueueName.DailyResourceCleanUp}: prune notifications completed with persistent errors after ${MAX_RETRY_ON_FAILURE} retries. Some notifications might not have been pruned.` + ); + } else { + logger.info(`${QueueName.DailyResourceCleanUp}: prune notifications completed`); + } + }; + + const markAllNotificationsAsRead = async (userId: string, orgId: string) => { + await db(TableName.UserNotifications) + .where({ userId }) + .andWhere((qb) => { + void qb.where({ orgId }).orWhereNull("orgId"); + }) + .update({ isRead: true }); + }; + + return { ...notificationOrm, pruneNotifications, find, markAllNotificationsAsRead }; +}; diff --git a/backend/src/services/offline-usage-report/offline-usage-report-dal.ts b/backend/src/services/offline-usage-report/offline-usage-report-dal.ts new file mode 100644 index 000000000..109d90da8 --- /dev/null +++ b/backend/src/services/offline-usage-report/offline-usage-report-dal.ts @@ -0,0 +1,208 @@ +import { TDbClient } from "@app/db"; +import { ProjectType, TableName } from "@app/db/schemas"; + +export type TOfflineUsageReportDALFactory = ReturnType; + +export const offlineUsageReportDALFactory = (db: TDbClient) => { + const getUserMetrics = async () => { + // Get total users and admin users + const userMetrics = (await db + .from(TableName.Users) + .select( + db.raw( + ` + COUNT(*) as total_users, + COUNT(CASE WHEN "superAdmin" = true THEN 1 END) as admin_users + ` + ) + ) + .where({ isGhost: false }) + .first()) as { total_users: string; admin_users: string } | undefined; + + // Get users by auth method + const authMethodStats = (await db + .from(TableName.Users) + .select( + db.raw(` + unnest("authMethods") as auth_method, + COUNT(*) as count + `) + ) + .where({ isGhost: false }) + .whereNotNull("authMethods") + .groupBy(db.raw('unnest("authMethods")'))) as Array<{ auth_method: string; count: string }>; + + const usersByAuthMethod = authMethodStats.reduce( + (acc: Record, row: { auth_method: string; count: string }) => { + acc[row.auth_method] = parseInt(row.count, 10); + return acc; + }, + {} as Record + ); + + return { + totalUsers: parseInt(userMetrics?.total_users || "0", 10), + adminUsers: parseInt(userMetrics?.admin_users || "0", 10), + usersByAuthMethod + }; + }; + + const getMachineIdentityMetrics = async () => { + // Get total machine identities + const identityMetrics = (await db + .from(TableName.Identity) + .select( + db.raw( + ` + COUNT(*) as total_identities + ` + ) + ) + .first()) as { total_identities: string } | undefined; + + // Get identities by auth method + const authMethodStats = (await db + .from(TableName.Identity) + .select("authMethod") + .count("* as count") + .whereNotNull("authMethod") + .groupBy("authMethod")) as Array<{ authMethod: string; count: string }>; + + const machineIdentitiesByAuthMethod = authMethodStats.reduce( + (acc: Record, row: { authMethod: string; count: string }) => { + acc[row.authMethod] = parseInt(row.count, 10); + return acc; + }, + {} as Record + ); + + return { + totalMachineIdentities: parseInt(identityMetrics?.total_identities || "0", 10), + machineIdentitiesByAuthMethod + }; + }; + + const getProjectMetrics = async () => { + // Get total projects and projects by type + const projectMetrics = (await db + .from(TableName.Project) + .select("type") + .count("* as count") + .groupBy("type")) as Array<{ type: string; count: string }>; + + const totalProjects = projectMetrics.reduce( + (sum, row: { type: string; count: string }) => sum + parseInt(row.count, 10), + 0 + ); + const projectsByType = projectMetrics.reduce( + (acc: Record, row: { type: string; count: string }) => { + acc[row.type] = parseInt(row.count, 10); + return acc; + }, + {} as Record + ); + + // Calculate average secrets per project + const secretsPerProject = (await db + .from(`${TableName.SecretV2} as s`) + .select("p.id as projectId") + .count("s.id as count") + .leftJoin(`${TableName.SecretFolder} as sf`, "s.folderId", "sf.id") + .leftJoin(`${TableName.Environment} as e`, "sf.envId", "e.id") + .leftJoin(`${TableName.Project} as p`, "e.projectId", "p.id") + .where("p.type", ProjectType.SecretManager) + .groupBy("p.id") + .whereNotNull("p.id")) as Array<{ projectId: string; count: string }>; + + const averageSecretsPerProject = + secretsPerProject.length > 0 + ? secretsPerProject.reduce( + (sum, row: { projectId: string; count: string }) => sum + parseInt(row.count, 10), + 0 + ) / secretsPerProject.length + : 0; + + return { + totalProjects, + projectsByType, + averageSecretsPerProject: Math.round(averageSecretsPerProject * 100) / 100 + }; + }; + + const getSecretMetrics = async () => { + // Get total secrets + const totalSecretsResult = (await db.from(TableName.SecretV2).count("* as count").first()) as + | { count: string } + | undefined; + + const totalSecrets = parseInt(totalSecretsResult?.count || "0", 10); + + // Get secrets by project + const secretsByProject = (await db + .from(`${TableName.SecretV2} as s`) + .select("p.id as projectId", "p.name as projectName") + .count("s.id as secretCount") + .leftJoin(`${TableName.SecretFolder} as sf`, "s.folderId", "sf.id") + .leftJoin(`${TableName.Environment} as e`, "sf.envId", "e.id") + .leftJoin(`${TableName.Project} as p`, "e.projectId", "p.id") + .where("p.type", ProjectType.SecretManager) + .groupBy("p.id", "p.name") + .whereNotNull("p.id")) as Array<{ projectId: string; projectName: string; secretCount: string }>; + + return { + totalSecrets, + secretsByProject: secretsByProject.map( + (row: { projectId: string; projectName: string; secretCount: string }) => ({ + projectId: row.projectId, + projectName: row.projectName, + secretCount: parseInt(row.secretCount, 10) + }) + ) + }; + }; + + const getSecretSyncMetrics = async () => { + const totalSecretSyncsResult = (await db.from(TableName.SecretSync).count("* as count").first()) as + | { count: string } + | undefined; + + return { + totalSecretSyncs: parseInt(totalSecretSyncsResult?.count || "0", 10) + }; + }; + + const getDynamicSecretMetrics = async () => { + const totalDynamicSecretsResult = (await db.from(TableName.DynamicSecret).count("* as count").first()) as + | { count: string } + | undefined; + + return { + totalDynamicSecrets: parseInt(totalDynamicSecretsResult?.count || "0", 10) + }; + }; + + const getSecretRotationMetrics = async () => { + // Check both v1 and v2 secret rotation tables + const [v1RotationsResult, v2RotationsResult] = await Promise.all([ + db.from(TableName.SecretRotation).count("* as count").first() as Promise<{ count: string } | undefined>, + db.from(TableName.SecretRotationV2).count("* as count").first() as Promise<{ count: string } | undefined> + ]); + + const totalV1Rotations = parseInt(v1RotationsResult?.count || "0", 10); + const totalV2Rotations = parseInt(v2RotationsResult?.count || "0", 10); + + return { + totalSecretRotations: totalV1Rotations + totalV2Rotations + }; + }; + + return { + getUserMetrics, + getMachineIdentityMetrics, + getProjectMetrics, + getSecretMetrics, + getSecretSyncMetrics, + getDynamicSecretMetrics, + getSecretRotationMetrics + }; +}; diff --git a/backend/src/services/offline-usage-report/offline-usage-report-service.ts b/backend/src/services/offline-usage-report/offline-usage-report-service.ts new file mode 100644 index 000000000..179232aa4 --- /dev/null +++ b/backend/src/services/offline-usage-report/offline-usage-report-service.ts @@ -0,0 +1,133 @@ +import crypto from "crypto"; + +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; + +import { TOfflineUsageReportDALFactory } from "./offline-usage-report-dal"; + +type TOfflineUsageReportServiceFactoryDep = { + offlineUsageReportDAL: TOfflineUsageReportDALFactory; + licenseService: Pick; +}; + +export type TOfflineUsageReportServiceFactory = ReturnType; + +export const offlineUsageReportServiceFactory = ({ + offlineUsageReportDAL, + licenseService +}: TOfflineUsageReportServiceFactoryDep) => { + const signReportContent = (content: string, licenseId: string): string => { + const contentHash = crypto.createHash("sha256").update(content).digest("hex"); + const hmac = crypto.createHmac("sha256", licenseId); + hmac.update(contentHash); + return hmac.digest("hex"); + }; + + const verifyReportContent = (content: string, signature: string, licenseId: string): boolean => { + const expectedSignature = signReportContent(content, licenseId); + return signature === expectedSignature; + }; + + const generateUsageReportCSV = async () => { + const cfg = getConfig(); + if (!cfg.LICENSE_KEY_OFFLINE) { + throw new BadRequestError({ + message: "Offline usage reports are not enabled. LICENSE_KEY_OFFLINE must be configured." + }); + } + + const customerId = licenseService.getCustomerId() as string; + const licenseId = licenseService.getLicenseId(); + + const [ + userMetrics, + machineIdentityMetrics, + projectMetrics, + secretMetrics, + secretSyncMetrics, + dynamicSecretMetrics, + secretRotationMetrics + ] = await Promise.all([ + offlineUsageReportDAL.getUserMetrics(), + offlineUsageReportDAL.getMachineIdentityMetrics(), + offlineUsageReportDAL.getProjectMetrics(), + offlineUsageReportDAL.getSecretMetrics(), + offlineUsageReportDAL.getSecretSyncMetrics(), + offlineUsageReportDAL.getDynamicSecretMetrics(), + offlineUsageReportDAL.getSecretRotationMetrics() + ]); + + const headers = [ + "Total Users", + "Admin Users", + "Total Identities", + "Total Projects", + "Total Secrets", + "Total Secret Syncs", + "Total Dynamic Secrets", + "Total Secret Rotations", + "Avg Secrets Per Project" + ]; + + const allUserAuthMethods = Object.keys(userMetrics.usersByAuthMethod); + allUserAuthMethods.forEach((method) => { + headers.push(`Users Auth ${method}`); + }); + + const allIdentityAuthMethods = Object.keys(machineIdentityMetrics.machineIdentitiesByAuthMethod); + allIdentityAuthMethods.forEach((method) => { + headers.push(`Identities Auth ${method}`); + }); + + const allProjectTypes = Object.keys(projectMetrics.projectsByType); + allProjectTypes.forEach((type) => { + headers.push(`Projects ${type}`); + }); + + headers.push("Signature"); + + const dataRow: (string | number)[] = [ + userMetrics.totalUsers, + userMetrics.adminUsers, + machineIdentityMetrics.totalMachineIdentities, + projectMetrics.totalProjects, + secretMetrics.totalSecrets, + secretSyncMetrics.totalSecretSyncs, + dynamicSecretMetrics.totalDynamicSecrets, + secretRotationMetrics.totalSecretRotations, + projectMetrics.averageSecretsPerProject + ]; + + allUserAuthMethods.forEach((method) => { + dataRow.push(userMetrics.usersByAuthMethod[method] || 0); + }); + allIdentityAuthMethods.forEach((method) => { + dataRow.push(machineIdentityMetrics.machineIdentitiesByAuthMethod[method] || 0); + }); + + allProjectTypes.forEach((type) => { + dataRow.push(projectMetrics.projectsByType[type] || 0); + }); + + const headersWithoutSignature = headers.slice(0, -1); + const contentWithoutSignature = [headersWithoutSignature.join(","), dataRow.join(",")].join("\n"); + + const signature = signReportContent(contentWithoutSignature, licenseId); + dataRow.push(signature); + + const csvContent = [headers.join(","), dataRow.join(",")].join("\n"); + + return { + csvContent, + signature, + filename: `infisical-usage-report-${customerId}-${new Date().toISOString().split("T")[0]}.csv` + }; + }; + + return { + generateUsageReportCSV, + verifyReportSignature: (csvContent: string, signature: string, licenseId: string) => + verifyReportContent(csvContent, signature, licenseId) + }; +}; diff --git a/backend/src/services/offline-usage-report/offline-usage-report-types.ts b/backend/src/services/offline-usage-report/offline-usage-report-types.ts new file mode 100644 index 000000000..614d5ccce --- /dev/null +++ b/backend/src/services/offline-usage-report/offline-usage-report-types.ts @@ -0,0 +1,42 @@ +export interface TUsageMetrics { + // User metrics + totalUsers: number; + usersByAuthMethod: Record; + adminUsers: number; + + // Machine identity metrics + totalMachineIdentities: number; + machineIdentitiesByAuthMethod: Record; + + // Project metrics + totalProjects: number; + projectsByType: Record; + averageSecretsPerProject: number; + + // Secret metrics + totalSecrets: number; + totalSecretSyncs: number; + totalDynamicSecrets: number; + totalSecretRotations: number; +} + +export interface TUsageReportMetadata { + generatedAt: string; + instanceId: string; + reportVersion: string; +} + +export interface TUsageReport { + metadata: TUsageReportMetadata; + metrics: TUsageMetrics; + signature?: string; +} + +export interface TGenerateUsageReportDTO { + includeSignature?: boolean; +} + +export interface TVerifyUsageReportDTO { + reportData: string; + signature: string; +} diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index aa22b11c7..925784bff 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -83,6 +83,7 @@ export const orgDALFactory = (db: TDbClient) => { .select(db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId")) .select(db.ref("role").withSchema(TableName.OrgMembership).as("orgMembershipRole")) .select(db.ref("roleId").withSchema(TableName.OrgMembership).as("orgMembershipRoleId")) + .select(db.ref("status").withSchema(TableName.OrgMembership).as("orgMembershipStatus")) .select(db.ref("name").withSchema(TableName.OrgRoles).as("orgMembershipRoleName")); const formattedDocs = sqlNestRelationships({ @@ -112,7 +113,8 @@ export const orgDALFactory = (db: TDbClient) => { orgMembershipId, orgMembershipRole, orgMembershipRoleName, - orgMembershipRoleId + orgMembershipRoleId, + orgMembershipStatus }) => ({ user: { id: userId, @@ -121,6 +123,7 @@ export const orgDALFactory = (db: TDbClient) => { firstName, lastName }, + status: orgMembershipStatus, membershipId: orgMembershipId, role: orgMembershipRoleName || orgMembershipRole, // custom role name or pre-defined role name roleId: orgMembershipRoleId @@ -488,6 +491,15 @@ export const orgDALFactory = (db: TDbClient) => { } }; + const bulkCreateMemberships = async (data: TOrgMembershipsInsert[], tx?: Knex) => { + try { + const memberships = await (tx || db)(TableName.OrgMembership).insert(data).returning("*"); + return memberships; + } catch (error) { + throw new DatabaseError({ error, name: "Create org memberships" }); + } + }; + const updateMembershipById = async (id: string, data: TOrgMembershipsUpdate, tx?: Knex) => { try { const [membership] = await (tx || db)(TableName.OrgMembership).where({ id }).update(data).returning("*"); @@ -668,6 +680,7 @@ export const orgDALFactory = (db: TDbClient) => { findMembership, findMembershipWithScimFilter, createMembership, + bulkCreateMemberships, updateMembershipById, deleteMembershipById, deleteMembershipsById, diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 356a8451c..ffcf4459e 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -9,11 +9,14 @@ import { ProjectMembershipRole, ProjectVersion, TableName, + TOidcConfigs, TProjectMemberships, TProjectUserMembershipRolesInsert, + TSamlConfigs, TUsers } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; +import { TLdapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { @@ -125,6 +128,7 @@ type TOrgServiceFactoryDep = { incidentContactDAL: TIncidentContactsDALFactory; samlConfigDAL: Pick; oidcConfigDAL: Pick; + ldapConfigDAL: Pick; smtpService: TSmtpService; tokenService: TAuthTokenServiceFactory; permissionService: TPermissionServiceFactory; @@ -165,6 +169,7 @@ export const orgServiceFactory = ({ projectRoleDAL, samlConfigDAL, oidcConfigDAL, + ldapConfigDAL, projectUserMembershipRoleDAL, identityMetadataDAL, projectBotService, @@ -446,16 +451,20 @@ export const orgServiceFactory = ({ }); } - if (authEnforced) { - const samlCfg = await samlConfigDAL.findOne({ + let samlCfg: TSamlConfigs | undefined; + let oidcCfg: TOidcConfigs | undefined; + if (authEnforced || googleSsoAuthEnforced) { + samlCfg = await samlConfigDAL.findOne({ orgId, isActive: true }); - const oidcCfg = await oidcConfigDAL.findOne({ + oidcCfg = await oidcConfigDAL.findOne({ orgId, isActive: true }); + } + if (authEnforced) { if (!samlCfg && !oidcCfg) throw new NotFoundError({ message: `SAML or OIDC configuration for organization with ID '${orgId}' not found` @@ -483,6 +492,32 @@ export const orgServiceFactory = ({ }); } + if (samlCfg) { + throw new BadRequestError({ + message: + "Cannot enable Google OAuth enforcement while SAML SSO is configured. Disable SAML SSO to enforce Google OAuth." + }); + } + + if (oidcCfg) { + throw new BadRequestError({ + message: + "Cannot enable Google OAuth enforcement while OIDC SSO is configured. Disable OIDC SSO to enforce Google OAuth." + }); + } + + const ldapCfg = await ldapConfigDAL.findOne({ + orgId, + isActive: true + }); + + if (ldapCfg) { + throw new BadRequestError({ + message: + "Cannot enable Google OAuth enforcement while LDAP SSO is configured. Disable LDAP SSO to enforce Google OAuth." + }); + } + if (!currentOrg.googleSsoAuthLastUsed) { throw new BadRequestError({ message: @@ -528,15 +563,18 @@ export const orgServiceFactory = ({ /* * Create organization * */ - const createOrganization = async ({ - userId, - userEmail, - orgName - }: { - userId: string; - orgName: string; - userEmail?: string | null; - }) => { + const createOrganization = async ( + { + userId, + userEmail, + orgName + }: { + userId?: string; + orgName: string; + userEmail?: string | null; + }, + trx?: Knex + ) => { const { privateKey, publicKey } = await crypto.encryption().asymmetric().generateKeyPair(); const key = crypto.randomBytes(32).toString("base64"); const { @@ -555,22 +593,25 @@ export const orgServiceFactory = ({ } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(key); const customerId = await licenseService.generateOrgCustomerId(orgName, userEmail); - const organization = await orgDAL.transaction(async (tx) => { + + const createOrg = async (tx: Knex) => { // akhilmhdh: for now this is auto created. in future we can input from user and for previous users just modifiy const org = await orgDAL.create( { name: orgName, customerId, slug: slugify(`${orgName}-${alphaNumericNanoId(4)}`) }, tx ); - await orgDAL.createMembership( - { - userId, - orgId: org.id, - role: OrgMembershipRole.Admin, - status: OrgMembershipStatus.Accepted, - isActive: true - }, - tx - ); + if (userId) { + await orgDAL.createMembership( + { + userId, + orgId: org.id, + role: OrgMembershipRole.Admin, + status: OrgMembershipStatus.Accepted, + isActive: true + }, + tx + ); + } await orgBotDAL.create( { name: org.name, @@ -590,7 +631,9 @@ export const orgServiceFactory = ({ tx ); return org; - }); + }; + + const organization = await (trx ? createOrg(trx) : orgDAL.transaction(createOrg)); await licenseService.updateSubscriptionOrgMemberCount(organization.id); return organization; diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 2766db379..d64977f8b 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -399,6 +399,7 @@ export const projectDALFactory = (db: TDbClient) => { name?: string; sortBy?: SearchProjectSortBy; sortDir?: SortDirection; + projectIds?: string[]; }) => { const { limit = 20, offset = 0, sortBy = SearchProjectSortBy.NAME, sortDir = SortDirection.ASC } = dto; @@ -454,6 +455,11 @@ export const projectDALFactory = (db: TDbClient) => { if (dto.name) { void query.whereILike(`${TableName.Project}.name`, `%${dto.name}%`); } + + if (dto.projectIds?.length) { + void query.whereIn(`${TableName.Project}.id`, dto.projectIds); + } + const docs = await query; return { docs, totalCount: Number(docs?.[0]?.count ?? 0) }; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 4261870b1..57539f002 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1,4 +1,5 @@ -import { ForbiddenError, subject } from "@casl/ability"; +import { createMongoAbility, ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability"; +import { PackRule, unpackRules } from "@casl/ability/extra"; import slugify from "@sindresorhus/slugify"; import { @@ -16,9 +17,11 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { ProjectPermissionActions, ProjectPermissionCertificateActions, + ProjectPermissionMemberActions, ProjectPermissionPkiSubscriberActions, ProjectPermissionPkiTemplateActions, ProjectPermissionSecretActions, + ProjectPermissionSet, ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -234,8 +237,8 @@ export const projectServiceFactory = ({ actorId, actorOrgId, actorAuthMethod, - workspaceName, - workspaceDescription, + projectName: workspaceName, + projectDescription: workspaceDescription, slug: projectSlug, kmsKeyId, tx: trx, @@ -251,7 +254,13 @@ export const projectServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); + + if ( + permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace) && + permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Project) + ) { + throw new ForbiddenRequestError({ message: "You don't have permission to create a project" }); + } const results = await (trx || projectDAL).transaction(async (tx) => { await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.CreateProject(organization.id)]); @@ -588,7 +597,8 @@ export const projectServiceFactory = ({ secretSharing: update.secretSharing, defaultProduct: update.defaultProduct, showSnapshotsLegacy: update.showSnapshotsLegacy, - secretDetectionIgnoreValues: update.secretDetectionIgnoreValues + secretDetectionIgnoreValues: update.secretDetectionIgnoreValues, + pitVersionLimit: update.pitVersionLimit }); return updatedProject; @@ -681,19 +691,21 @@ export const projectServiceFactory = ({ actorOrgId, actorAuthMethod, auditLogsRetentionDays, - workspaceSlug + filter }: TUpdateAuditLogsRetentionDTO) => { - const project = await projectDAL.findProjectBySlug(workspaceSlug, actorOrgId); + const project = await projectDAL.findProjectByFilter(filter); + const projectId = project.id; + if (!project) { throw new NotFoundError({ - message: `Project with slug '${workspaceSlug}' not found` + message: `Project not found` }); } const { hasRole } = await permissionService.getProjectPermission({ actor, actorId, - projectId: project.id, + projectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.Any @@ -1803,7 +1815,8 @@ export const projectServiceFactory = ({ limit, type, orderBy, - orderDirection + orderDirection, + projectIds }: TSearchProjectsDTO) => { // check user belong to org await permissionService.getOrgPermission( @@ -1819,6 +1832,7 @@ export const projectServiceFactory = ({ offset, name, type, + projectIds, orgId: permission.orgId, actor: permission.type, actorId: permission.id, @@ -1852,9 +1866,52 @@ export const projectServiceFactory = ({ if (projectMember) throw new BadRequestError({ message: "User already has access to the project" }); const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); - const filteredProjectMembers = projectMembers + + let filteredProjectMembers = projectMembers .filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin)) .map((el) => el.user.email!); + if (filteredProjectMembers.length === 0) { + const customRolesWithMemberCreate = await projectRoleDAL.find({ projectId }); + const customRoleSlugsCanCreate = customRolesWithMemberCreate + .filter((role) => { + try { + const permissions = ( + typeof role.permissions === "string" + ? (JSON.parse(role.permissions) as PackRule>>[]) + : role.permissions + ) as PackRule>>[]; + + const ability = createMongoAbility>( + unpackRules>>(permissions) + ); + return ability.can(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member); + } catch { + return false; + } + }) + .map((role) => role.slug); + + if (customRoleSlugsCanCreate.length > 0) { + const usersWithCustomCreateMemberRole = projectMembers + .filter((member) => + member.roles.some((role) => role.customRoleSlug && customRoleSlugsCanCreate.includes(role.customRoleSlug)) + ) + .map((el) => el.user.email!) + .filter(Boolean); + + if (usersWithCustomCreateMemberRole.length > 0) { + filteredProjectMembers = usersWithCustomCreateMemberRole; + } + } + } + + if (filteredProjectMembers.length === 0) { + throw new BadRequestError({ + message: + "No users in this project have permission to grant you access. Please contact an organization administrator to assign the necessary permissions." + }); + } + const org = await orgDAL.findOne({ id: permission.orgId }); const project = await projectDAL.findById(projectId); const userDetails = await userDAL.findById(permission.id); diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index ceef78f6a..74c7e95f4 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -42,12 +42,13 @@ export type TCreateProjectDTO = { actorAuthMethod: ActorAuthMethod; actorId: string; actorOrgId?: string; - workspaceName: string; - workspaceDescription?: string; + projectName: string; + projectDescription?: string; slug?: string; kmsKeyId?: string; createDefaultEnvs?: boolean; template?: string; + pitVersionLimit?: number; tx?: Knex; type?: ProjectType; }; @@ -78,7 +79,7 @@ export type TUpdateProjectVersionLimitDTO = { export type TUpdateAuditLogsRetentionDTO = { auditLogsRetentionDays: number; - workspaceSlug: string; + filter: Filter; } & Omit; export type TUpdateProjectNameDTO = { @@ -90,6 +91,7 @@ export type TUpdateProjectDTO = { update: { name?: string; description?: string; + pitVersionLimit?: number; autoCapitalization?: boolean; hasDeleteProtection?: boolean; defaultProduct?: ProjectType; @@ -221,6 +223,7 @@ export type TSearchProjectsDTO = { limit?: number; offset?: number; orderBy?: SearchProjectSortBy; + projectIds?: string[]; orderDirection?: SortDirection; }; diff --git a/backend/src/services/reminder/reminder-dal.ts b/backend/src/services/reminder/reminder-dal.ts index 897a75234..4161552a9 100644 --- a/backend/src/services/reminder/reminder-dal.ts +++ b/backend/src/services/reminder/reminder-dal.ts @@ -39,7 +39,7 @@ export const reminderDALFactory = (db: TDbClient) => { const findSecretDailyReminders = async (tx?: Knex) => { const { startOfDay, endOfDay } = getTodayDateRange(); - const rawReminders = await (tx || db)(TableName.Reminder) + const rawReminders = await (tx || db.replicaNode())(TableName.Reminder) .whereBetween("nextReminderDate", [startOfDay, endOfDay]) .leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`) .leftJoin(TableName.Users, `${TableName.ReminderRecipient}.userId`, `${TableName.Users}.id`) @@ -90,7 +90,7 @@ export const reminderDALFactory = (db: TDbClient) => { const futureDate = new Date(startOfDay); futureDate.setDate(futureDate.getDate() + daysAhead); - const reminders = await (tx || db)(TableName.Reminder) + const reminders = await (tx || db.replicaNode())(TableName.Reminder) .where("nextReminderDate", ">=", startOfDay) .where("nextReminderDate", "<=", futureDate) .orderBy("nextReminderDate", "asc") @@ -101,7 +101,7 @@ export const reminderDALFactory = (db: TDbClient) => { }; const findSecretReminder = async (secretId: string, tx?: Knex) => { - const rawReminders = await (tx || db)(TableName.Reminder) + const rawReminders = await (tx || db.replicaNode())(TableName.Reminder) .where(`${TableName.Reminder}.secretId`, secretId) .leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`) .select(selectAllTableCols(TableName.Reminder)) @@ -125,7 +125,7 @@ export const reminderDALFactory = (db: TDbClient) => { }; const findSecretReminders = async (secretIds: string[], tx?: Knex) => { - const rawReminders = await (tx || db)(TableName.Reminder) + const rawReminders = await (tx || db.replicaNode())(TableName.Reminder) .whereIn(`${TableName.Reminder}.secretId`, secretIds) .leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`) .select(selectAllTableCols(TableName.Reminder)) diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index dcfa7ea0d..185ab5e94 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -1,8 +1,10 @@ import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal"; import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; +import { TKeyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { TUserNotificationDALFactory } from "@app/services/notification/user-notification-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal"; @@ -25,6 +27,8 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = { serviceTokenService: Pick; queueService: TQueueServiceFactory; orgService: TOrgServiceFactory; + userNotificationDAL: Pick; + keyValueStoreDAL: Pick; }; export type TDailyResourceCleanUpQueueServiceFactory = ReturnType; @@ -40,7 +44,9 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ secretVersionV2DAL, identityUniversalAuthClientSecretDAL, serviceTokenService, - orgService + orgService, + userNotificationDAL, + keyValueStoreDAL }: TDailyResourceCleanUpQueueServiceFactoryDep) => { const appCfg = getConfig(); @@ -49,6 +55,10 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ } const init = async () => { + if (appCfg.isSecondaryInstance) { + return; + } + await queueService.stopRepeatableJob( QueueName.AuditLogPrune, QueueJobs.AuditLogPrune, @@ -78,6 +88,8 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ await serviceTokenService.notifyExpiringTokens(); await orgService.notifyInvitedUsers(); await auditLogDAL.pruneAuditLog(); + await userNotificationDAL.pruneNotifications(); + await keyValueStoreDAL.pruneExpiredKeys(); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); } catch (error) { logger.error(error, `${QueueName.DailyResourceCleanUp}: resource cleanup failed`); diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index c5eb0adde..8e0892bd1 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -47,7 +47,7 @@ type TSecretFolderServiceFactoryDep = { folderCommitService: Pick; projectDAL: Pick; secretApprovalPolicyService: Pick; - secretV2BridgeDAL: Pick; + secretV2BridgeDAL: Pick; }; export type TSecretFolderServiceFactory = ReturnType; @@ -398,6 +398,7 @@ export const secretFolderServiceFactory = ({ await Promise.all(result.map(async (res) => snapshotService.performSnapshot(res.newFolder.parentId as string))); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); return { projectId, newFolders: result.map((res) => res.newFolder), @@ -522,6 +523,7 @@ export const secretFolderServiceFactory = ({ } await snapshotService.performSnapshot(newFolder.parentId as string); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); return { folder: { ...newFolder, path: newFolderWithFullPath.path }, old: { ...folder, path: folderWithFullPath.path } @@ -724,6 +726,7 @@ export const secretFolderServiceFactory = ({ }); await snapshotService.performSnapshot(folder.parentId as string); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); return folder; }; diff --git a/backend/src/services/secret-folder/secret-folder-version-dal.ts b/backend/src/services/secret-folder/secret-folder-version-dal.ts index 46ff49692..5504c6e0b 100644 --- a/backend/src/services/secret-folder/secret-folder-version-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-version-dal.ts @@ -45,7 +45,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => { ) .whereIn(`${TableName.SecretFolderVersion}.folderId`, folderIds) .join( - (tx || db)(TableName.SecretFolderVersion) + (tx || db.replicaNode())(TableName.SecretFolderVersion) .groupBy("folderId") .max("version") .select("folderId") diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index dbe2f6a84..2261c2418 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -15,7 +15,7 @@ export const secretImportDALFactory = (db: TDbClient) => { // we are using postion based sorting as its a small list // this will return the last value of the position in a folder with secret imports const findLastImportPosition = async (folderId: string, tx?: Knex) => { - const lastPos = await (tx || db)(TableName.SecretImport) + const lastPos = await (tx || db.replicaNode())(TableName.SecretImport) .where({ folderId }) .max("position", { as: "position" }) .first(); @@ -127,6 +127,27 @@ export const secretImportDALFactory = (db: TDbClient) => { } }; + const findByIds = async (ids: string[], tx?: Knex) => { + try { + const docs = await (tx || db.replicaNode())(TableName.SecretImport) + .whereIn(`${TableName.SecretImport}.id`, ids) + .join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`) + .select( + db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports, + db.ref("slug").withSchema(TableName.Environment), + db.ref("name").withSchema(TableName.Environment), + db.ref("id").withSchema(TableName.Environment).as("envId") + ); + + return docs.map(({ envId, slug, name, ...el }) => ({ + ...el, + importEnv: { id: envId, slug, name } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find secret imports by ids" }); + } + }; + const getProjectImportCount = async ( { search, ...filter }: Partial, tx?: Knex @@ -325,6 +346,7 @@ export const secretImportDALFactory = (db: TDbClient) => { ...secretImportOrm, find, findById, + findByIds, findByFolderIds, findLastImportPosition, updateAllPosition, diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index 6aa73465d..c739c5ad2 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -1,3 +1,5 @@ +import RE2 from "re2"; + import { SecretType, TSecretImports, TSecrets, TSecretsV2 } from "@app/db/schemas"; import { groupBy, unique } from "@app/lib/fn"; @@ -54,6 +56,74 @@ type TSecretImportSecretsV2 = { const LEVEL_BREAK = 10; const getImportUniqKey = (envSlug: string, path: string) => `${envSlug}=${path}`; +const RESERVED_IMPORT_REGEX = new RE2("/__reserve_replication_([a-f0-9-]{36})"); + +/** + * Processes reserved imports by resolving them to their replication source. + */ +const processReservedImports = async < + T extends { + isReserved?: boolean | null; + importPath: string; + importEnv: { id: string; slug: string; name: string }; + folderId: string; + } +>( + imports: T[], + secretImportDAL: Pick +): Promise => { + const reservedImportIds: string[] = []; + + imports.forEach((secretImport) => { + if (secretImport.isReserved) { + const reservedMatch = RESERVED_IMPORT_REGEX.exec(secretImport.importPath); + if (reservedMatch) { + const referencedImportId = reservedMatch[1]; + reservedImportIds.push(referencedImportId); + } + } + }); + + if (reservedImportIds.length === 0) { + return imports; + } + + try { + const importDetailsMap = new Map< + string, + { importPath: string; importEnv: { id: string; slug: string; name: string } } + >(); + + const referencedImports = await secretImportDAL.findByIds(reservedImportIds); + referencedImports.forEach((referencedImport) => { + importDetailsMap.set(referencedImport.id, { + importPath: referencedImport.importPath, + importEnv: referencedImport.importEnv + }); + }); + + return imports.map((secretImport) => { + if (secretImport.isReserved) { + const reservedMatch = RESERVED_IMPORT_REGEX.exec(secretImport.importPath); + if (reservedMatch) { + const referencedImportId = reservedMatch[1]; + const referencedDetails = importDetailsMap.get(referencedImportId); + + if (referencedDetails) { + return { + ...secretImport, + importPath: referencedDetails.importPath, + importEnv: referencedDetails.importEnv + }; + } + } + } + return secretImport; + }); + } catch (error) { + return imports; + } +}; export const fnSecretsFromImports = async ({ allowedImports: possibleCyclicImports, folderDAL, @@ -167,7 +237,7 @@ export const fnSecretsV2FromImports = async ({ folderDAL: Pick; viewSecretValue: boolean; secretDAL: Pick; - secretImportDAL: Pick; + secretImportDAL: Pick; decryptor: (value?: Buffer | null) => string; expandSecretReferences?: (inputSecret: { value?: string; @@ -188,6 +258,10 @@ export const fnSecretsV2FromImports = async ({ })[]; }[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }]; + const processedSecretImports = await processReservedImports(rootSecretImports, secretImportDAL); + + stack[0] = { secretImports: processedSecretImports, depth: 0, parentImportedSecrets: [] }; + const processedImports: TSecretImportSecretsV2[] = []; while (stack.length) { diff --git a/backend/src/services/secret-sharing/secret-sharing-dal.ts b/backend/src/services/secret-sharing/secret-sharing-dal.ts index 7cdccd4f8..08e0ad257 100644 --- a/backend/src/services/secret-sharing/secret-sharing-dal.ts +++ b/backend/src/services/secret-sharing/secret-sharing-dal.ts @@ -119,7 +119,7 @@ export const secretSharingDALFactory = (db: TDbClient) => { const findActiveSharedSecrets = async (filters: Partial, tx?: Knex) => { try { const now = new Date(); - return await (tx || db)(TableName.SecretSharing) + return await (tx || db.replicaNode())(TableName.SecretSharing) .where(filters) .andWhere("expiresAt", ">", now) .andWhere("encryptedValue", "<>", "") diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts index 680dd4256..9bcf659aa 100644 --- a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts @@ -12,7 +12,7 @@ type TAWSParameterStoreRecord = Record; type TAWSParameterStoreMetadataRecord = Record; type TAWSParameterStoreTagsRecord = Record>; -const MAX_RETRIES = 5; +const MAX_RETRIES = 10; const BATCH_SIZE = 10; const getSSM = async (secretSync: TAwsParameterStoreSyncWithCredentials) => { diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts index 8e37cf277..8573d6ffc 100644 --- a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts @@ -38,7 +38,7 @@ type TAwsSecretsRecord = Record; type TAwsSecretValuesRecord = Record; type TAwsSecretDescriptionsRecord = Record; -const MAX_RETRIES = 5; +const MAX_RETRIES = 10; const BATCH_SIZE = 20; const getSecretsManagerClient = async (secretSync: TAwsSecretsManagerSyncWithCredentials) => { diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts index 2cae048aa..4b174ca2a 100644 --- a/backend/src/services/secret-sync/github/github-sync-fns.ts +++ b/backend/src/services/secret-sync/github/github-sync-fns.ts @@ -1,6 +1,7 @@ import sodium from "libsodium-wrappers"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { getGitHubAppAuthToken, getGitHubInstanceApiUrl, @@ -20,7 +21,8 @@ import { TGitHubPublicKey, TGitHubSecret, TGitHubSecretPayload, TGitHubSyncWithC const getEncryptedSecrets = async ( secretSync: TGitHubSyncWithCredentials, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { const { destinationConfig, connection } = secretSync; @@ -44,6 +46,7 @@ const getEncryptedSecrets = async ( return makePaginatedGitHubRequest( connection, gatewayService, + gatewayV2Service, path, (data) => data.secrets ); @@ -52,6 +55,7 @@ const getEncryptedSecrets = async ( const getPublicKey = async ( secretSync: TGitHubSyncWithCredentials, gatewayService: Pick, + gatewayV2Service: Pick, token: string ) => { const { destinationConfig, connection } = secretSync; @@ -73,7 +77,7 @@ const getPublicKey = async ( } } - const response = await requestWithGitHubGateway(connection, gatewayService, { + const response = await requestWithGitHubGateway(connection, gatewayService, gatewayV2Service, { url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`, method: "GET", headers: { @@ -89,6 +93,7 @@ const getPublicKey = async ( const deleteSecret = async ( secretSync: TGitHubSyncWithCredentials, gatewayService: Pick, + gatewayV2Service: Pick, token: string, encryptedSecret: TGitHubSecret ) => { @@ -111,7 +116,7 @@ const deleteSecret = async ( } } - await requestWithGitHubGateway(connection, gatewayService, { + await requestWithGitHubGateway(connection, gatewayService, gatewayV2Service, { url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`, method: "DELETE", headers: { @@ -125,6 +130,7 @@ const deleteSecret = async ( const putSecret = async ( secretSync: TGitHubSyncWithCredentials, gatewayService: Pick, + gatewayV2Service: Pick, token: string, payload: TGitHubSecretPayload ) => { @@ -157,7 +163,7 @@ const putSecret = async ( } } - await requestWithGitHubGateway(connection, gatewayService, { + await requestWithGitHubGateway(connection, gatewayService, gatewayV2Service, { url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`, method: "PUT", headers: { @@ -173,7 +179,8 @@ export const GithubSyncFns = { syncSecrets: async ( secretSync: TGitHubSyncWithCredentials, ogSecretMap: TSecretMap, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { const secretMap = Object.fromEntries(Object.entries(ogSecretMap).map(([i, v]) => [i.toUpperCase(), v])); @@ -207,10 +214,10 @@ export const GithubSyncFns = { const token = connection.method === GitHubConnectionMethod.OAuth ? connection.credentials.accessToken - : await getGitHubAppAuthToken(connection, gatewayService); + : await getGitHubAppAuthToken(connection, gatewayService, gatewayV2Service); - const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService); - const publicKey = await getPublicKey(secretSync, gatewayService, token); + const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService, gatewayV2Service); + const publicKey = await getPublicKey(secretSync, gatewayService, gatewayV2Service, token); await sodium.ready; for await (const key of Object.keys(secretMap)) { @@ -225,7 +232,7 @@ export const GithubSyncFns = { const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL); try { - await putSecret(secretSync, gatewayService, token, { + await putSecret(secretSync, gatewayService, gatewayV2Service, token, { secret_name: key, encrypted_value: encryptedSecretValue, key_id: publicKey.key_id @@ -246,7 +253,7 @@ export const GithubSyncFns = { continue; if (!(encryptedSecret.name in secretMap)) { - await deleteSecret(secretSync, gatewayService, token, encryptedSecret); + await deleteSecret(secretSync, gatewayService, gatewayV2Service, token, encryptedSecret); } } }, @@ -256,7 +263,8 @@ export const GithubSyncFns = { removeSecrets: async ( secretSync: TGitHubSyncWithCredentials, ogSecretMap: TSecretMap, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { const secretMap = Object.fromEntries(Object.entries(ogSecretMap).map(([i, v]) => [i.toUpperCase(), v])); @@ -264,13 +272,13 @@ export const GithubSyncFns = { const token = connection.method === GitHubConnectionMethod.OAuth ? connection.credentials.accessToken - : await getGitHubAppAuthToken(connection, gatewayService); + : await getGitHubAppAuthToken(connection, gatewayService, gatewayV2Service); - const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService); + const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService, gatewayV2Service); for await (const encryptedSecret of encryptedSecrets) { if (encryptedSecret.name in secretMap) { - await deleteSecret(secretSync, gatewayService, token, encryptedSecret); + await deleteSecret(secretSync, gatewayService, gatewayV2Service, token, encryptedSecret); } } } diff --git a/backend/src/services/secret-sync/gitlab/gitlab-sync-fns.ts b/backend/src/services/secret-sync/gitlab/gitlab-sync-fns.ts index 3c152d853..7bf45d47c 100644 --- a/backend/src/services/secret-sync/gitlab/gitlab-sync-fns.ts +++ b/backend/src/services/secret-sync/gitlab/gitlab-sync-fns.ts @@ -53,6 +53,7 @@ const getValidAccessToken = async ( connection.credentials.refreshToken, connection.id, connection.orgId, + connection.projectId, appConnectionDAL, kmsService, connection.credentials.instanceUrl diff --git a/backend/src/services/secret-sync/heroku/heroku-sync-fns.ts b/backend/src/services/secret-sync/heroku/heroku-sync-fns.ts index d2f0817db..5f2375979 100644 --- a/backend/src/services/secret-sync/heroku/heroku-sync-fns.ts +++ b/backend/src/services/secret-sync/heroku/heroku-sync-fns.ts @@ -32,6 +32,7 @@ const getValidAuthToken = async ( connection.credentials.refreshToken, connection.id, connection.orgId, + connection.projectId, appConnectionDAL, kmsService ); diff --git a/backend/src/services/secret-sync/secret-sync-dal.ts b/backend/src/services/secret-sync/secret-sync-dal.ts index e50593f10..57c6581ce 100644 --- a/backend/src/services/secret-sync/secret-sync-dal.ts +++ b/backend/src/services/secret-sync/secret-sync-dal.ts @@ -31,6 +31,7 @@ const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: Secre db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"), db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"), + db.ref("projectId").withSchema(TableName.AppConnection).as("connectionProjectId"), db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"), db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), db @@ -67,6 +68,7 @@ const expandSecretSync = ( connectionVersion, connectionIsPlatformManagedCredentials, connectionGatewayId, + connectionProjectId, ...el } = secretSync; @@ -86,7 +88,8 @@ const expandSecretSync = ( updatedAt: connectionUpdatedAt, version: connectionVersion, isPlatformManagedCredentials: connectionIsPlatformManagedCredentials, - gatewayId: connectionGatewayId + gatewayId: connectionGatewayId, + projectId: connectionProjectId }, folder: folder ? { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 5b083baf6..a6faebd1e 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -2,6 +2,7 @@ import { AxiosError } from "axios"; import handlebars from "handlebars"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault"; import { BadRequestError } from "@app/lib/errors"; @@ -101,6 +102,7 @@ type TSyncSecretDeps = { appConnectionDAL: Pick; kmsService: Pick; gatewayService: Pick; + gatewayV2Service: Pick; }; // Add schema to secret keys @@ -195,7 +197,7 @@ export const SecretSyncFns = { syncSecrets: ( secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap, - { kmsService, appConnectionDAL, gatewayService }: TSyncSecretDeps + { kmsService, appConnectionDAL, gatewayService, gatewayV2Service }: TSyncSecretDeps ): Promise => { const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema); @@ -205,7 +207,7 @@ export const SecretSyncFns = { case SecretSync.AWSSecretsManager: return AwsSecretsManagerSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.GitHub: - return GithubSyncFns.syncSecrets(secretSync, schemaSecretMap, gatewayService); + return GithubSyncFns.syncSecrets(secretSync, schemaSecretMap, gatewayService, gatewayV2Service); case SecretSync.GCPSecretManager: return GcpSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.AzureKeyVault: @@ -404,7 +406,7 @@ export const SecretSyncFns = { removeSecrets: ( secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap, - { kmsService, appConnectionDAL, gatewayService }: TSyncSecretDeps + { kmsService, appConnectionDAL, gatewayService, gatewayV2Service }: TSyncSecretDeps ): Promise => { const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema); @@ -414,7 +416,7 @@ export const SecretSyncFns = { case SecretSync.AWSSecretsManager: return AwsSecretsManagerSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.GitHub: - return GithubSyncFns.removeSecrets(secretSync, schemaSecretMap, gatewayService); + return GithubSyncFns.removeSecrets(secretSync, schemaSecretMap, gatewayService, gatewayV2Service); case SecretSync.GCPSecretManager: return GcpSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.AzureKeyVault: diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index 7bef7d8c7..a31bb202d 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -5,6 +5,7 @@ import { Job } from "bullmq"; import { ProjectMembershipRole, SecretType } from "@app/db/schemas"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; @@ -80,8 +81,8 @@ type TSecretSyncQueueFactoryDep = { | "deleteMany" | "invalidateSecretCacheByProjectId" >; - secretImportDAL: Pick; - secretSyncDAL: Pick; + secretImportDAL: Pick; + secretSyncDAL: Pick; auditLogService: Pick; projectMembershipDAL: Pick; projectDAL: TProjectDALFactory; @@ -98,23 +99,22 @@ type TSecretSyncQueueFactoryDep = { folderCommitService: Pick; licenseService: Pick; gatewayService: Pick; + gatewayV2Service: Pick; }; type SecretSyncActionJob = Job< TQueueSecretSyncSyncSecretsByIdDTO | TQueueSecretSyncImportSecretsByIdDTO | TQueueSecretSyncRemoveSecretsByIdDTO >; +const JITTER_MS = 10 * 1000; +const REQUEUE_MS = 30 * 1000; +const REQUEUE_LIMIT = 30; +const CONNECTION_CONCURRENCY_LIMIT = 3; + const getRequeueDelay = (failureCount?: number) => { - if (!failureCount) return 0; - - const baseDelay = 1000; - const maxDelay = 30000; - - const delay = Math.min(baseDelay * 2 ** failureCount, maxDelay); - - const jitter = delay * (0.5 + Math.random() * 0.5); - - return jitter; + const jitter = Math.random() * JITTER_MS; + if (!failureCount) return jitter; + return REQUEUE_MS + jitter; }; export const secretSyncQueueFactory = ({ @@ -141,7 +141,8 @@ export const secretSyncQueueFactory = ({ resourceMetadataDAL, folderCommitService, licenseService, - gatewayService + gatewayService, + gatewayV2Service }: TSecretSyncQueueFactoryDep) => { const appCfg = getConfig(); @@ -193,6 +194,46 @@ export const secretSyncQueueFactory = ({ folderCommitService }); + const $isConnectionConcurrencyLimitReached = async (connectionId: string) => { + const concurrencyCount = await keyStore.getItem(KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId)); + + if (!concurrencyCount) return false; + + const count = Number.parseInt(concurrencyCount, 10); + + if (Number.isNaN(count)) return false; + + return count >= CONNECTION_CONCURRENCY_LIMIT; + }; + + const $incrementConnectionConcurrencyCount = async (connectionId: string) => { + const concurrencyCount = await keyStore.getItem(KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId)); + + const currentCount = Number.parseInt(concurrencyCount || "0", 10); + + const incrementedCount = Number.isNaN(currentCount) ? 1 : currentCount + 1; + + await keyStore.setItemWithExpiry( + KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId), + (REQUEUE_MS * REQUEUE_LIMIT) / 1000, // in seconds + incrementedCount + ); + }; + + const $decrementConnectionConcurrencyCount = async (connectionId: string) => { + const concurrencyCount = await keyStore.getItem(KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId)); + + const currentCount = Number.parseInt(concurrencyCount || "0", 10); + + const decrementedCount = Math.max(0, Number.isNaN(currentCount) ? 0 : currentCount - 1); + + await keyStore.setItemWithExpiry( + KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId), + (REQUEUE_MS * REQUEUE_LIMIT) / 1000, // in seconds + decrementedCount + ); + }; + const $getInfisicalSecrets = async ( secretSync: TSecretSyncRaw | TSecretSyncWithCredentials, includeImports = true @@ -357,7 +398,8 @@ export const secretSyncQueueFactory = ({ const importedSecrets = await SecretSyncFns.getSecrets(secretSync, { appConnectionDAL, kmsService, - gatewayService + gatewayService, + gatewayV2Service }); if (!Object.keys(importedSecrets).length) return {}; @@ -416,15 +458,11 @@ export const secretSyncQueueFactory = ({ return importedSecretMap; }; - const $handleSyncSecretsJob = async (job: TSecretSyncSyncSecretsDTO) => { + const $handleSyncSecretsJob = async (job: TSecretSyncSyncSecretsDTO, secretSync: TSecretSyncRaw) => { const { data: { syncId, auditLogInfo } } = job; - const secretSync = await secretSyncDAL.findById(syncId); - - if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`); - await enterpriseSyncCheck( licenseService, secretSync.destination as SecretSync, @@ -446,13 +484,14 @@ export const secretSyncQueueFactory = ({ try { const { - connection: { orgId, encryptedCredentials } + connection: { orgId, encryptedCredentials, projectId } } = secretSync; const credentials = await decryptAppConnectionCredentials({ orgId, encryptedCredentials, - kmsService + kmsService, + projectId }); const secretSyncWithCredentials = { @@ -486,7 +525,8 @@ export const secretSyncQueueFactory = ({ await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap, { appConnectionDAL, kmsService, - gatewayService + gatewayService, + gatewayV2Service }); isSynced = true; @@ -566,15 +606,11 @@ export const secretSyncQueueFactory = ({ logger.info("SecretSync Sync Job with ID %s Completed", job.id); }; - const $handleImportSecretsJob = async (job: TSecretSyncImportSecretsDTO) => { + const $handleImportSecretsJob = async (job: TSecretSyncImportSecretsDTO, secretSync: TSecretSyncRaw) => { const { data: { syncId, auditLogInfo, importBehavior } } = job; - const secretSync = await secretSyncDAL.findById(syncId); - - if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`); - await secretSyncDAL.updateById(syncId, { importStatus: SecretSyncStatus.Running }); @@ -589,13 +625,14 @@ export const secretSyncQueueFactory = ({ try { const { - connection: { orgId, encryptedCredentials } + connection: { orgId, encryptedCredentials, projectId } } = secretSync; const credentials = await decryptAppConnectionCredentials({ orgId, encryptedCredentials, - kmsService + kmsService, + projectId }); await $importSecrets( @@ -683,15 +720,11 @@ export const secretSyncQueueFactory = ({ logger.info("SecretSync Import Job with ID %s Completed", job.id); }; - const $handleRemoveSecretsJob = async (job: TSecretSyncRemoveSecretsDTO) => { + const $handleRemoveSecretsJob = async (job: TSecretSyncRemoveSecretsDTO, secretSync: TSecretSyncRaw) => { const { data: { syncId, auditLogInfo, deleteSyncOnComplete } } = job; - const secretSync = await secretSyncDAL.findById(syncId); - - if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`); - await enterpriseSyncCheck( licenseService, secretSync.destination as SecretSync, @@ -713,13 +746,14 @@ export const secretSyncQueueFactory = ({ try { const { - connection: { orgId, encryptedCredentials } + connection: { orgId, encryptedCredentials, projectId } } = secretSync; const credentials = await decryptAppConnectionCredentials({ orgId, encryptedCredentials, - kmsService + kmsService, + projectId }); const secretMap = await $getInfisicalSecrets(secretSync); @@ -736,7 +770,8 @@ export const secretSyncQueueFactory = ({ { appConnectionDAL, kmsService, - gatewayService + gatewayService, + gatewayV2Service } ); @@ -894,6 +929,17 @@ export const secretSyncQueueFactory = ({ const secretSyncs = await secretSyncDAL.find({ folderId: folder.id, isAutoSyncEnabled: true }); + await secretSyncDAL.update( + { + $in: { + id: secretSyncs.map((sync) => sync.id) + } + }, + { + syncStatus: SecretSyncStatus.Pending + } + ); + await Promise.all(secretSyncs.map((secretSync) => queueSecretSyncSyncSecretsById({ syncId: secretSync.id }))); }; @@ -904,7 +950,7 @@ export const secretSyncQueueFactory = ({ case QueueJobs.SecretSyncSyncSecrets: { const { failedToAcquireLockCount = 0, ...rest } = job.data as TQueueSecretSyncSyncSecretsByIdDTO; - if (failedToAcquireLockCount < 10) { + if (failedToAcquireLockCount < REQUEUE_LIMIT) { await queueSecretSyncSyncSecretsById({ ...rest, failedToAcquireLockCount: failedToAcquireLockCount + 1 }); return; } @@ -974,6 +1020,26 @@ export const secretSyncQueueFactory = ({ | TQueueSecretSyncImportSecretsByIdDTO | TQueueSecretSyncRemoveSecretsByIdDTO; + const secretSync = await secretSyncDAL.findById(syncId); + + if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`); + + const { connectionId } = secretSync; + + if (job.name === QueueJobs.SecretSyncSyncSecrets) { + const isConcurrentLimitReached = await $isConnectionConcurrencyLimitReached(connectionId); + + if (isConcurrentLimitReached) { + logger.info( + `SecretSync Concurrency limit reached [syncId=${syncId}] [job=${job.name}] [connectionId=${connectionId}]` + ); + + await $handleAcquireLockFailure(job as SecretSyncActionJob); + + return; + } + } + let lock: Awaited>; try { @@ -993,20 +1059,26 @@ export const secretSyncQueueFactory = ({ try { switch (job.name) { - case QueueJobs.SecretSyncSyncSecrets: - await $handleSyncSecretsJob(job as TSecretSyncSyncSecretsDTO); + case QueueJobs.SecretSyncSyncSecrets: { + await $incrementConnectionConcurrencyCount(connectionId); + await $handleSyncSecretsJob(job as TSecretSyncSyncSecretsDTO, secretSync); break; + } case QueueJobs.SecretSyncImportSecrets: - await $handleImportSecretsJob(job as TSecretSyncImportSecretsDTO); + await $handleImportSecretsJob(job as TSecretSyncImportSecretsDTO, secretSync); break; case QueueJobs.SecretSyncRemoveSecrets: - await $handleRemoveSecretsJob(job as TSecretSyncRemoveSecretsDTO); + await $handleRemoveSecretsJob(job as TSecretSyncRemoveSecretsDTO, secretSync); break; default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled Secret Sync Job ${job.name}`); } } finally { + if (job.name === QueueJobs.SecretSyncSyncSecrets) { + await $decrementConnectionConcurrencyCount(connectionId); + } + await lock.release(); } }); diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index 3fdb7fea6..ecd7d04a5 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -41,7 +41,7 @@ import { TSecretSyncQueueFactory } from "./secret-sync-queue"; type TSecretSyncServiceFactoryDep = { secretSyncDAL: TSecretSyncDALFactory; secretImportDAL: TSecretImportDALFactory; - appConnectionService: Pick; + appConnectionService: Pick; permissionService: Pick; projectBotService: Pick; folderDAL: Pick; @@ -267,7 +267,11 @@ export const secretSyncServiceFactory = ({ const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination]; // validates permission to connect and app is valid for sync destination - await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor); + await appConnectionService.validateAppConnectionUsageById( + destinationApp, + { connectionId: params.connectionId, projectId }, + actor + ); try { const secretSync = await secretSyncDAL.create({ @@ -362,7 +366,11 @@ export const secretSyncServiceFactory = ({ const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync]; // validates permission to connect and app is valid for sync destination - await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor); + await appConnectionService.validateAppConnectionUsageById( + destinationApp, + { connectionId: params.connectionId, projectId: secretSync.projectId }, + actor + ); } if ( diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 8d9c6958a..93afb3b55 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -50,20 +50,19 @@ interface TSecretV2DalArg { } export const SECRET_DAL_TTL = () => applyJitter(10 * 60, 2 * 60); -export const SECRET_DAL_VERSION_TTL = 15 * 60; +export const SECRET_DAL_VERSION_TTL = "15m"; export const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024; export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { const secretOrm = ormify(db, TableName.SecretV2); - const invalidateSecretCacheByProjectId = async (projectId: string) => { + const invalidateSecretCacheByProjectId = async (projectId: string, tx?: Knex) => { const secretDalVersionKey = SecretServiceCacheKeys.getSecretDalVersion(projectId); - await keyStore.incrementBy(secretDalVersionKey, 1); - await keyStore.setExpiry(secretDalVersionKey, SECRET_DAL_VERSION_TTL); + await keyStore.pgIncrementBy(secretDalVersionKey, { incr: 1, tx, expiry: SECRET_DAL_VERSION_TTL }); }; const findOne = async (filter: Partial, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.SecretV2) + const docs = await (tx || db.replicaNode())(TableName.SecretV2) // eslint-disable-next-line @typescript-eslint/no-misused-promises .where(buildFindFilter(filter, TableName.SecretV2)) .leftJoin( @@ -144,7 +143,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { const find = async (filter: TFindFilter, opts: TFindOpt = {}) => { const { offset, limit, sort, tx } = opts; try { - const query = (tx || db)(TableName.SecretV2) + const query = (tx || db.replicaNode())(TableName.SecretV2) // eslint-disable-next-line @typescript-eslint/no-misused-promises .where(buildFindFilter(filter)) .leftJoin( @@ -888,13 +887,13 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { const findSecretsWithReminderRecipients = async (ids: string[], limit: number, tx?: Knex) => { try { // Create a subquery to get limited secret IDs - const limitedSecretIds = (tx || db)(TableName.SecretV2) + const limitedSecretIds = (tx || db.replicaNode())(TableName.SecretV2) .whereIn(`${TableName.SecretV2}.id`, ids) .limit(limit) .select("id"); // Join with all recipients for the limited secrets - const docs = await (tx || db)(TableName.SecretV2) + const docs = await (tx || db.replicaNode())(TableName.SecretV2) .whereIn(`${TableName.SecretV2}.id`, limitedSecretIds) .leftJoin(TableName.Reminder, `${TableName.SecretV2}.id`, `${TableName.Reminder}.secretId`) .leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`) @@ -926,13 +925,13 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { const findSecretsWithReminderRecipientsOld = async (ids: string[], limit: number, tx?: Knex) => { try { // Create a subquery to get limited secret IDs - const limitedSecretIds = (tx || db)(TableName.SecretV2) + const limitedSecretIds = (tx || db.replicaNode())(TableName.SecretV2) .whereIn(`${TableName.SecretV2}.id`, ids) .limit(limit) .select("id"); // Join with all recipients for the limited secrets - const docs = await (tx || db)(TableName.SecretV2) + const docs = await (tx || db.replicaNode())(TableName.SecretV2) .whereIn(`${TableName.SecretV2}.id`, limitedSecretIds) .leftJoin(TableName.Reminder, `${TableName.SecretV2}.id`, `${TableName.Reminder}.secretId`) .leftJoin( diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index b23cd0c56..9db535fbe 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -108,7 +108,7 @@ type TSecretV2BridgeServiceFactoryDep = { | "findBySecretPathMultiEnv" | "findSecretPathByFolderIds" >; - secretImportDAL: Pick; + secretImportDAL: Pick; secretQueueService: Pick; secretApprovalPolicyService: Pick; secretApprovalRequestDAL: Pick; @@ -118,7 +118,7 @@ type TSecretV2BridgeServiceFactoryDep = { >; snapshotService: Pick; resourceMetadataDAL: Pick; - keyStore: Pick; + keyStore: Pick; reminderService: Pick; }; @@ -360,6 +360,7 @@ export const secretV2BridgeServiceFactory = ({ tx }); + await secretDAL.invalidateSecretCacheByProjectId(projectId, tx); return createdSecret; }); @@ -377,7 +378,6 @@ export const secretV2BridgeServiceFactory = ({ }); } - await secretDAL.invalidateSecretCacheByProjectId(projectId); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -566,8 +566,8 @@ export const secretV2BridgeServiceFactory = ({ await $validateSecretReferences(projectId, permission, allSecretReferences); } - const updatedSecret = await secretDAL.transaction(async (tx) => - fnSecretBulkUpdate({ + const updatedSecret = await secretDAL.transaction(async (tx) => { + const modifiedSecretsInDB = await fnSecretBulkUpdate({ folderId, orgId: actorOrgId, resourceMetadataDAL, @@ -598,8 +598,11 @@ export const secretV2BridgeServiceFactory = ({ actorId }, tx - }) - ); + }); + + await secretDAL.invalidateSecretCacheByProjectId(projectId, tx); + return modifiedSecretsInDB; + }); if (inputSecret.secretReminderRepeatDays) { await reminderService.createReminder({ actor, @@ -615,7 +618,6 @@ export const secretV2BridgeServiceFactory = ({ }); } - await secretDAL.invalidateSecretCacheByProjectId(projectId); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -715,8 +717,8 @@ export const secretV2BridgeServiceFactory = ({ ); try { - const deletedSecret = await secretDAL.transaction(async (tx) => - fnSecretBulkDelete({ + const deletedSecret = await secretDAL.transaction(async (tx) => { + const modifiedSecretsInDB = await fnSecretBulkDelete({ projectId, folderId, actorId, @@ -732,10 +734,11 @@ export const secretV2BridgeServiceFactory = ({ } ], tx - }) - ); + }); + await secretDAL.invalidateSecretCacheByProjectId(projectId, tx); + return modifiedSecretsInDB; + }); - await secretDAL.invalidateSecretCacheByProjectId(projectId); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -1027,7 +1030,7 @@ export const secretV2BridgeServiceFactory = ({ }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); - const cachedSecretDalVersion = await keyStore.getItem(SecretServiceCacheKeys.getSecretDalVersion(projectId)); + const cachedSecretDalVersion = await keyStore.pgGetIntItem(SecretServiceCacheKeys.getSecretDalVersion(projectId)); const secretDalVersion = Number(cachedSecretDalVersion || 0); const cacheKey = SecretServiceCacheKeys.getSecretsOfServiceLayer(projectId, secretDalVersion, { ...dto, @@ -1692,7 +1695,7 @@ export const secretV2BridgeServiceFactory = ({ await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); const executeBulkInsert = async (tx: Knex) => { - return fnSecretBulkInsert({ + const modifiedSecretsInDB = await fnSecretBulkInsert({ inputSecrets: inputSecrets.map((el) => { const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences; @@ -1728,13 +1731,14 @@ export const secretV2BridgeServiceFactory = ({ }, tx }); + await secretDAL.invalidateSecretCacheByProjectId(projectId, tx); + return modifiedSecretsInDB; }; const newSecrets = providedTx ? await executeBulkInsert(providedTx) : await secretDAL.transaction(executeBulkInsert); - await secretDAL.invalidateSecretCacheByProjectId(projectId); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ actor, @@ -2099,6 +2103,7 @@ export const secretV2BridgeServiceFactory = ({ } } + await secretDAL.invalidateSecretCacheByProjectId(projectId, tx); return updatedSecrets; }; @@ -2106,7 +2111,6 @@ export const secretV2BridgeServiceFactory = ({ ? await executeBulkUpdate(providedTx) : await secretDAL.transaction(executeBulkUpdate); - await secretDAL.invalidateSecretCacheByProjectId(projectId); await Promise.allSettled(folders.map((el) => (el?.id ? snapshotService.performSnapshot(el.id) : undefined))); await Promise.allSettled( folders.map((el) => @@ -2233,7 +2237,7 @@ export const secretV2BridgeServiceFactory = ({ }); const executeBulkDelete = async (tx: Knex) => { - return fnSecretBulkDelete({ + const modifiedSecretsInDB = await fnSecretBulkDelete({ secretDAL, secretQueueService, folderCommitService, @@ -2249,6 +2253,8 @@ export const secretV2BridgeServiceFactory = ({ commitChanges, tx }); + await secretDAL.invalidateSecretCacheByProjectId(projectId, tx); + return modifiedSecretsInDB; }; try { @@ -2256,7 +2262,6 @@ export const secretV2BridgeServiceFactory = ({ ? await executeBulkDelete(providedTx) : await secretDAL.transaction(executeBulkDelete); - await secretDAL.invalidateSecretCacheByProjectId(projectId); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ actor, diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index 9537b79e3..0282fa537 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -72,7 +72,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { .where(`${TableName.SecretVersionV2}.folderId`, folderId) .join(TableName.SecretV2, `${TableName.SecretV2}.id`, `${TableName.SecretVersionV2}.secretId`) .join( - (tx || db)(TableName.SecretVersionV2) + (tx || db.replicaNode())(TableName.SecretVersionV2) .where(`${TableName.SecretVersionV2}.folderId`, folderId) .groupBy("secretId") .max("version") @@ -121,7 +121,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { .where("folderId", folderId) .whereIn(`${TableName.SecretVersionV2}.secretId`, secretIds) .join( - (tx || db)(TableName.SecretVersionV2) + (tx || db.replicaNode())(TableName.SecretVersionV2) .groupBy("secretId") .max("version") .select("secretId") @@ -189,7 +189,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { }) => { try { const { offset, limit, sort = [["createdAt", "desc"]] } = findOpt; - const query = (tx || db)(TableName.SecretVersionV2) + const query = (tx || db.replicaNode())(TableName.SecretVersionV2) .leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`) .leftJoin( TableName.ProjectMembership, diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index dc5713901..ed794a664 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -86,7 +86,7 @@ type TSecretQueueFactoryDep = { integrationAuthService: Pick; folderDAL: TSecretFolderDALFactory; secretDAL: TSecretDALFactory; - secretImportDAL: Pick; + secretImportDAL: Pick; webhookDAL: Pick; projectEnvDAL: Pick; projectDAL: TProjectDALFactory; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 6bafa3ba6..8758a992e 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -2970,14 +2970,23 @@ export const secretServiceFactory = ({ actor, actorId, actorAuthMethod, - actorOrgId + actorOrgId, + projectId: inputProjectId }: TMoveSecretsDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + let project; + if (projectSlug) { + project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + } else if (inputProjectId) { + project = await projectDAL.findById(inputProjectId); + } + if (!project) { throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); } + + const projectId = project.id; if (project.version === ProjectVersion.V3) { return secretV2BridgeService.moveSecrets({ sourceEnvironment, @@ -3170,7 +3179,7 @@ export const secretServiceFactory = ({ }); } const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( - project.id, + projectId, destinationFolder.environment.slug, destinationFolder.path ); @@ -3257,7 +3266,7 @@ export const secretServiceFactory = ({ } if (locallyUpdatedSecrets.length) { await fnSecretBulkUpdate({ - projectId: project.id, + projectId, folderId: destinationFolder.id, secretVersionDAL, secretDAL, @@ -3300,7 +3309,7 @@ export const secretServiceFactory = ({ const locallyDeletedSecrets = decryptedSourceSecrets.map((el) => ({ ...el, operation: SecretOperations.Delete })); const sourceFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( - project.id, + projectId, sourceFolder.environment.slug, sourceFolder.path ); diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index cd341a8b5..07f46bf1e 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -534,7 +534,8 @@ export type TSyncSecretsDTO = { }); export type TMoveSecretsDTO = { - projectSlug: string; + projectId?: string; + projectSlug?: string; sourceEnvironment: string; sourceSecretPath: string; destinationEnvironment: string; diff --git a/backend/src/services/smtp/emails/OrganizationAssignmentTemplate.tsx b/backend/src/services/smtp/emails/OrganizationAssignmentTemplate.tsx new file mode 100644 index 000000000..14fb30d80 --- /dev/null +++ b/backend/src/services/smtp/emails/OrganizationAssignmentTemplate.tsx @@ -0,0 +1,69 @@ +import { Heading, Section, Text } from "@react-email/components"; +import React from "react"; + +import { BaseButton } from "./BaseButton"; +import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; + +interface OrganizationAssignmentTemplateProps extends Omit { + inviterFirstName?: string; + inviterUsername?: string; + organizationName: string; + callback_url: string; +} + +export const OrganizationAssignmentTemplate = ({ + organizationName, + inviterFirstName, + inviterUsername, + callback_url, + siteUrl +}: OrganizationAssignmentTemplateProps) => { + return ( + + + You've been added to the organization +
+ {organizationName} on Infisical +
+
+ + {inviterFirstName && inviterUsername ? ( + <> + {inviterFirstName} ( + {inviterUsername}) has added you as an + organization admin to {organizationName}. + + ) : ( + <> + An instance admin has added you as an organization admin to {organizationName}. + + )} + +
+
+ View Dashboard +
+
+ + About Infisical: Infisical is an all-in-one platform to securely manage application secrets, + certificates, SSH keys, and configurations across your team and infrastructure. + +
+
+ ); +}; + +export default OrganizationAssignmentTemplate; + +OrganizationAssignmentTemplate.PreviewProps = { + organizationName: "Example Organization", + inviterFirstName: "Jane", + inviterUsername: "jane@infisical.com", + siteUrl: "https://infisical.com", + callback_url: "https://app.infisical.com" +} as OrganizationAssignmentTemplateProps; diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts index 209cd672b..71c338def 100644 --- a/backend/src/services/smtp/emails/index.ts +++ b/backend/src/services/smtp/emails/index.ts @@ -9,6 +9,7 @@ export * from "./IntegrationSyncFailedTemplate"; export * from "./NewDeviceLoginTemplate"; export * from "./OrgAdminBreakglassAccessTemplate"; export * from "./OrgAdminProjectGrantAccessTemplate"; +export * from "./OrganizationAssignmentTemplate"; export * from "./OrganizationInvitationTemplate"; export * from "./PasswordResetTemplate"; export * from "./PasswordSetupTemplate"; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 500d0d89c..224f78265 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -18,6 +18,7 @@ import { NewDeviceLoginTemplate, OrgAdminBreakglassAccessTemplate, OrgAdminProjectGrantAccessTemplate, + OrganizationAssignmentTemplate, OrganizationInvitationTemplate, PasswordResetTemplate, PasswordSetupTemplate, @@ -61,6 +62,7 @@ export enum SmtpTemplates { // HistoricalSecretList = "historicalSecretLeakIncident", not used anymore? NewDeviceJoin = "newDevice", OrgInvite = "organizationInvitation", + OrgAssignment = "organizationAssignment", ResetPassword = "passwordReset", SetupPassword = "passwordSetup", SecretLeakIncident = "secretLeakIncident", @@ -94,6 +96,7 @@ export enum SmtpHost { // eslint-disable-next-line @typescript-eslint/no-explicit-any const EmailTemplateMap: Record> = { [SmtpTemplates.OrgInvite]: OrganizationInvitationTemplate, + [SmtpTemplates.OrgAssignment]: OrganizationAssignmentTemplate, [SmtpTemplates.NewDeviceJoin]: NewDeviceLoginTemplate, [SmtpTemplates.SignupEmailVerification]: SignupEmailVerificationTemplate, [SmtpTemplates.EmailMfa]: EmailMfaTemplate, diff --git a/backend/src/services/super-admin/super-admin-dal.ts b/backend/src/services/super-admin/super-admin-dal.ts index d7d11a5d2..571583cc3 100644 --- a/backend/src/services/super-admin/super-admin-dal.ts +++ b/backend/src/services/super-admin/super-admin-dal.ts @@ -11,7 +11,7 @@ export const superAdminDALFactory = (db: TDbClient) => { const superAdminOrm = ormify(db, TableName.SuperAdmin); const findById = async (id: string, tx?: Knex) => { - const config = await (tx || db)(TableName.SuperAdmin) + const config = await (tx || db.replicaNode())(TableName.SuperAdmin) .where(`${TableName.SuperAdmin}.id`, id) .leftJoin(TableName.Organization, `${TableName.SuperAdmin}.defaultAuthOrgId`, `${TableName.Organization}.id`) .leftJoin(TableName.SamlConfig, (qb) => { diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 03091a255..b43e4e0a2 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,6 +1,13 @@ import { CronJob } from "cron"; -import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; +import { + IdentityAuthMethod, + OrgMembershipRole, + OrgMembershipStatus, + TSuperAdmin, + TSuperAdminUpdate, + TUsers +} from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { @@ -13,7 +20,12 @@ import { import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; +import { isDisposableEmail } from "@app/lib/validator"; +import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; +import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TAuthLoginFactory } from "../auth/auth-login-service"; import { ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type"; @@ -43,7 +55,9 @@ import { TAdminGetUsersDTO, TAdminIntegrationConfig, TAdminSignUpDTO, - TGetOrganizationsDTO + TCreateOrganizationDTO, + TGetOrganizationsDTO, + TResendOrgInviteDTO } from "./super-admin-types"; type TSuperAdminServiceFactoryDep = { @@ -59,11 +73,13 @@ type TSuperAdminServiceFactoryDep = { authService: Pick; kmsService: Pick; kmsRootConfigDAL: TKmsRootConfigDALFactory; - orgService: Pick; + orgService: Pick; keyStore: Pick; - licenseService: Pick; + licenseService: Pick; microsoftTeamsService: Pick; invalidateCacheQueue: TInvalidateCacheQueueFactory; + smtpService: Pick; + tokenService: TAuthTokenServiceFactory; }; export type TSuperAdminServiceFactory = ReturnType; @@ -123,7 +139,9 @@ export const superAdminServiceFactory = ({ identityTokenAuthDAL, identityOrgMembershipDAL, microsoftTeamsService, - invalidateCacheQueue + invalidateCacheQueue, + smtpService, + tokenService }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -732,6 +750,159 @@ export const superAdminServiceFactory = ({ return organizations; }; + const createOrganization = async ( + { name, inviteAdminEmails: emails }: TCreateOrganizationDTO, + actor: OrgServiceActor + ) => { + const appCfg = getConfig(); + + const inviteAdminEmails = [...new Set(emails)]; + + if (!appCfg.isDevelopmentMode && appCfg.isCloud) + throw new BadRequestError({ message: "This endpoint is not supported for cloud instances" }); + + const serverAdmin = await userDAL.findById(actor.id); + const plan = licenseService.onPremFeatures; + + const isEmailInvalid = await isDisposableEmail(inviteAdminEmails); + if (isEmailInvalid) { + throw new BadRequestError({ + message: "Disposable emails are not allowed", + name: "InviteUser" + }); + } + + const { organization, users: usersToEmail } = await orgDAL.transaction(async (tx) => { + const org = await orgService.createOrganization( + { + orgName: name, + userEmail: serverAdmin?.email ?? serverAdmin?.username // identities can be server admins so we can't require this + }, + tx + ); + + const users: Pick[] = []; + + for await (const inviteeEmail of inviteAdminEmails) { + const usersByUsername = await userDAL.findUserByUsername(inviteeEmail, tx); + let inviteeUser = + usersByUsername?.length > 1 + ? usersByUsername.find((el) => el.username === inviteeEmail) + : usersByUsername?.[0]; + + // if the user doesn't exist we create the user with the email + if (!inviteeUser) { + // TODO(carlos): will be removed once the function receives usernames instead of emails + const usersByEmail = await userDAL.findUserByEmail(inviteeEmail, tx); + if (usersByEmail?.length === 1) { + [inviteeUser] = usersByEmail; + } else { + inviteeUser = await userDAL.create( + { + isAccepted: false, + email: inviteeEmail, + username: inviteeEmail, + authMethods: [AuthMethod.EMAIL], + isGhost: false + }, + tx + ); + } + } + + const inviteeUserId = inviteeUser?.id; + const existingEncryptionKey = await userDAL.findUserEncKeyByUserId(inviteeUserId, tx); + + // when user is missing the encrytion keys + // this could happen either if user doesn't exist or user didn't find step 3 of generating the encryption keys of srp + // So what we do is we generate a random secure password and then encrypt it with a random pub-private key + // Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password + if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncryptionKey)) { + await userDAL.createUserEncryption( + { + userId: inviteeUserId, + encryptionVersion: 2 + }, + tx + ); + } + + if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { + // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed + throw new BadRequestError({ + name: "InviteUser", + message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members." + }); + } + + await orgDAL.createMembership( + { + userId: inviteeUser.id, + inviteEmail: inviteeEmail, + orgId: org.id, + role: OrgMembershipRole.Admin, + status: inviteeUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, + isActive: true + }, + tx + ); + + users.push(inviteeUser); + } + + return { organization: org, users }; + }); + + await licenseService.updateSubscriptionOrgMemberCount(organization.id); + + await Promise.allSettled( + usersToEmail.map(async (user) => { + if (!user.email) return; + + if (user.isAccepted) { + return smtpService.sendMail({ + template: SmtpTemplates.OrgAssignment, + subjectLine: "You've been added to an Infisical organization", + recipients: [user.email], + substitutions: { + inviterFirstName: serverAdmin?.firstName, + inviterUsername: serverAdmin?.email, + organizationName: organization.name, + email: user.email, + organizationId: organization.id, + callback_url: `${appCfg.SITE_URL}/login?org_id=${organization.id}` + } + }); + } + + // new user, send regular invite + + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_ORG_INVITATION, + userId: user.id, + orgId: organization.id + }); + + return smtpService.sendMail({ + template: SmtpTemplates.OrgInvite, + subjectLine: "Infisical organization invitation", + recipients: [user.email], + substitutions: { + inviterFirstName: serverAdmin?.firstName, + inviterUsername: serverAdmin?.email, + organizationName: organization.name, + email: user.email, + organizationId: organization.id, + token, + callback_url: `${appCfg.SITE_URL}/signupinvite` + } + }); + }) + ); + + return organization; + }; + const deleteOrganization = async (organizationId: string) => { const organization = await orgDAL.deleteById(organizationId); return organization; @@ -763,6 +934,86 @@ export const superAdminServiceFactory = ({ return organizationMembership; }; + const joinOrganization = async (orgId: string, actor: OrgServiceActor) => { + const serverAdmin = await userDAL.findById(actor.id); + + if (!serverAdmin) { + throw new NotFoundError({ message: "Could not find server admin user" }); + } + + const org = await orgDAL.findById(orgId); + + if (!org) { + throw new NotFoundError({ message: `Could not organization with ID "${orgId}"` }); + } + + const existingOrgMembership = await orgMembershipDAL.findOne({ userId: serverAdmin.id, orgId }); + + if (existingOrgMembership) { + throw new BadRequestError({ message: `You are already a part of the organization with ID ${orgId}` }); + } + + const orgMembership = await orgDAL.createMembership({ + userId: serverAdmin.id, + orgId: org.id, + role: OrgMembershipRole.Admin, + status: OrgMembershipStatus.Accepted, + isActive: true + }); + + return orgMembership; + }; + + const resendOrgInvite = async ({ organizationId, membershipId }: TResendOrgInviteDTO, actor: OrgServiceActor) => { + const orgMembership = await orgMembershipDAL.findOne({ id: membershipId, orgId: organizationId }); + + if (!orgMembership) { + throw new NotFoundError({ name: "Organization Membership", message: "Organization membership not found" }); + } + + if (orgMembership.status === OrgMembershipStatus.Accepted) { + throw new BadRequestError({ + message: "This user has already accepted their invitation." + }); + } + + if (!orgMembership.userId) { + throw new NotFoundError({ message: "Cannot find user associated with Org Membership." }); + } + + if (!orgMembership.inviteEmail) { + throw new BadRequestError({ message: "No invite email associated with user." }); + } + + const org = await orgDAL.findOrgById(orgMembership.orgId); + + const appCfg = getConfig(); + const serverAdmin = await userDAL.findById(actor.id); + + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_ORG_INVITATION, + userId: orgMembership.userId, + orgId: orgMembership.orgId + }); + + await smtpService.sendMail({ + template: SmtpTemplates.OrgInvite, + subjectLine: "Infisical organization invitation", + recipients: [orgMembership.inviteEmail], + substitutions: { + inviterFirstName: serverAdmin?.firstName, + inviterUsername: serverAdmin?.email, + organizationName: org?.name, + email: orgMembership.inviteEmail, + organizationId: orgMembership.orgId, + token, + callback_url: `${appCfg.SITE_URL}/signupinvite` + } + }); + + return orgMembership; + }; + const getIdentities = async ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => { const identities = await identityDAL.getIdentitiesByFilter({ limit, @@ -901,6 +1152,9 @@ export const superAdminServiceFactory = ({ initializeEnvConfigSync, getEnvOverrides, getEnvOverridesOrganized, - deleteUsers + deleteUsers, + createOrganization, + joinOrganization, + resendOrgInvite }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 919b0541c..6cfdd384d 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -34,6 +34,16 @@ export type TGetOrganizationsDTO = { searchTerm: string; }; +export type TCreateOrganizationDTO = { + name: string; + inviteAdminEmails: string[]; +}; + +export type TResendOrgInviteDTO = { + organizationId: string; + membershipId: string; +}; + export enum LoginMethod { EMAIL = "email", GOOGLE = "google", diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index 7e2027f25..a43a2f746 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -46,7 +46,7 @@ export type TSecretModifiedEvent = { properties: { numberOfSecrets: number; environment: string; - workspaceId: string; + projectId: string; secretPath: string; channel?: string; userAgent?: string; diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 0f623dff1..4267d13ee 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -19,12 +19,16 @@ export type TUserDALFactory = ReturnType; export const userDALFactory = (db: TDbClient) => { const userOrm = ormify(db, TableName.Users); const findUserByUsername = async (username: string, tx?: Knex) => - (tx || db)(TableName.Users).whereRaw('lower("username") = :username', { username: username.toLowerCase() }); + (tx || db.replicaNode())(TableName.Users).whereRaw('lower("username") = :username', { + username: username.toLowerCase() + }); const findUserByEmail = async (email: string, tx?: Knex) => - (tx || db)(TableName.Users).whereRaw('lower("email") = :email', { email: email.toLowerCase() }).where({ - isEmailVerified: true - }); + (tx || db.replicaNode())(TableName.Users) + .whereRaw('lower("email") = :email', { email: email.toLowerCase() }) + .where({ + isEmailVerified: true + }); const getUsersByFilter = async ({ limit, diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index b0d7be0fe..f97e98fa3 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import { Knex } from "knex"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -7,6 +8,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/ import { logger } from "@app/lib/logger"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; @@ -15,7 +17,7 @@ import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { TUserDALFactory } from "./user-dal"; -import { TListUserGroupsDTO, TUpdateUserMfaDTO } from "./user-types"; +import { TListUserGroupsDTO, TUpdateUserEmailDTO, TUpdateUserMfaDTO } from "./user-types"; type TUserServiceFactoryDep = { userDAL: Pick< @@ -34,18 +36,20 @@ type TUserServiceFactoryDep = { | "findAllMyAccounts" >; groupProjectDAL: Pick; + orgDAL: Pick; orgMembershipDAL: Pick; - tokenService: Pick; + tokenService: Pick; projectMembershipDAL: Pick; smtpService: Pick; permissionService: TPermissionServiceFactory; - userAliasDAL: Pick; + userAliasDAL: Pick; }; export type TUserServiceFactory = ReturnType; export const userServiceFactory = ({ userDAL, + orgDAL, orgMembershipDAL, projectMembershipDAL, groupProjectDAL, @@ -178,6 +182,135 @@ export const userServiceFactory = ({ return updatedUser; }; + const checkUserScimRestriction = async (userId: string, tx?: Knex) => { + const userOrgs = await orgMembershipDAL.find({ userId }, { tx }); + + if (userOrgs.length === 0) { + return false; + } + + const orgIds = userOrgs.map((membership) => membership.orgId); + const organizations = await orgDAL.find({ $in: { id: orgIds } }, { tx }); + + return organizations.some((org) => org.scimEnabled); + }; + + const requestEmailChangeOTP = async ({ userId, newEmail }: TUpdateUserEmailDTO) => { + const startTime = new Date(); + const changeEmailOTP = await userDAL.transaction(async (tx) => { + const user = await userDAL.findById(userId, tx); + if (!user) + throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "RequestEmailChangeOTP" }); + + if (user.authMethods?.includes(AuthMethod.LDAP)) { + throw new BadRequestError({ message: "Cannot update email for LDAP users", name: "RequestEmailChangeOTP" }); + } + + const hasScimRestriction = await checkUserScimRestriction(userId, tx); + if (hasScimRestriction) { + throw new BadRequestError({ + message: "Email changes are disabled because SCIM is enabled for one or more of your organizations", + name: "RequestEmailChangeOTP" + }); + } + + // Silently check if another user already has this email - don't send OTP if email is taken + const existingUsers = await userDAL.findUserByUsername(newEmail.toLowerCase(), tx); + const existingUser = existingUsers?.find((u) => u.id !== userId); + if (!existingUser) { + // Generate 6-digit OTP + const otpCode = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_CHANGE_OTP, + userId, + payload: newEmail.toLowerCase() + }); + + // Send OTP to NEW email address + await smtpService.sendMail({ + template: SmtpTemplates.EmailVerification, + subjectLine: "Infisical email change verification", + recipients: [newEmail.toLowerCase()], + substitutions: { + code: otpCode + } + }); + } + + return { success: true, message: "Verification code sent to new email address" }; + }); + // Force this function to have a minimum execution time of 2 seconds to avoid possible information disclosure about existing users + const endTime = new Date(); + const timeDiff = endTime.getTime() - startTime.getTime(); + if (timeDiff < 2000) { + await new Promise((resolve) => { + setTimeout(resolve, 2000 - timeDiff); + }); + } + return changeEmailOTP; + }; + + const updateUserEmail = async ({ userId, newEmail, otpCode }: TUpdateUserEmailDTO & { otpCode: string }) => { + const changedUser = await userDAL.transaction(async (tx) => { + const user = await userDAL.findById(userId, tx); + if (!user) throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "UpdateUserEmail" }); + + if (user.authMethods?.includes(AuthMethod.LDAP)) { + throw new BadRequestError({ message: "Cannot update email for LDAP users", name: "UpdateUserEmail" }); + } + + const hasScimRestriction = await checkUserScimRestriction(userId, tx); + if (hasScimRestriction) { + throw new BadRequestError({ + message: "You are part of an organization that has SCIM enabled, and email changes are not allowed", + name: "UpdateUserEmail" + }); + } + + // Validate OTP and get the new email from token aliasId field + let tokenData; + try { + tokenData = await tokenService.validateTokenForUser({ + type: TokenType.TOKEN_EMAIL_CHANGE_OTP, + userId, + code: otpCode + }); + } catch (error) { + throw new BadRequestError({ message: "Invalid verification code", name: "UpdateUserEmail" }); + } + + // Verify the new email matches what was stored in payload + const tokenNewEmail = tokenData?.payload; + if (!tokenNewEmail || tokenNewEmail !== newEmail.toLowerCase()) { + throw new BadRequestError({ message: "Invalid verification code", name: "UpdateUserEmail" }); + } + + // Final check if another user has this email + const existingUsers = await userDAL.findUserByUsername(newEmail.toLowerCase(), tx); + const existingUser = existingUsers?.find((u) => u.id !== userId); + if (existingUser) { + throw new BadRequestError({ message: "Email is no longer available", name: "UpdateUserEmail" }); + } + + // Delete all user aliases since the email is changing + await userAliasDAL.delete({ userId }, tx); + + const updatedUser = await userDAL.updateById( + userId, + { + email: newEmail.toLowerCase(), + username: newEmail.toLowerCase() + }, + tx + ); + + // Revoke all sessions to force re-login + await tokenService.revokeAllMySessions(userId); + + return updatedUser; + }); + return changedUser; + }; + const getAllMyAccounts = async (email: string, userId: string) => { const users = await userDAL.findAllMyAccounts(email); return users?.map((el) => ({ ...el, isMyAccount: el.id === userId })); @@ -313,6 +446,8 @@ export const userServiceFactory = ({ updateUserMfa, updateUserName, updateAuthMethods, + requestEmailChangeOTP, + updateUserEmail, deleteUser, getMe, createUserAction, diff --git a/backend/src/services/user/user-types.ts b/backend/src/services/user/user-types.ts index cef13f27a..7a974a899 100644 --- a/backend/src/services/user/user-types.ts +++ b/backend/src/services/user/user-types.ts @@ -16,3 +16,8 @@ export type TUpdateUserMfaDTO = { isMfaEnabled?: boolean; selectedMfaMethod?: MfaMethod; }; + +export type TUpdateUserEmailDTO = { + userId: string; + newEmail: string; +}; diff --git a/docs/api-reference/endpoints/deprecated/environments/create.mdx b/docs/api-reference/endpoints/deprecated/environments/create.mdx new file mode 100644 index 000000000..826dcce3d --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/environments/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/workspace/{workspaceId}/environments" +--- diff --git a/docs/api-reference/endpoints/deprecated/environments/delete.mdx b/docs/api-reference/endpoints/deprecated/environments/delete.mdx new file mode 100644 index 000000000..903e58d2a --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/environments/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/workspace/{workspaceId}/environments/{id}" +--- diff --git a/docs/api-reference/endpoints/deprecated/environments/update.mdx b/docs/api-reference/endpoints/deprecated/environments/update.mdx new file mode 100644 index 000000000..f93968668 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/environments/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/workspace/{workspaceId}/environments/{id}" +--- diff --git a/docs/api-reference/endpoints/deprecated/folders/create.mdx b/docs/api-reference/endpoints/deprecated/folders/create.mdx new file mode 100644 index 000000000..e1ff3004a --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/folders/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/folders" +--- diff --git a/docs/api-reference/endpoints/deprecated/folders/delete.mdx b/docs/api-reference/endpoints/deprecated/folders/delete.mdx new file mode 100644 index 000000000..a106cc2eb --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/folders/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/folders/{folderIdOrName}" +--- diff --git a/docs/api-reference/endpoints/deprecated/folders/get-by-id.mdx b/docs/api-reference/endpoints/deprecated/folders/get-by-id.mdx new file mode 100644 index 000000000..db3c4d0cc --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/folders/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/folders/{id}" +--- diff --git a/docs/api-reference/endpoints/deprecated/folders/list.mdx b/docs/api-reference/endpoints/deprecated/folders/list.mdx new file mode 100644 index 000000000..f40f93273 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/folders/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/folders" +--- diff --git a/docs/api-reference/endpoints/deprecated/folders/update.mdx b/docs/api-reference/endpoints/deprecated/folders/update.mdx new file mode 100644 index 000000000..c54778e94 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/folders/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/folders/{folderId}" +--- diff --git a/docs/api-reference/endpoints/organizations/workspaces.mdx b/docs/api-reference/endpoints/deprecated/organizations/projects.mdx similarity index 100% rename from docs/api-reference/endpoints/organizations/workspaces.mdx rename to docs/api-reference/endpoints/deprecated/organizations/projects.mdx diff --git a/docs/api-reference/endpoints/deprecated/project-groups/create.mdx b/docs/api-reference/endpoints/deprecated/project-groups/create.mdx new file mode 100644 index 000000000..6dd7f1a4f --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-groups/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create Project Membership" +openapi: "POST /api/v2/workspace/{projectId}/groups/{groupIdOrName}" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-groups/delete.mdx b/docs/api-reference/endpoints/deprecated/project-groups/delete.mdx new file mode 100644 index 000000000..07db40d7f --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-groups/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete Project Membership" +openapi: "DELETE /api/v2/workspace/{projectId}/groups/{groupId}" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-groups/get-by-id.mdx b/docs/api-reference/endpoints/deprecated/project-groups/get-by-id.mdx new file mode 100644 index 000000000..611f39059 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-groups/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Project Membership" +openapi: "GET /api/v2/workspace/{projectId}/groups/{groupId}" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-groups/list.mdx b/docs/api-reference/endpoints/deprecated/project-groups/list.mdx new file mode 100644 index 000000000..1488fb6ae --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-groups/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List Project Memberships" +openapi: "GET /api/v2/workspace/{projectId}/groups" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-groups/update.mdx b/docs/api-reference/endpoints/deprecated/project-groups/update.mdx new file mode 100644 index 000000000..8b963a1d8 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-groups/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update Project Membership" +openapi: "PATCH /api/v2/workspace/{projectId}/groups/{groupId}" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-identities/add-identity-membership.mdx b/docs/api-reference/endpoints/deprecated/project-identities/add-identity-membership.mdx new file mode 100644 index 000000000..285b1d1c4 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-identities/add-identity-membership.mdx @@ -0,0 +1,4 @@ +--- +title: "Create Identity Membership" +openapi: "POST /api/v2/workspace/{projectId}/identity-memberships/{identityId}" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-identities/delete-identity-membership.mdx b/docs/api-reference/endpoints/deprecated/project-identities/delete-identity-membership.mdx new file mode 100644 index 000000000..e2b266626 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-identities/delete-identity-membership.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete Identity Membership" +openapi: "DELETE /api/v2/workspace/{projectId}/identity-memberships/{identityId}" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-identities/get-by-id.mdx b/docs/api-reference/endpoints/deprecated/project-identities/get-by-id.mdx new file mode 100644 index 000000000..37f4192d7 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-identities/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Identity by ID" +openapi: "GET /api/v2/workspace/{projectId}/identity-memberships/{identityId}" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-identities/list-identity-memberships.mdx b/docs/api-reference/endpoints/deprecated/project-identities/list-identity-memberships.mdx new file mode 100644 index 000000000..e5162e693 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-identities/list-identity-memberships.mdx @@ -0,0 +1,4 @@ +--- +title: "List Identity Memberships" +openapi: "GET /api/v2/workspace/{projectId}/identity-memberships" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-identities/update-identity-membership.mdx b/docs/api-reference/endpoints/deprecated/project-identities/update-identity-membership.mdx new file mode 100644 index 000000000..667cf7eb3 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-identities/update-identity-membership.mdx @@ -0,0 +1,4 @@ +--- +title: "Update Identity Membership" +openapi: "PATCH /api/v2/workspace/{projectId}/identity-memberships/{identityId}" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-roles/create.mdx b/docs/api-reference/endpoints/deprecated/project-roles/create.mdx new file mode 100644 index 000000000..97570ec36 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-roles/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v2/workspace/{projectId}/roles" +--- + + + You can read more about the permissions field in the [permissions + documentation](/internals/permissions). + + diff --git a/docs/api-reference/endpoints/deprecated/project-roles/delete.mdx b/docs/api-reference/endpoints/deprecated/project-roles/delete.mdx new file mode 100644 index 000000000..41edfa7c3 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-roles/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v2/workspace/{projectId}/roles/{roleId}" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-roles/get-by-slug.mdx b/docs/api-reference/endpoints/deprecated/project-roles/get-by-slug.mdx new file mode 100644 index 000000000..dfc5c582a --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-roles/get-by-slug.mdx @@ -0,0 +1,4 @@ +--- +title: "Get By Slug" +openapi: "GET /api/v2/workspace/{projectId}/roles/slug/{roleSlug}" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-roles/list.mdx b/docs/api-reference/endpoints/deprecated/project-roles/list.mdx new file mode 100644 index 000000000..8d8dc10c1 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-roles/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/workspace/{projectId}/roles" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-roles/update.mdx b/docs/api-reference/endpoints/deprecated/project-roles/update.mdx new file mode 100644 index 000000000..662d5e617 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-roles/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v2/workspace/{projectId}/roles/{roleId}" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-users/delete-membership.mdx b/docs/api-reference/endpoints/deprecated/project-users/delete-membership.mdx new file mode 100644 index 000000000..1995e4726 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-users/delete-membership.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete User Membership" +openapi: "DELETE /api/v1/workspace/{workspaceId}/memberships/{membershipId}" +--- diff --git a/docs/api-reference/endpoints/deprecated/project-users/get-by-username.mdx b/docs/api-reference/endpoints/deprecated/project-users/get-by-username.mdx new file mode 100644 index 000000000..ec69d4947 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-users/get-by-username.mdx @@ -0,0 +1,4 @@ +--- +title: "Get By Username" +openapi: "POST /api/v1/workspace/{workspaceId}/memberships/details" +--- diff --git a/docs/api-reference/endpoints/project-users/invite-member-to-workspace.mdx b/docs/api-reference/endpoints/deprecated/project-users/invite-member-to-project.mdx similarity index 100% rename from docs/api-reference/endpoints/project-users/invite-member-to-workspace.mdx rename to docs/api-reference/endpoints/deprecated/project-users/invite-member-to-project.mdx diff --git a/docs/api-reference/endpoints/deprecated/project-users/memberships.mdx b/docs/api-reference/endpoints/deprecated/project-users/memberships.mdx new file mode 100644 index 000000000..3c4735f94 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-users/memberships.mdx @@ -0,0 +1,4 @@ +--- +title: "Get User Memberships" +openapi: "GET /api/v1/workspace/{workspaceId}/memberships" +--- diff --git a/docs/api-reference/endpoints/project-users/remove-member-from-workspace.mdx b/docs/api-reference/endpoints/deprecated/project-users/remove-member-from-project.mdx similarity index 100% rename from docs/api-reference/endpoints/project-users/remove-member-from-workspace.mdx rename to docs/api-reference/endpoints/deprecated/project-users/remove-member-from-project.mdx diff --git a/docs/api-reference/endpoints/deprecated/project-users/update-membership.mdx b/docs/api-reference/endpoints/deprecated/project-users/update-membership.mdx new file mode 100644 index 000000000..9a7aa600f --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/project-users/update-membership.mdx @@ -0,0 +1,4 @@ +--- +title: "Update User Membership" +openapi: "PATCH /api/v1/workspace/{workspaceId}/memberships/{membershipId}" +--- diff --git a/docs/api-reference/endpoints/workspaces/create-workspace.mdx b/docs/api-reference/endpoints/deprecated/projects/create-project.mdx similarity index 100% rename from docs/api-reference/endpoints/workspaces/create-workspace.mdx rename to docs/api-reference/endpoints/deprecated/projects/create-project.mdx diff --git a/docs/api-reference/endpoints/workspaces/delete-workspace.mdx b/docs/api-reference/endpoints/deprecated/projects/delete-project.mdx similarity index 100% rename from docs/api-reference/endpoints/workspaces/delete-workspace.mdx rename to docs/api-reference/endpoints/deprecated/projects/delete-project.mdx diff --git a/docs/api-reference/endpoints/workspaces/get-workspace-by-slug.mdx b/docs/api-reference/endpoints/deprecated/projects/get-project-by-slug.mdx similarity index 100% rename from docs/api-reference/endpoints/workspaces/get-workspace-by-slug.mdx rename to docs/api-reference/endpoints/deprecated/projects/get-project-by-slug.mdx diff --git a/docs/api-reference/endpoints/workspaces/get-workspace.mdx b/docs/api-reference/endpoints/deprecated/projects/get-project.mdx similarity index 94% rename from docs/api-reference/endpoints/workspaces/get-workspace.mdx rename to docs/api-reference/endpoints/deprecated/projects/get-project.mdx index edd0a0276..a27d41b43 100644 --- a/docs/api-reference/endpoints/workspaces/get-workspace.mdx +++ b/docs/api-reference/endpoints/deprecated/projects/get-project.mdx @@ -1,4 +1,4 @@ --- title: "Get Project" openapi: "GET /api/v1/workspace/{workspaceId}" ---- \ No newline at end of file +--- diff --git a/docs/api-reference/endpoints/workspaces/workspace-key.mdx b/docs/api-reference/endpoints/deprecated/projects/project-key.mdx similarity index 100% rename from docs/api-reference/endpoints/workspaces/workspace-key.mdx rename to docs/api-reference/endpoints/deprecated/projects/project-key.mdx diff --git a/docs/api-reference/endpoints/workspaces/update-workspace.mdx b/docs/api-reference/endpoints/deprecated/projects/project-workspace.mdx similarity index 100% rename from docs/api-reference/endpoints/workspaces/update-workspace.mdx rename to docs/api-reference/endpoints/deprecated/projects/project-workspace.mdx diff --git a/docs/api-reference/endpoints/workspaces/rollback-snapshot.mdx b/docs/api-reference/endpoints/deprecated/projects/rollback-snapshot.mdx similarity index 100% rename from docs/api-reference/endpoints/workspaces/rollback-snapshot.mdx rename to docs/api-reference/endpoints/deprecated/projects/rollback-snapshot.mdx diff --git a/docs/api-reference/endpoints/workspaces/secret-snapshots.mdx b/docs/api-reference/endpoints/deprecated/projects/secret-snapshots.mdx similarity index 100% rename from docs/api-reference/endpoints/workspaces/secret-snapshots.mdx rename to docs/api-reference/endpoints/deprecated/projects/secret-snapshots.mdx diff --git a/docs/api-reference/endpoints/deprecated/projects/update-project.mdx b/docs/api-reference/endpoints/deprecated/projects/update-project.mdx new file mode 100644 index 000000000..699e3e3af --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/projects/update-project.mdx @@ -0,0 +1,4 @@ +--- +title: "Update Project" +openapi: "PATCH /api/v1/workspace/{workspaceId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/deprecated/secret-imports/create.mdx b/docs/api-reference/endpoints/deprecated/secret-imports/create.mdx new file mode 100644 index 000000000..3abfb320f --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secret-imports/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-imports" +--- diff --git a/docs/api-reference/endpoints/deprecated/secret-imports/delete.mdx b/docs/api-reference/endpoints/deprecated/secret-imports/delete.mdx new file mode 100644 index 000000000..cfa5960b1 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secret-imports/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-imports/{secretImportId}" +--- diff --git a/docs/api-reference/endpoints/deprecated/secret-imports/list.mdx b/docs/api-reference/endpoints/deprecated/secret-imports/list.mdx new file mode 100644 index 000000000..580d4be8d --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secret-imports/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-imports" +--- diff --git a/docs/api-reference/endpoints/deprecated/secret-imports/update.mdx b/docs/api-reference/endpoints/deprecated/secret-imports/update.mdx new file mode 100644 index 000000000..f21133223 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secret-imports/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-imports/{secretImportId}" +--- diff --git a/docs/api-reference/endpoints/deprecated/secret-tags/create.mdx b/docs/api-reference/endpoints/deprecated/secret-tags/create.mdx new file mode 100644 index 000000000..82d0eed17 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secret-tags/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/workspace/{projectId}/tags" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/deprecated/secret-tags/delete.mdx b/docs/api-reference/endpoints/deprecated/secret-tags/delete.mdx new file mode 100644 index 000000000..cc98f03c2 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secret-tags/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/workspace/{projectId}/tags/{tagId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/deprecated/secret-tags/get-by-id.mdx b/docs/api-reference/endpoints/deprecated/secret-tags/get-by-id.mdx new file mode 100644 index 000000000..de02fe133 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secret-tags/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get By ID" +openapi: "GET /api/v1/workspace/{projectId}/tags/{tagId}" +--- diff --git a/docs/api-reference/endpoints/deprecated/secret-tags/get-by-slug.mdx b/docs/api-reference/endpoints/deprecated/secret-tags/get-by-slug.mdx new file mode 100644 index 000000000..91eab730f --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secret-tags/get-by-slug.mdx @@ -0,0 +1,4 @@ +--- +title: "Get By Slug" +openapi: "GET /api/v1/workspace/{projectId}/tags/slug/{tagSlug}" +--- diff --git a/docs/api-reference/endpoints/deprecated/secret-tags/list.mdx b/docs/api-reference/endpoints/deprecated/secret-tags/list.mdx new file mode 100644 index 000000000..c4a940f77 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secret-tags/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/workspace/{projectId}/tags" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/deprecated/secret-tags/update.mdx b/docs/api-reference/endpoints/deprecated/secret-tags/update.mdx new file mode 100644 index 000000000..b9c290db8 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secret-tags/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/workspace/{projectId}/tags/{tagId}" +--- diff --git a/docs/api-reference/endpoints/secrets/attach-tags.mdx b/docs/api-reference/endpoints/deprecated/secrets/attach-tags.mdx similarity index 100% rename from docs/api-reference/endpoints/secrets/attach-tags.mdx rename to docs/api-reference/endpoints/deprecated/secrets/attach-tags.mdx diff --git a/docs/api-reference/endpoints/deprecated/secrets/create-many.mdx b/docs/api-reference/endpoints/deprecated/secrets/create-many.mdx new file mode 100644 index 000000000..227c5470d --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secrets/create-many.mdx @@ -0,0 +1,5 @@ +--- +title: "Bulk Create" +openapi: "POST /api/v3/secrets/batch/raw" +--- + diff --git a/docs/api-reference/endpoints/deprecated/secrets/create.mdx b/docs/api-reference/endpoints/deprecated/secrets/create.mdx new file mode 100644 index 000000000..afee0a719 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secrets/create.mdx @@ -0,0 +1,5 @@ +--- +title: "Create" +openapi: "POST /api/v3/secrets/raw/{secretName}" +--- + diff --git a/docs/api-reference/endpoints/deprecated/secrets/delete-many.mdx b/docs/api-reference/endpoints/deprecated/secrets/delete-many.mdx new file mode 100644 index 000000000..57c8588d8 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secrets/delete-many.mdx @@ -0,0 +1,5 @@ +--- +title: "Bulk Delete" +openapi: "DELETE /api/v3/secrets/batch/raw" +--- + diff --git a/docs/api-reference/endpoints/deprecated/secrets/delete.mdx b/docs/api-reference/endpoints/deprecated/secrets/delete.mdx new file mode 100644 index 000000000..ef3abc722 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secrets/delete.mdx @@ -0,0 +1,5 @@ +--- +title: "Delete" +openapi: "DELETE /api/v3/secrets/raw/{secretName}" +--- + diff --git a/docs/api-reference/endpoints/secrets/detach-tags.mdx b/docs/api-reference/endpoints/deprecated/secrets/detach-tags.mdx similarity index 100% rename from docs/api-reference/endpoints/secrets/detach-tags.mdx rename to docs/api-reference/endpoints/deprecated/secrets/detach-tags.mdx diff --git a/docs/api-reference/endpoints/deprecated/secrets/list.mdx b/docs/api-reference/endpoints/deprecated/secrets/list.mdx new file mode 100644 index 000000000..4808f6690 --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secrets/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v3/secrets/raw" +--- diff --git a/docs/api-reference/endpoints/deprecated/secrets/read.mdx b/docs/api-reference/endpoints/deprecated/secrets/read.mdx new file mode 100644 index 000000000..6af308e0f --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secrets/read.mdx @@ -0,0 +1,5 @@ +--- +title: "Retrieve" +openapi: "GET /api/v3/secrets/raw/{secretName}" +--- + diff --git a/docs/api-reference/endpoints/deprecated/secrets/update-many.mdx b/docs/api-reference/endpoints/deprecated/secrets/update-many.mdx new file mode 100644 index 000000000..7586d91bc --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secrets/update-many.mdx @@ -0,0 +1,5 @@ +--- +title: "Bulk Update" +openapi: "PATCH /api/v3/secrets/batch/raw" +--- + diff --git a/docs/api-reference/endpoints/deprecated/secrets/update.mdx b/docs/api-reference/endpoints/deprecated/secrets/update.mdx new file mode 100644 index 000000000..ce68c492e --- /dev/null +++ b/docs/api-reference/endpoints/deprecated/secrets/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v3/secrets/raw/{secretName}" +--- diff --git a/docs/api-reference/endpoints/environments/create.mdx b/docs/api-reference/endpoints/environments/create.mdx index 826dcce3d..c7f9b0658 100644 --- a/docs/api-reference/endpoints/environments/create.mdx +++ b/docs/api-reference/endpoints/environments/create.mdx @@ -1,4 +1,4 @@ --- title: "Create" -openapi: "POST /api/v1/workspace/{workspaceId}/environments" +openapi: "POST /api/v1/projects/{projectId}/environments" --- diff --git a/docs/api-reference/endpoints/environments/delete.mdx b/docs/api-reference/endpoints/environments/delete.mdx index 903e58d2a..2d2419255 100644 --- a/docs/api-reference/endpoints/environments/delete.mdx +++ b/docs/api-reference/endpoints/environments/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v1/workspace/{workspaceId}/environments/{id}" +openapi: "DELETE /api/v1/projects/{projectId}/environments/{id}" --- diff --git a/docs/api-reference/endpoints/environments/update.mdx b/docs/api-reference/endpoints/environments/update.mdx index f93968668..1583f5220 100644 --- a/docs/api-reference/endpoints/environments/update.mdx +++ b/docs/api-reference/endpoints/environments/update.mdx @@ -1,4 +1,4 @@ --- title: "Update" -openapi: "PATCH /api/v1/workspace/{workspaceId}/environments/{id}" +openapi: "PATCH /api/v1/projects/{projectId}/environments/{id}" --- diff --git a/docs/api-reference/endpoints/folders/create.mdx b/docs/api-reference/endpoints/folders/create.mdx index e1ff3004a..13c4abea2 100644 --- a/docs/api-reference/endpoints/folders/create.mdx +++ b/docs/api-reference/endpoints/folders/create.mdx @@ -1,4 +1,4 @@ --- title: "Create" -openapi: "POST /api/v1/folders" +openapi: "POST /api/v2/folders" --- diff --git a/docs/api-reference/endpoints/folders/delete.mdx b/docs/api-reference/endpoints/folders/delete.mdx index a106cc2eb..71ba9684c 100644 --- a/docs/api-reference/endpoints/folders/delete.mdx +++ b/docs/api-reference/endpoints/folders/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v1/folders/{folderIdOrName}" +openapi: "DELETE /api/v2/folders/{folderIdOrName}" --- diff --git a/docs/api-reference/endpoints/folders/get-by-id.mdx b/docs/api-reference/endpoints/folders/get-by-id.mdx index db3c4d0cc..ebfda35da 100644 --- a/docs/api-reference/endpoints/folders/get-by-id.mdx +++ b/docs/api-reference/endpoints/folders/get-by-id.mdx @@ -1,4 +1,4 @@ --- title: "Get by ID" -openapi: "GET /api/v1/folders/{id}" +openapi: "GET /api/v2/folders/{id}" --- diff --git a/docs/api-reference/endpoints/folders/list.mdx b/docs/api-reference/endpoints/folders/list.mdx index f40f93273..1d3a10ed8 100644 --- a/docs/api-reference/endpoints/folders/list.mdx +++ b/docs/api-reference/endpoints/folders/list.mdx @@ -1,4 +1,4 @@ --- title: "List" -openapi: "GET /api/v1/folders" +openapi: "GET /api/v2/folders" --- diff --git a/docs/api-reference/endpoints/folders/update.mdx b/docs/api-reference/endpoints/folders/update.mdx index c54778e94..969833a04 100644 --- a/docs/api-reference/endpoints/folders/update.mdx +++ b/docs/api-reference/endpoints/folders/update.mdx @@ -1,4 +1,4 @@ --- title: "Update" -openapi: "PATCH /api/v1/folders/{folderId}" +openapi: "PATCH /api/v2/folders/{folderId}" --- diff --git a/docs/api-reference/endpoints/project-groups/create.mdx b/docs/api-reference/endpoints/project-groups/create.mdx index 6dd7f1a4f..c40c506a6 100644 --- a/docs/api-reference/endpoints/project-groups/create.mdx +++ b/docs/api-reference/endpoints/project-groups/create.mdx @@ -1,4 +1,4 @@ --- title: "Create Project Membership" -openapi: "POST /api/v2/workspace/{projectId}/groups/{groupIdOrName}" +openapi: "POST /api/v1/projects/{projectId}/groups/{groupIdOrName}" --- diff --git a/docs/api-reference/endpoints/project-groups/delete.mdx b/docs/api-reference/endpoints/project-groups/delete.mdx index 07db40d7f..450e05501 100644 --- a/docs/api-reference/endpoints/project-groups/delete.mdx +++ b/docs/api-reference/endpoints/project-groups/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete Project Membership" -openapi: "DELETE /api/v2/workspace/{projectId}/groups/{groupId}" +openapi: "DELETE /api/v1/projects/{projectId}/groups/{groupId}" --- diff --git a/docs/api-reference/endpoints/project-groups/get-by-id.mdx b/docs/api-reference/endpoints/project-groups/get-by-id.mdx index 611f39059..89aa6ebac 100644 --- a/docs/api-reference/endpoints/project-groups/get-by-id.mdx +++ b/docs/api-reference/endpoints/project-groups/get-by-id.mdx @@ -1,4 +1,4 @@ --- title: "Get Project Membership" -openapi: "GET /api/v2/workspace/{projectId}/groups/{groupId}" +openapi: "GET /api/v1/projects/{projectId}/groups/{groupId}" --- diff --git a/docs/api-reference/endpoints/project-groups/list.mdx b/docs/api-reference/endpoints/project-groups/list.mdx index 1488fb6ae..90b4f957a 100644 --- a/docs/api-reference/endpoints/project-groups/list.mdx +++ b/docs/api-reference/endpoints/project-groups/list.mdx @@ -1,4 +1,4 @@ --- title: "List Project Memberships" -openapi: "GET /api/v2/workspace/{projectId}/groups" +openapi: "GET /api/v1/projects/{projectId}/groups" --- diff --git a/docs/api-reference/endpoints/project-groups/update.mdx b/docs/api-reference/endpoints/project-groups/update.mdx index 8b963a1d8..029815673 100644 --- a/docs/api-reference/endpoints/project-groups/update.mdx +++ b/docs/api-reference/endpoints/project-groups/update.mdx @@ -1,4 +1,4 @@ --- title: "Update Project Membership" -openapi: "PATCH /api/v2/workspace/{projectId}/groups/{groupId}" +openapi: "PATCH /api/v1/projects/{projectId}/groups/{groupId}" --- diff --git a/docs/api-reference/endpoints/project-identities/add-identity-membership.mdx b/docs/api-reference/endpoints/project-identities/add-identity-membership.mdx index 285b1d1c4..8b0122efd 100644 --- a/docs/api-reference/endpoints/project-identities/add-identity-membership.mdx +++ b/docs/api-reference/endpoints/project-identities/add-identity-membership.mdx @@ -1,4 +1,4 @@ --- title: "Create Identity Membership" -openapi: "POST /api/v2/workspace/{projectId}/identity-memberships/{identityId}" +openapi: "POST /api/v1/projects/{projectId}/identity-memberships/{identityId}" --- diff --git a/docs/api-reference/endpoints/project-identities/delete-identity-membership.mdx b/docs/api-reference/endpoints/project-identities/delete-identity-membership.mdx index e2b266626..10c56e5ec 100644 --- a/docs/api-reference/endpoints/project-identities/delete-identity-membership.mdx +++ b/docs/api-reference/endpoints/project-identities/delete-identity-membership.mdx @@ -1,4 +1,4 @@ --- title: "Delete Identity Membership" -openapi: "DELETE /api/v2/workspace/{projectId}/identity-memberships/{identityId}" +openapi: "DELETE /api/v1/projects/{projectId}/identity-memberships/{identityId}" --- diff --git a/docs/api-reference/endpoints/project-identities/get-by-id.mdx b/docs/api-reference/endpoints/project-identities/get-by-id.mdx index 37f4192d7..801b0d422 100644 --- a/docs/api-reference/endpoints/project-identities/get-by-id.mdx +++ b/docs/api-reference/endpoints/project-identities/get-by-id.mdx @@ -1,4 +1,4 @@ --- title: "Get Identity by ID" -openapi: "GET /api/v2/workspace/{projectId}/identity-memberships/{identityId}" +openapi: "GET /api/v1/projects/{projectId}/identity-memberships/{identityId}" --- diff --git a/docs/api-reference/endpoints/project-identities/list-identity-memberships.mdx b/docs/api-reference/endpoints/project-identities/list-identity-memberships.mdx index e5162e693..8673350fa 100644 --- a/docs/api-reference/endpoints/project-identities/list-identity-memberships.mdx +++ b/docs/api-reference/endpoints/project-identities/list-identity-memberships.mdx @@ -1,4 +1,4 @@ --- title: "List Identity Memberships" -openapi: "GET /api/v2/workspace/{projectId}/identity-memberships" +openapi: "GET /api/v1/projects/{projectId}/identity-memberships" --- diff --git a/docs/api-reference/endpoints/project-identities/update-identity-membership.mdx b/docs/api-reference/endpoints/project-identities/update-identity-membership.mdx index 667cf7eb3..ef34363d6 100644 --- a/docs/api-reference/endpoints/project-identities/update-identity-membership.mdx +++ b/docs/api-reference/endpoints/project-identities/update-identity-membership.mdx @@ -1,4 +1,4 @@ --- title: "Update Identity Membership" -openapi: "PATCH /api/v2/workspace/{projectId}/identity-memberships/{identityId}" +openapi: "PATCH /api/v1/projects/{projectId}/identity-memberships/{identityId}" --- diff --git a/docs/api-reference/endpoints/project-roles/create.mdx b/docs/api-reference/endpoints/project-roles/create.mdx index 97570ec36..1d3f9eb51 100644 --- a/docs/api-reference/endpoints/project-roles/create.mdx +++ b/docs/api-reference/endpoints/project-roles/create.mdx @@ -1,10 +1,9 @@ --- title: "Create" -openapi: "POST /api/v2/workspace/{projectId}/roles" +openapi: "POST /api/v1/projects/{projectId}/roles" --- You can read more about the permissions field in the [permissions documentation](/internals/permissions). - diff --git a/docs/api-reference/endpoints/project-roles/delete.mdx b/docs/api-reference/endpoints/project-roles/delete.mdx index 41edfa7c3..7b162c6da 100644 --- a/docs/api-reference/endpoints/project-roles/delete.mdx +++ b/docs/api-reference/endpoints/project-roles/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v2/workspace/{projectId}/roles/{roleId}" +openapi: "DELETE /api/v1/projects/{projectId}/roles/{roleId}" --- diff --git a/docs/api-reference/endpoints/project-roles/get-by-slug.mdx b/docs/api-reference/endpoints/project-roles/get-by-slug.mdx index dfc5c582a..adfac8cd3 100644 --- a/docs/api-reference/endpoints/project-roles/get-by-slug.mdx +++ b/docs/api-reference/endpoints/project-roles/get-by-slug.mdx @@ -1,4 +1,4 @@ --- title: "Get By Slug" -openapi: "GET /api/v2/workspace/{projectId}/roles/slug/{roleSlug}" +openapi: "GET /api/v1/projects/{projectId}/roles/slug/{roleSlug}" --- diff --git a/docs/api-reference/endpoints/project-roles/list.mdx b/docs/api-reference/endpoints/project-roles/list.mdx index 8d8dc10c1..df6164370 100644 --- a/docs/api-reference/endpoints/project-roles/list.mdx +++ b/docs/api-reference/endpoints/project-roles/list.mdx @@ -1,4 +1,4 @@ --- title: "List" -openapi: "GET /api/v2/workspace/{projectId}/roles" +openapi: "GET /api/v1/projects/{projectId}/roles" --- diff --git a/docs/api-reference/endpoints/project-roles/update.mdx b/docs/api-reference/endpoints/project-roles/update.mdx index 662d5e617..56ac34f8b 100644 --- a/docs/api-reference/endpoints/project-roles/update.mdx +++ b/docs/api-reference/endpoints/project-roles/update.mdx @@ -1,4 +1,4 @@ --- title: "Update" -openapi: "PATCH /api/v2/workspace/{projectId}/roles/{roleId}" +openapi: "PATCH /api/v1/projects/{projectId}/roles/{roleId}" --- diff --git a/docs/api-reference/endpoints/project-users/delete-membership.mdx b/docs/api-reference/endpoints/project-users/delete-membership.mdx index 1995e4726..5c87e94af 100644 --- a/docs/api-reference/endpoints/project-users/delete-membership.mdx +++ b/docs/api-reference/endpoints/project-users/delete-membership.mdx @@ -1,4 +1,4 @@ --- title: "Delete User Membership" -openapi: "DELETE /api/v1/workspace/{workspaceId}/memberships/{membershipId}" +openapi: "DELETE /api/v1/projects/{projectId}/memberships/{membershipId}" --- diff --git a/docs/api-reference/endpoints/project-users/get-by-username.mdx b/docs/api-reference/endpoints/project-users/get-by-username.mdx index ec69d4947..5e4916b8e 100644 --- a/docs/api-reference/endpoints/project-users/get-by-username.mdx +++ b/docs/api-reference/endpoints/project-users/get-by-username.mdx @@ -1,4 +1,4 @@ --- title: "Get By Username" -openapi: "POST /api/v1/workspace/{workspaceId}/memberships/details" +openapi: "POST /api/v1/projects/{projectId}/memberships/details" --- diff --git a/docs/api-reference/endpoints/project-users/invite-member-to-project.mdx b/docs/api-reference/endpoints/project-users/invite-member-to-project.mdx new file mode 100644 index 000000000..c6fda8a09 --- /dev/null +++ b/docs/api-reference/endpoints/project-users/invite-member-to-project.mdx @@ -0,0 +1,4 @@ +--- +title: "Invite Member" +openapi: "POST /api/v1/projects/{projectId}/memberships" +--- diff --git a/docs/api-reference/endpoints/project-users/memberships.mdx b/docs/api-reference/endpoints/project-users/memberships.mdx index 3c4735f94..dd79667fe 100644 --- a/docs/api-reference/endpoints/project-users/memberships.mdx +++ b/docs/api-reference/endpoints/project-users/memberships.mdx @@ -1,4 +1,4 @@ --- title: "Get User Memberships" -openapi: "GET /api/v1/workspace/{workspaceId}/memberships" +openapi: "GET /api/v1/projects/{projectId}/memberships" --- diff --git a/docs/api-reference/endpoints/project-users/remove-member-from-project.mdx b/docs/api-reference/endpoints/project-users/remove-member-from-project.mdx new file mode 100644 index 000000000..8646aa158 --- /dev/null +++ b/docs/api-reference/endpoints/project-users/remove-member-from-project.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Member" +openapi: "DELETE /api/v1/projects/{projectId}/memberships" +--- diff --git a/docs/api-reference/endpoints/project-users/update-membership.mdx b/docs/api-reference/endpoints/project-users/update-membership.mdx index 9a7aa600f..5f1edcf0a 100644 --- a/docs/api-reference/endpoints/project-users/update-membership.mdx +++ b/docs/api-reference/endpoints/project-users/update-membership.mdx @@ -1,4 +1,4 @@ --- title: "Update User Membership" -openapi: "PATCH /api/v1/workspace/{workspaceId}/memberships/{membershipId}" +openapi: "PATCH /api/v1/projects/{projectId}/memberships/{membershipId}" --- diff --git a/docs/api-reference/endpoints/projects/create-project.mdx b/docs/api-reference/endpoints/projects/create-project.mdx new file mode 100644 index 000000000..7ee1caff8 --- /dev/null +++ b/docs/api-reference/endpoints/projects/create-project.mdx @@ -0,0 +1,4 @@ +--- +title: "Create Project" +openapi: "POST /api/v1/projects" +--- diff --git a/docs/api-reference/endpoints/projects/delete-project.mdx b/docs/api-reference/endpoints/projects/delete-project.mdx new file mode 100644 index 000000000..eff96ded5 --- /dev/null +++ b/docs/api-reference/endpoints/projects/delete-project.mdx @@ -0,0 +1,8 @@ +--- +title: "Delete Project" +openapi: "DELETE /api/v1/projects/{projectId}" +--- + + + This operation is irreversible. All data associated with the project will be deleted. Please use with caution. + diff --git a/docs/api-reference/endpoints/projects/get-project-by-slug.mdx b/docs/api-reference/endpoints/projects/get-project-by-slug.mdx new file mode 100644 index 000000000..9bcccc31e --- /dev/null +++ b/docs/api-reference/endpoints/projects/get-project-by-slug.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Project By Slug" +openapi: "GET /api/v1/projects/slug/{slug}" +--- diff --git a/docs/api-reference/endpoints/projects/get-project.mdx b/docs/api-reference/endpoints/projects/get-project.mdx new file mode 100644 index 000000000..6ed92833e --- /dev/null +++ b/docs/api-reference/endpoints/projects/get-project.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Project" +openapi: "GET /api/v1/projects/{projectId}" +--- diff --git a/docs/api-reference/endpoints/projects/list-projects.mdx b/docs/api-reference/endpoints/projects/list-projects.mdx new file mode 100644 index 000000000..5017023a1 --- /dev/null +++ b/docs/api-reference/endpoints/projects/list-projects.mdx @@ -0,0 +1,4 @@ +--- +title: "List Projects" +openapi: "GET /api/v1/projects" +--- diff --git a/docs/api-reference/endpoints/projects/rollback-snapshot.mdx b/docs/api-reference/endpoints/projects/rollback-snapshot.mdx new file mode 100644 index 000000000..527c861b2 --- /dev/null +++ b/docs/api-reference/endpoints/projects/rollback-snapshot.mdx @@ -0,0 +1,4 @@ +--- +title: "Roll Back to Snapshot" +openapi: "POST /api/v1/secret-snapshot/{secretSnapshotId}/rollback" +--- diff --git a/docs/api-reference/endpoints/projects/secret-snapshots.mdx b/docs/api-reference/endpoints/projects/secret-snapshots.mdx new file mode 100644 index 000000000..182e61507 --- /dev/null +++ b/docs/api-reference/endpoints/projects/secret-snapshots.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Snapshots" +openapi: "GET /api/v1/projects/{projectId}/secret-snapshots" +--- diff --git a/docs/api-reference/endpoints/projects/update-project.mdx b/docs/api-reference/endpoints/projects/update-project.mdx new file mode 100644 index 000000000..99fcc7b76 --- /dev/null +++ b/docs/api-reference/endpoints/projects/update-project.mdx @@ -0,0 +1,4 @@ +--- +title: "Update Project" +openapi: "PATCH /api/v1/projects/{projectId}" +--- diff --git a/docs/api-reference/endpoints/secret-imports/create.mdx b/docs/api-reference/endpoints/secret-imports/create.mdx index 3abfb320f..f072e59a4 100644 --- a/docs/api-reference/endpoints/secret-imports/create.mdx +++ b/docs/api-reference/endpoints/secret-imports/create.mdx @@ -1,4 +1,4 @@ --- title: "Create" -openapi: "POST /api/v1/secret-imports" +openapi: "POST /api/v2/secret-imports" --- diff --git a/docs/api-reference/endpoints/secret-imports/delete.mdx b/docs/api-reference/endpoints/secret-imports/delete.mdx index cfa5960b1..d0b35426b 100644 --- a/docs/api-reference/endpoints/secret-imports/delete.mdx +++ b/docs/api-reference/endpoints/secret-imports/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v1/secret-imports/{secretImportId}" +openapi: "DELETE /api/v2/secret-imports/{secretImportId}" --- diff --git a/docs/api-reference/endpoints/secret-imports/list.mdx b/docs/api-reference/endpoints/secret-imports/list.mdx index 580d4be8d..1cf3e1386 100644 --- a/docs/api-reference/endpoints/secret-imports/list.mdx +++ b/docs/api-reference/endpoints/secret-imports/list.mdx @@ -1,4 +1,4 @@ --- title: "List" -openapi: "GET /api/v1/secret-imports" +openapi: "GET /api/v2/secret-imports" --- diff --git a/docs/api-reference/endpoints/secret-imports/update.mdx b/docs/api-reference/endpoints/secret-imports/update.mdx index f21133223..146fcd76b 100644 --- a/docs/api-reference/endpoints/secret-imports/update.mdx +++ b/docs/api-reference/endpoints/secret-imports/update.mdx @@ -1,4 +1,4 @@ --- title: "Update" -openapi: "PATCH /api/v1/secret-imports/{secretImportId}" +openapi: "PATCH /api/v2/secret-imports/{secretImportId}" --- diff --git a/docs/api-reference/endpoints/secret-tags/create.mdx b/docs/api-reference/endpoints/secret-tags/create.mdx index 82d0eed17..d4c587979 100644 --- a/docs/api-reference/endpoints/secret-tags/create.mdx +++ b/docs/api-reference/endpoints/secret-tags/create.mdx @@ -1,4 +1,4 @@ --- title: "Create" -openapi: "POST /api/v1/workspace/{projectId}/tags" ---- \ No newline at end of file +openapi: "POST /api/v1/projects/{projectId}/tags" +--- diff --git a/docs/api-reference/endpoints/secret-tags/delete.mdx b/docs/api-reference/endpoints/secret-tags/delete.mdx index cc98f03c2..121d6816c 100644 --- a/docs/api-reference/endpoints/secret-tags/delete.mdx +++ b/docs/api-reference/endpoints/secret-tags/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v1/workspace/{projectId}/tags/{tagId}" ---- \ No newline at end of file +openapi: "DELETE /api/v1/projects/{projectId}/tags/{tagId}" +--- diff --git a/docs/api-reference/endpoints/secret-tags/get-by-id.mdx b/docs/api-reference/endpoints/secret-tags/get-by-id.mdx index de02fe133..7e1fa8d7f 100644 --- a/docs/api-reference/endpoints/secret-tags/get-by-id.mdx +++ b/docs/api-reference/endpoints/secret-tags/get-by-id.mdx @@ -1,4 +1,4 @@ --- title: "Get By ID" -openapi: "GET /api/v1/workspace/{projectId}/tags/{tagId}" +openapi: "GET /api/v1/projects/{projectId}/tags/{tagId}" --- diff --git a/docs/api-reference/endpoints/secret-tags/get-by-slug.mdx b/docs/api-reference/endpoints/secret-tags/get-by-slug.mdx index 91eab730f..c012c97d4 100644 --- a/docs/api-reference/endpoints/secret-tags/get-by-slug.mdx +++ b/docs/api-reference/endpoints/secret-tags/get-by-slug.mdx @@ -1,4 +1,4 @@ --- title: "Get By Slug" -openapi: "GET /api/v1/workspace/{projectId}/tags/slug/{tagSlug}" +openapi: "GET /api/v1/projects/{projectId}/tags/slug/{tagSlug}" --- diff --git a/docs/api-reference/endpoints/secret-tags/list.mdx b/docs/api-reference/endpoints/secret-tags/list.mdx index c4a940f77..b7e1eca74 100644 --- a/docs/api-reference/endpoints/secret-tags/list.mdx +++ b/docs/api-reference/endpoints/secret-tags/list.mdx @@ -1,4 +1,4 @@ --- title: "List" -openapi: "GET /api/v1/workspace/{projectId}/tags" ---- \ No newline at end of file +openapi: "GET /api/v1/projects/{projectId}/tags" +--- diff --git a/docs/api-reference/endpoints/secret-tags/update.mdx b/docs/api-reference/endpoints/secret-tags/update.mdx index b9c290db8..b20ce49f4 100644 --- a/docs/api-reference/endpoints/secret-tags/update.mdx +++ b/docs/api-reference/endpoints/secret-tags/update.mdx @@ -1,4 +1,4 @@ --- title: "Update" -openapi: "PATCH /api/v1/workspace/{projectId}/tags/{tagId}" +openapi: "PATCH /api/v1/projects/{projectId}/tags/{tagId}" --- diff --git a/docs/api-reference/endpoints/secrets/create-many.mdx b/docs/api-reference/endpoints/secrets/create-many.mdx index 227c5470d..e3d30aef8 100644 --- a/docs/api-reference/endpoints/secrets/create-many.mdx +++ b/docs/api-reference/endpoints/secrets/create-many.mdx @@ -1,5 +1,4 @@ --- title: "Bulk Create" -openapi: "POST /api/v3/secrets/batch/raw" +openapi: "POST /api/v4/secrets/batch" --- - diff --git a/docs/api-reference/endpoints/secrets/create.mdx b/docs/api-reference/endpoints/secrets/create.mdx index afee0a719..c04dcf9f4 100644 --- a/docs/api-reference/endpoints/secrets/create.mdx +++ b/docs/api-reference/endpoints/secrets/create.mdx @@ -1,5 +1,4 @@ --- title: "Create" -openapi: "POST /api/v3/secrets/raw/{secretName}" +openapi: "POST /api/v4/secrets/{secretName}" --- - diff --git a/docs/api-reference/endpoints/secrets/delete-many.mdx b/docs/api-reference/endpoints/secrets/delete-many.mdx index 57c8588d8..49491c3b9 100644 --- a/docs/api-reference/endpoints/secrets/delete-many.mdx +++ b/docs/api-reference/endpoints/secrets/delete-many.mdx @@ -1,5 +1,4 @@ --- title: "Bulk Delete" -openapi: "DELETE /api/v3/secrets/batch/raw" +openapi: "DELETE /api/v4/secrets/batch" --- - diff --git a/docs/api-reference/endpoints/secrets/delete.mdx b/docs/api-reference/endpoints/secrets/delete.mdx index ef3abc722..f71a851e8 100644 --- a/docs/api-reference/endpoints/secrets/delete.mdx +++ b/docs/api-reference/endpoints/secrets/delete.mdx @@ -1,5 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v3/secrets/raw/{secretName}" +openapi: "DELETE /api/v4/secrets/{secretName}" --- - diff --git a/docs/api-reference/endpoints/secrets/list.mdx b/docs/api-reference/endpoints/secrets/list.mdx index 4808f6690..6a1f38d68 100644 --- a/docs/api-reference/endpoints/secrets/list.mdx +++ b/docs/api-reference/endpoints/secrets/list.mdx @@ -1,4 +1,4 @@ --- title: "List" -openapi: "GET /api/v3/secrets/raw" +openapi: "GET /api/v4/secrets" --- diff --git a/docs/api-reference/endpoints/secrets/read.mdx b/docs/api-reference/endpoints/secrets/read.mdx index 6af308e0f..10e725ba5 100644 --- a/docs/api-reference/endpoints/secrets/read.mdx +++ b/docs/api-reference/endpoints/secrets/read.mdx @@ -1,5 +1,4 @@ --- title: "Retrieve" -openapi: "GET /api/v3/secrets/raw/{secretName}" +openapi: "GET /api/v4/secrets/{secretName}" --- - diff --git a/docs/api-reference/endpoints/secrets/update-many.mdx b/docs/api-reference/endpoints/secrets/update-many.mdx index 7586d91bc..9aca17148 100644 --- a/docs/api-reference/endpoints/secrets/update-many.mdx +++ b/docs/api-reference/endpoints/secrets/update-many.mdx @@ -1,5 +1,4 @@ --- title: "Bulk Update" -openapi: "PATCH /api/v3/secrets/batch/raw" +openapi: "PATCH /api/v4/secrets/batch" --- - diff --git a/docs/api-reference/endpoints/secrets/update.mdx b/docs/api-reference/endpoints/secrets/update.mdx index ce68c492e..435f3e0c4 100644 --- a/docs/api-reference/endpoints/secrets/update.mdx +++ b/docs/api-reference/endpoints/secrets/update.mdx @@ -1,4 +1,4 @@ --- title: "Update" -openapi: "PATCH /api/v3/secrets/raw/{secretName}" +openapi: "PATCH /api/v4/secrets/{secretName}" --- diff --git a/docs/cli/commands/gateway.mdx b/docs/cli/commands/gateway.mdx index a12493c58..99d0e1086 100644 --- a/docs/cli/commands/gateway.mdx +++ b/docs/cli/commands/gateway.mdx @@ -4,34 +4,384 @@ description: "Run the Infisical gateway or manage its systemd service" --- - + ```bash - infisical gateway --token= + infisical gateway start --name= --relay= --auth-method= ``` - + ```bash - sudo infisical gateway install --token= --domain= + sudo infisical gateway systemd install --token= --domain= --name= --relay= ``` ## Description -Run the Infisical gateway in the foreground or manage its systemd service installation. The gateway allows secure communication between your self-hosted Infisical instance and client applications. +The Infisical gateway provides secure access to private resources using modern TCP-based SSH tunnel architecture with enhanced security and flexible deployment options. + +The gateway system uses SSH reverse tunnels over TCP, eliminating firewall complexity and providing excellent performance for enterprise environments. + + +**Deprecation and Migration Notice:** The legacy `infisical gateway` command (v1) will be removed in a future release. Please migrate to `infisical gateway start` (Gateway v2). + +If you are moving from Gateway v1 to Gateway v2, this is NOT a drop-in switch. Gateway v2 creates new gateway instances with new gateway IDs. You must update any existing resources that reference gateway IDs (for example: dynamic secret configs, app connections, or other gateway-bound resources) to point to the new Gateway v2 gateway ID. Until you update those references, traffic will continue to target the old v1 gateway. + + ## Subcommands & flags - - Run the Infisical gateway in the foreground. The gateway will connect to the relay service and maintain a persistent connection. + + Run the Infisical gateway component within your VPC. The gateway establishes an SSH reverse tunnel to the specified relay server and provides secure access to private resources. - ```bash - infisical gateway --domain= --auth-method= - ``` +```bash +infisical gateway start --relay= --name= --auth-method= +``` - ### Authentication +The gateway component: - The Infisical CLI supports multiple authentication methods. Below are the available authentication methods, with their respective flags. +- Establishes outbound SSH reverse tunnels to relay servers (no inbound firewall rules needed) +- Authenticates using SSH certificates issued by Infisical +- Automatically reconnects if the connection is lost +- Provides access to private resources within your network + +### Authentication + +The Infisical CLI supports multiple authentication methods. Below are the available authentication methods, with their respective flags. + + + + The Universal Auth method is a simple and secure way to authenticate with Infisical. It requires a client ID and a client secret to authenticate with Infisical. + + + + + Your machine identity client ID. + + + Your machine identity client secret. + + + The authentication method to use. Must be `universal-auth` when using Universal Auth. + + + + + ```bash + infisical gateway start --auth-method=universal-auth --client-id= --client-secret= --relay= --name= + ``` + + + + The Native Kubernetes method is used to authenticate with Infisical when running in a Kubernetes environment. It requires a service account token to authenticate with Infisical. + + + + + Your machine identity ID. + + + Path to the Kubernetes service account token to use. Default: `/var/run/secrets/kubernetes.io/serviceaccount/token`. + + + The authentication method to use. Must be `kubernetes` when using Native Kubernetes. + + + + + + + ```bash + infisical gateway start --auth-method=kubernetes --machine-identity-id= --relay= --name= + ``` + + + + The Native Azure method is used to authenticate with Infisical when running in an Azure environment. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `azure` when using Native Azure. + + + + + + + ```bash + infisical gateway start --auth-method=azure --machine-identity-id= --relay= --name= + ``` + + + + The Native GCP ID Token method is used to authenticate with Infisical when running in a GCP environment. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `gcp-id-token` when using Native GCP ID Token. + + + + + + + ```bash + infisical gateway start --auth-method=gcp-id-token --machine-identity-id= --relay= --name= + ``` + + + + The GCP IAM method is used to authenticate with Infisical with a GCP service account key. + + + + + Your machine identity ID. + + + Path to your GCP service account key file _(Must be in JSON format!)_ + + + The authentication method to use. Must be `gcp-iam` when using GCP IAM. + + + + + ```bash + infisical gateway start --auth-method=gcp-iam --machine-identity-id= --service-account-key-file-path= --relay= --name= + ``` + + + + The AWS IAM method is used to authenticate with Infisical with an AWS IAM role while running in an AWS environment like EC2, Lambda, etc. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `aws-iam` when using Native AWS IAM. + + + + + ```bash + infisical gateway start --auth-method=aws-iam --machine-identity-id= --relay= --name= + ``` + + + + The OIDC Auth method is used to authenticate with Infisical via identity tokens with OIDC. + + + + + Your machine identity ID. + + + The OIDC JWT from the identity provider. + + + The authentication method to use. Must be `oidc-auth` when using OIDC Auth. + + + + + ```bash + infisical gateway start --auth-method=oidc-auth --machine-identity-id= --jwt= --relay= --name= + ``` + + + + + The JWT Auth method is used to authenticate with Infisical via a JWT token. + + + + + The JWT token to use for authentication. + + + Your machine identity ID. + + + The authentication method to use. Must be `jwt-auth` when using JWT Auth. + + + + + + ```bash + infisical gateway start --auth-method=jwt-auth --jwt= --machine-identity-id= --relay= --name= + ``` + + + + You can use the `INFISICAL_TOKEN` environment variable to authenticate with Infisical with a raw machine identity access token. + + + + + The machine identity access token to use for authentication. + + + + + ```bash + infisical gateway start --token= --relay= --name= + ``` + + + + +### Other Flags + + + The name of the relay that this gateway should connect to. The relay must be running and registered before starting the gateway. + + ```bash + # Example + infisical gateway start --relay=my-relay --name=my-gateway --token= + ``` + + **Note:** If using organization relays or self-hosted instance relays, you must first start a relay server using `infisical relay start` before connecting gateways to it. For Infisical Cloud users using instance relays, the relay infrastructure is already running and managed by Infisical. + + + + + The name of the gateway instance. + + ```bash + # Example + infisical gateway start --name=my-gateway --relay=my-relay --token= + ``` + + + + + Domain of your self-hosted Infisical instance. + + ```bash + # Example + infisical gateway start --domain=https://app.your-domain.com --relay= --name= + ``` + + + + + + Install and enable the gateway as a systemd service. This command must be run with sudo on Linux. + +```bash +sudo infisical gateway systemd install --token= --domain= --name= --relay= +``` + +### Requirements + +- Must be run on Linux +- Must be run with root/sudo privileges +- Requires systemd + +### Flags + + + The machine identity access token to authenticate with Infisical. + + ```bash + # Example + sudo infisical gateway systemd install --token= --name= --relay= + ``` + + You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the install command. + + + + + Domain of your self-hosted Infisical instance. + + ```bash + # Example + sudo infisical gateway systemd install --domain=https://app.your-domain.com --name= --relay= + ``` + + + + + The name of the gateway instance. + + ```bash + # Example + sudo infisical gateway systemd install --name=my-gateway --token= --relay= + ``` + + + + + The name of the relay that this gateway should connect to. + + ```bash + # Example + sudo infisical gateway systemd install --relay=my-relay --token= --name= + ``` + + + +### Service Details + +The systemd service is installed with secure defaults: + +- Service file: `/etc/systemd/system/infisical-gateway.service` +- Config file: `/etc/infisical/gateway.conf` +- Runs with restricted privileges: + - InaccessibleDirectories=/home + - PrivateTmp=yes + - Resource limits configured for stability +- Automatically restarts on failure +- Enabled to start on boot +- Maintains persistent SSH reverse tunnel connections to the specified relay +- Handles certificate rotation and connection recovery automatically + +After installation, manage the service with standard systemd commands: + +```bash +sudo systemctl start infisical-gateway # Start the service +sudo systemctl stop infisical-gateway # Stop the service +sudo systemctl status infisical-gateway # Check service status +sudo systemctl disable infisical-gateway # Disable auto-start on boot +``` + + + +## Legacy Gateway Commands (Deprecated) + + + + **This command is deprecated and will be removed in a future release.** + + Please migrate to `infisical gateway start` for the new TCP-based SSH tunnel architecture. + +**Migration required:** If you are currently using Gateway v1 (via `infisical gateway`), moving to Gateway v2 is not in-place. Gateway v2 provisions new gateway instances with new gateway IDs. Update any resources that reference a gateway ID (for example: dynamic secret configs, app connections, or other gateway-bound resources) to use the new Gateway v2 gateway ID. Until you update those references, traffic will continue to target the old v1 gateway. + + + +Run the legacy Infisical gateway in the foreground. The gateway will connect to the relay service and maintain a persistent connection. + +```bash +infisical gateway --domain= --auth-method= +``` + +### Authentication + +The Infisical CLI supports multiple authentication methods. Below are the available authentication methods, with their respective flags. @@ -121,7 +471,6 @@ Run the Infisical gateway in the foreground or manage its systemd service instal infisical gateway --auth-method=gcp-id-token --machine-identity-id= ``` - The GCP IAM method is used to authenticate with Infisical with a GCP service account key. @@ -163,7 +512,6 @@ Run the Infisical gateway in the foreground or manage its systemd service instal infisical gateway --auth-method=aws-iam --machine-identity-id= ``` - The OIDC Auth method is used to authenticate with Infisical via identity tokens with OIDC. @@ -185,6 +533,7 @@ Run the Infisical gateway in the foreground or manage its systemd service instal ```bash infisical gateway --auth-method=oidc-auth --machine-identity-id= --jwt= ``` + @@ -208,6 +557,7 @@ Run the Infisical gateway in the foreground or manage its systemd service instal ```bash infisical gateway --auth-method=jwt-auth --jwt= --machine-identity-id= ``` + You can use the `INFISICAL_TOKEN` environment variable to authenticate with Infisical with a raw machine identity access token. @@ -227,7 +577,7 @@ Run the Infisical gateway in the foreground or manage its systemd service instal - ### Other Flags +### Other Flags Domain of your self-hosted Infisical instance. @@ -236,22 +586,33 @@ Run the Infisical gateway in the foreground or manage its systemd service instal # Example infisical gateway --domain=https://app.your-domain.com ``` + - - Install and enable the gateway as a systemd service. This command must be run with sudo on Linux. + + + **This command is deprecated and will be removed in a future release.** + + Please migrate to `infisical gateway systemd install` for the new TCP-based SSH tunnel architecture with enhanced security and better performance. - ```bash - sudo infisical gateway install --token= --domain= - ``` +**Migration required:** If you previously installed Gateway v1 via `infisical gateway install`, moving to Gateway v2 is not in-place. Gateway v2 provisions new gateway instances with new gateway IDs. Update any resources that reference a gateway ID (for example: dynamic secret configs, app connections, or other gateway-bound resources) to use the new Gateway v2 gateway ID. Until you update those references, traffic will continue to target the old v1 gateway. - ### Requirements - - Must be run on Linux - - Must be run with root/sudo privileges - - Requires systemd + - ### Flags +Install and enable the legacy gateway as a systemd service. This command must be run with sudo on Linux. + +```bash +sudo infisical gateway install --token= --domain= +``` + +### Requirements + +- Must be run on Linux +- Must be run with root/sudo privileges +- Requires systemd + +### Flags The machine identity access token to authenticate with Infisical. @@ -262,6 +623,7 @@ Run the Infisical gateway in the foreground or manage its systemd service instal ``` You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the install command. + @@ -271,24 +633,29 @@ Run the Infisical gateway in the foreground or manage its systemd service instal # Example sudo infisical gateway install --domain=https://app.your-domain.com ``` + - ### Service Details - The systemd service is installed with secure defaults: - - Service file: `/etc/systemd/system/infisical-gateway.service` - - Config file: `/etc/infisical/gateway.conf` - - Runs with restricted privileges: - - InaccessibleDirectories=/home - - PrivateTmp=yes - - Resource limits configured for stability - - Automatically restarts on failure - - Enabled to start on boot +### Service Details + +The systemd service is installed with secure defaults: + +- Service file: `/etc/systemd/system/infisical-gateway.service` +- Config file: `/etc/infisical/gateway.conf` +- Runs with restricted privileges: + - InaccessibleDirectories=/home + - PrivateTmp=yes + - Resource limits configured for stability +- Automatically restarts on failure +- Enabled to start on boot + +After installation, manage the service with standard systemd commands: + +```bash +sudo systemctl start infisical-gateway # Start the service +sudo systemctl stop infisical-gateway # Stop the service +sudo systemctl status infisical-gateway # Check service status +sudo systemctl disable infisical-gateway # Disable auto-start on boot +``` - After installation, manage the service with standard systemd commands: - ```bash - sudo systemctl start infisical-gateway # Start the service - sudo systemctl stop infisical-gateway # Stop the service - sudo systemctl status infisical-gateway # Check service status - sudo systemctl disable infisical-gateway # Disable auto-start on boot - ``` diff --git a/docs/cli/commands/relay.mdx b/docs/cli/commands/relay.mdx new file mode 100644 index 000000000..46a061da3 --- /dev/null +++ b/docs/cli/commands/relay.mdx @@ -0,0 +1,306 @@ +--- +title: "infisical relay" +description: "Relay-related commands for Infisical" +--- + + + + ```bash + infisical relay start --type= --host= --name= --auth-method= + ``` + + + +## Description + +Relay-related commands for Infisical that provide identity-aware relay infrastructure for routing encrypted traffic: + +- **Relay**: Identity-aware server that routes encrypted traffic (can be instance-wide or organization-specific) + +The relay system uses SSH reverse tunnels over TCP, eliminating firewall complexity and providing excellent performance for enterprise environments. + +## Subcommands & flags + + + Run the Infisical relay component. The relay handles network traffic routing and can operate in different modes. + +```bash +infisical relay start --type= --host= --name= --auth-method= +``` + +### Flags + + + The type of relay to run. Must be either 'instance' or 'org'. + + - **`instance`**: Shared relay server that can be used by all organizations on your Infisical instance. Set up by the instance administrator. Uses `INFISICAL_RELAY_AUTH_SECRET` environment variable for authentication, which must be configured by the instance admin. + - **`org`**: Dedicated relay server that individual organizations deploy and manage in their own infrastructure. Provides enhanced security, custom geographic placement, and compliance benefits. Uses standard Infisical authentication methods. + + ```bash + # Organization relay (customer-deployed) + infisical relay start --type=org --host=192.168.1.100 --name=my-org-relay + + # Instance relay (configured by instance admin) + INFISICAL_RELAY_AUTH_SECRET= infisical relay start --type=instance --host=10.0.1.50 --name=shared-relay + ``` + + + + + The host (IP address or hostname) of the instance where the relay is deployed. This must be a static public IP or resolvable hostname that gateways can reach. + + ```bash + # Example with IP address + infisical relay start --host=203.0.113.100 --type=org --name=my-relay + + # Example with hostname + infisical relay start --host=relay.example.com --type=org --name=my-relay + ``` + + + + + The name of the relay. + + ```bash + # Example + infisical relay start --name=my-relay --type=org --host=192.168.1.100 + ``` + + + +### Authentication + +**Organization Relays (`--type=org`):** +Deploy your own relay server in your infrastructure for enhanced security and reduced latency. Supports all standard Infisical authentication methods documented below. + +**Instance Relays (`--type=instance`):** +Shared relay servers that serve all organizations on your Infisical instance. For Infisical Cloud, these are already running and ready to use. For self-hosted deployments, they're set up by the instance administrator. Authentication is handled via the `INFISICAL_RELAY_AUTH_SECRET` environment variable. + +```bash +# Organization relay with Universal Auth (customer-deployed) +infisical relay start --type=org --host=192.168.1.100 --name=my-org-relay --auth-method=universal-auth --client-id= --client-secret= + +# Instance relay (configured by instance admin) +INFISICAL_RELAY_AUTH_SECRET= infisical relay start --type=instance --host=10.0.1.50 --name=shared-relay +``` + +### Authentication Methods + +The Infisical CLI supports multiple authentication methods for organization relays. Below are the available authentication methods, with their respective flags. + + + + The Universal Auth method is a simple and secure way to authenticate with Infisical. It requires a client ID and a client secret to authenticate with Infisical. + + + + + Your machine identity client ID. + + + Your machine identity client secret. + + + The authentication method to use. Must be `universal-auth` when using Universal Auth. + + + + + ```bash + infisical relay start --auth-method=universal-auth --client-id= --client-secret= --type=org --host= --name= + ``` + + + + The Native Kubernetes method is used to authenticate with Infisical when running in a Kubernetes environment. It requires a service account token to authenticate with Infisical. + + + + + Your machine identity ID. + + + Path to the Kubernetes service account token to use. Default: `/var/run/secrets/kubernetes.io/serviceaccount/token`. + + + The authentication method to use. Must be `kubernetes` when using Native Kubernetes. + + + + + + + ```bash + infisical relay start --auth-method=kubernetes --machine-identity-id= --type=org --host= --name= + ``` + + + + The Native Azure method is used to authenticate with Infisical when running in an Azure environment. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `azure` when using Native Azure. + + + + + + + ```bash + infisical relay start --auth-method=azure --machine-identity-id= --type=org --host= --name= + ``` + + + + The Native GCP ID Token method is used to authenticate with Infisical when running in a GCP environment. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `gcp-id-token` when using Native GCP ID Token. + + + + + + + ```bash + infisical relay start --auth-method=gcp-id-token --machine-identity-id= --type=org --host= --name= + ``` + + + + The GCP IAM method is used to authenticate with Infisical with a GCP service account key. + + + + + Your machine identity ID. + + + Path to your GCP service account key file _(Must be in JSON format!)_ + + + The authentication method to use. Must be `gcp-iam` when using GCP IAM. + + + + + ```bash + infisical relay start --auth-method=gcp-iam --machine-identity-id= --service-account-key-file-path= --type=org --host= --name= + ``` + + + + The AWS IAM method is used to authenticate with Infisical with an AWS IAM role while running in an AWS environment like EC2, Lambda, etc. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `aws-iam` when using Native AWS IAM. + + + + + ```bash + infisical relay start --auth-method=aws-iam --machine-identity-id= --type=org --host= --name= + ``` + + + + The OIDC Auth method is used to authenticate with Infisical via identity tokens with OIDC. + + + + + Your machine identity ID. + + + The OIDC JWT from the identity provider. + + + The authentication method to use. Must be `oidc-auth` when using OIDC Auth. + + + + + ```bash + infisical relay start --auth-method=oidc-auth --machine-identity-id= --jwt= --type=org --host= --name= + ``` + + + + + The JWT Auth method is used to authenticate with Infisical via a JWT token. + + + + + The JWT token to use for authentication. + + + Your machine identity ID. + + + The authentication method to use. Must be `jwt-auth` when using JWT Auth. + + + + + + ```bash + infisical relay start --auth-method=jwt-auth --jwt= --machine-identity-id= --type=org --host= --name= + ``` + + + + You can use the `INFISICAL_TOKEN` environment variable to authenticate with Infisical with a raw machine identity access token. + + + + + The machine identity access token to use for authentication. + + + + + ```bash + infisical relay start --token= --type=org --host= --name= + ``` + + + + +### Deployment Considerations + +**When to use Instance Relays (`--type=instance`):** + +- You want to get started quickly without setting up your own relay infrastructure +- You're using Infisical Cloud and want to leverage the existing relay infrastructure +- You're on a self-hosted instance where the admin has already set up shared relays +- You don't need custom geographic placement of relay servers +- You don't have specific compliance requirements that require dedicated infrastructure +- You want to minimize operational overhead by using shared infrastructure + +**When to use Organization Relays (`--type=org`):** + +- You need lower latency by deploying relay servers closer to your resources +- You have security requirements that mandate running infrastructure in your own environment +- You have compliance requirements such as data sovereignty or air-gapped environments +- You need custom network policies or specific networking configurations +- You have high-scale performance requirements that shared infrastructure can't meet +- You want full control over your relay infrastructure and its configuration + + diff --git a/docs/docs.json b/docs/docs.json index 08121fa08..8b64210a0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -174,7 +174,15 @@ "pages": [ "documentation/platform/gateways/overview", "documentation/platform/gateways/gateway-security", - "documentation/platform/gateways/networking" + "documentation/platform/gateways/networking", + { + "group": "Gateway (Deprecated)", + "pages": [ + "documentation/platform/gateways-deprecated/overview", + "documentation/platform/gateways-deprecated/gateway-security", + "documentation/platform/gateways-deprecated/networking" + ] + } ] } ] @@ -346,7 +354,10 @@ }, { "group": "Architecture", - "pages": ["internals/architecture/components", "internals/architecture/cloud"] + "pages": [ + "internals/architecture/components", + "internals/architecture/cloud" + ] }, "internals/security", "internals/service-tokens" @@ -564,7 +575,10 @@ "integrations/cloud/gcp-secret-manager", { "group": "Cloudflare", - "pages": ["integrations/cloud/cloudflare-pages", "integrations/cloud/cloudflare-workers"] + "pages": [ + "integrations/cloud/cloudflare-pages", + "integrations/cloud/cloudflare-workers" + ] }, "integrations/cloud/terraform-cloud", "integrations/cloud/databricks", @@ -659,7 +673,9 @@ "documentation/platform/secret-scanning/overview", { "group": "Concepts", - "pages": ["documentation/platform/secret-scanning/concepts/secret-scanning"] + "pages": [ + "documentation/platform/secret-scanning/concepts/secret-scanning" + ] } ] }, @@ -709,13 +725,18 @@ "documentation/platform/ssh/overview", { "group": "Concepts", - "pages": ["documentation/platform/ssh/concepts/ssh-certificates"] + "pages": [ + "documentation/platform/ssh/concepts/ssh-certificates" + ] } ] }, { "group": "Platform Reference", - "pages": ["documentation/platform/ssh/usage", "documentation/platform/ssh/host-groups"] + "pages": [ + "documentation/platform/ssh/usage", + "documentation/platform/ssh/host-groups" + ] } ] }, @@ -753,6 +774,7 @@ "cli/commands/dynamic-secrets", "cli/commands/ssh", "cli/commands/gateway", + "cli/commands/relay", "cli/commands/bootstrap", "cli/commands/export", "cli/commands/token", @@ -762,7 +784,11 @@ "cli/commands/reset", { "group": "infisical scan", - "pages": ["cli/commands/scan", "cli/commands/scan-git-changes", "cli/commands/scan-install"] + "pages": [ + "cli/commands/scan", + "cli/commands/scan-git-changes", + "cli/commands/scan-install" + ] } ] }, @@ -983,28 +1009,55 @@ "api-reference/endpoints/organizations/delete-membership", "api-reference/endpoints/organizations/bulk-delete-memberships", "api-reference/endpoints/organizations/list-identity-memberships", - "api-reference/endpoints/organizations/workspaces" + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/organizations/projects" + ] + } ] }, { "group": "Projects", "pages": [ - "api-reference/endpoints/workspaces/get-workspace-by-slug", - "api-reference/endpoints/workspaces/create-workspace", - "api-reference/endpoints/workspaces/delete-workspace", - "api-reference/endpoints/workspaces/get-workspace", - "api-reference/endpoints/workspaces/update-workspace", - "api-reference/endpoints/workspaces/secret-snapshots" + "api-reference/endpoints/projects/get-project-by-slug", + "api-reference/endpoints/projects/create-project", + "api-reference/endpoints/projects/delete-project", + "api-reference/endpoints/projects/get-project", + "api-reference/endpoints/projects/list-projects", + "api-reference/endpoints/projects/update-project", + "api-reference/endpoints/projects/secret-snapshots", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/projects/get-project-by-slug", + "api-reference/endpoints/deprecated/projects/create-project", + "api-reference/endpoints/deprecated/projects/delete-project", + "api-reference/endpoints/deprecated/projects/get-project", + "api-reference/endpoints/deprecated/projects/update-project", + "api-reference/endpoints/deprecated/projects/secret-snapshots" + ] + } ] }, { "group": "Project Users", "pages": [ - "api-reference/endpoints/project-users/invite-member-to-workspace", - "api-reference/endpoints/project-users/remove-member-from-workspace", + "api-reference/endpoints/project-users/invite-member-to-project", + "api-reference/endpoints/project-users/remove-member-from-project", "api-reference/endpoints/project-users/memberships", "api-reference/endpoints/project-users/get-by-username", - "api-reference/endpoints/project-users/update-membership" + "api-reference/endpoints/project-users/update-membership", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/project-users/invite-member-to-project", + "api-reference/endpoints/deprecated/project-users/remove-member-from-project", + "api-reference/endpoints/deprecated/project-users/memberships", + "api-reference/endpoints/deprecated/project-users/get-by-username", + "api-reference/endpoints/deprecated/project-users/update-membership" + ] + } ] }, { @@ -1014,7 +1067,17 @@ "api-reference/endpoints/project-groups/delete", "api-reference/endpoints/project-groups/get-by-id", "api-reference/endpoints/project-groups/list", - "api-reference/endpoints/project-groups/update" + "api-reference/endpoints/project-groups/update", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/project-groups/create", + "api-reference/endpoints/deprecated/project-groups/delete", + "api-reference/endpoints/deprecated/project-groups/get-by-id", + "api-reference/endpoints/deprecated/project-groups/list", + "api-reference/endpoints/deprecated/project-groups/update" + ] + } ] }, { @@ -1024,7 +1087,17 @@ "api-reference/endpoints/project-identities/list-identity-memberships", "api-reference/endpoints/project-identities/get-by-id", "api-reference/endpoints/project-identities/update-identity-membership", - "api-reference/endpoints/project-identities/delete-identity-membership" + "api-reference/endpoints/project-identities/delete-identity-membership", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/project-identities/add-identity-membership", + "api-reference/endpoints/deprecated/project-identities/list-identity-memberships", + "api-reference/endpoints/deprecated/project-identities/get-by-id", + "api-reference/endpoints/deprecated/project-identities/update-identity-membership", + "api-reference/endpoints/deprecated/project-identities/delete-identity-membership" + ] + } ] }, { @@ -1034,7 +1107,17 @@ "api-reference/endpoints/project-roles/update", "api-reference/endpoints/project-roles/delete", "api-reference/endpoints/project-roles/get-by-slug", - "api-reference/endpoints/project-roles/list" + "api-reference/endpoints/project-roles/list", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/project-roles/create", + "api-reference/endpoints/deprecated/project-roles/update", + "api-reference/endpoints/deprecated/project-roles/delete", + "api-reference/endpoints/deprecated/project-roles/get-by-slug", + "api-reference/endpoints/deprecated/project-roles/list" + ] + } ] }, { @@ -1052,7 +1135,15 @@ "pages": [ "api-reference/endpoints/environments/create", "api-reference/endpoints/environments/update", - "api-reference/endpoints/environments/delete" + "api-reference/endpoints/environments/delete", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/environments/create", + "api-reference/endpoints/deprecated/environments/update", + "api-reference/endpoints/deprecated/environments/delete" + ] + } ] }, { @@ -1062,7 +1153,17 @@ "api-reference/endpoints/folders/get-by-id", "api-reference/endpoints/folders/create", "api-reference/endpoints/folders/update", - "api-reference/endpoints/folders/delete" + "api-reference/endpoints/folders/delete", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/folders/list", + "api-reference/endpoints/deprecated/folders/get-by-id", + "api-reference/endpoints/deprecated/folders/create", + "api-reference/endpoints/deprecated/folders/update", + "api-reference/endpoints/deprecated/folders/delete" + ] + } ] }, { @@ -1073,7 +1174,18 @@ "api-reference/endpoints/secret-tags/get-by-slug", "api-reference/endpoints/secret-tags/create", "api-reference/endpoints/secret-tags/update", - "api-reference/endpoints/secret-tags/delete" + "api-reference/endpoints/secret-tags/delete", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/secret-tags/list", + "api-reference/endpoints/deprecated/secret-tags/get-by-id", + "api-reference/endpoints/deprecated/secret-tags/get-by-slug", + "api-reference/endpoints/deprecated/secret-tags/create", + "api-reference/endpoints/deprecated/secret-tags/update", + "api-reference/endpoints/deprecated/secret-tags/delete" + ] + } ] }, { @@ -1087,8 +1199,21 @@ "api-reference/endpoints/secrets/create-many", "api-reference/endpoints/secrets/update-many", "api-reference/endpoints/secrets/delete-many", - "api-reference/endpoints/secrets/attach-tags", - "api-reference/endpoints/secrets/detach-tags" + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/secrets/list", + "api-reference/endpoints/deprecated/secrets/create", + "api-reference/endpoints/deprecated/secrets/read", + "api-reference/endpoints/deprecated/secrets/update", + "api-reference/endpoints/deprecated/secrets/delete", + "api-reference/endpoints/deprecated/secrets/create-many", + "api-reference/endpoints/deprecated/secrets/update-many", + "api-reference/endpoints/deprecated/secrets/delete-many", + "api-reference/endpoints/deprecated/secrets/attach-tags", + "api-reference/endpoints/deprecated/secrets/detach-tags" + ] + } ] }, { @@ -1096,7 +1221,9 @@ "pages": [ { "group": "Kubernetes", - "pages": ["api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"] + "pages": [ + "api-reference/endpoints/dynamic-secrets/kubernetes/create-lease" + ] }, "api-reference/endpoints/dynamic-secrets/create", "api-reference/endpoints/dynamic-secrets/update", @@ -1116,7 +1243,16 @@ "api-reference/endpoints/secret-imports/list", "api-reference/endpoints/secret-imports/create", "api-reference/endpoints/secret-imports/update", - "api-reference/endpoints/secret-imports/delete" + "api-reference/endpoints/secret-imports/delete", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/secret-imports/list", + "api-reference/endpoints/deprecated/secret-imports/create", + "api-reference/endpoints/deprecated/secret-imports/update", + "api-reference/endpoints/deprecated/secret-imports/delete" + ] + } ] }, { @@ -2455,6 +2591,7 @@ "sdks/languages/cpp", "sdks/languages/rust", "sdks/languages/go", + "sdks/languages/php", "sdks/languages/ruby" ] } diff --git a/docs/documentation/platform/audit-log-streams/audit-log-streams.mdx b/docs/documentation/platform/audit-log-streams/audit-log-streams.mdx index 2ee4dff92..ad030ec26 100644 --- a/docs/documentation/platform/audit-log-streams/audit-log-streams.mdx +++ b/docs/documentation/platform/audit-log-streams/audit-log-streams.mdx @@ -6,84 +6,285 @@ description: "Learn how to stream Infisical Audit Logs to external logging provi Audit log streams is a paid feature. - If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, then you should contact team@infisical.com to purchase an enterprise license to use it. -Infisical Audit Log Streaming enables you to transmit your organization's Audit Logs to external logging providers for monitoring and analysis. - -The logs are formatted in JSON, requiring your logging provider to support JSON-based log parsing. - +Infisical Audit Log Streaming enables you to transmit your organization's audit logs to external logging providers for monitoring and analysis. ## Overview - - - ![stream create](/images/platform/audit-log-streams/stream-create.png) - - - ![stream create](/images/platform/audit-log-streams/stream-inputs.png) + + 1. Navigate to **Organization Settings** + 2. Select the **Audit Log Streams** tab + 3. Click **Add Log Stream** - Provide the following values - - The HTTPS endpoint URL of the logging provider that collects the JSON stream. - - - The HTTP headers for the logging provider for identification and authentication. - + ![stream create](/images/platform/audit-log-streams/stream-create.png) + + + If your log provider is included in this list, select it. Otherwise click on **Custom** to input your own Endpoint URL and headers. + + ![select provider](/images/platform/audit-log-streams/select-provider.png) + + + Depending on your chosen provider, you'll be asked to input different credentials. + + For **Custom**, you need to input an endpoint URL and headers. + + ![custom provider](/images/platform/audit-log-streams/custom-provider.png) + + Once you're finished, click **Create Log Stream**. + + + Your audit logs are now ready to be streamed. + + ![stream list](/images/platform/audit-log-streams/stream-list.png) -![stream listt](/images/platform/audit-log-streams/stream-list.png) -Your Audit Logs are now ready to be streamed. - ## Example Providers -### Better Stack + + + Infisical offers a dedicated **Azure** provider to stream your audit logs, enabling seamless integration with services like Microsoft Sentinel. - - - ![better stack connect source](/images/platform/audit-log-streams/betterstack-create-source.png) - - - - ![better stack connect](/images/platform/audit-log-streams/betterstack-source-details.png) + + After setting up all Azure resources, it may take 10-20 minutes for logs to begin streaming. + - 1. Copy the **endpoint** from Better Stack to the **Endpoint URL** field. - 3. Create a new header with key **Authorization** and set the value as **Bearer \**. - - + + + Navigate to [Data Collection Endpoints](https://portal.azure.com/#view/HubsExtension/BrowseResource.ReactView/resourceType/microsoft.insights%2Fdatacollectionendpoints) and click **Create**. -### Datadog + ![azure create dce](/images/platform/audit-log-streams/azure-create-dce.png) - - - ![api key create](/images/platform/audit-log-streams/datadog-api-sidebar.png) - - - ![api key form](/images/platform/audit-log-streams/data-create-api-key.png) - ![api key form](/images/platform/audit-log-streams/data-dog-api-key.png) - - - ![datadog url](/images/platform/audit-log-streams/datadog-logging-endpoint.png) + Configure your Data Collection Endpoint by providing an **Endpoint Name**, **Subscription**, and a **Resource group**. Then click **Review + Create**. - 1. Navigate to the [Datadog Send Logs API documentation](https://docs.datadoghq.com/api/latest/logs/?code-lang=curl&site=us5#send-logs). - 2. Pick your Datadog account region. - 3. Obtain your Datadog logging endpoint URL. - - - ![datadog api key details](/images/platform/audit-log-streams/datadog-source-details.png) + ![azure configure dce](/images/platform/audit-log-streams/azure-configure-dce.png) - 1. Copy the **logging endpoint** from Datadog to the **Endpoint URL** field. - 2. Copy the **API Key** from previous step - 3. Create a new header with key **DD-API-KEY** and set the value as **API Key**. - - + After creation, it may take a few minutes for the Data Collection Endpoint to appear. Once visible, click on it and copy the **Logs Ingestion** URL. You will need this URL in later steps. -## Audit Log Stream Data + ![azure dce url](/images/platform/audit-log-streams/azure-dce-url.png) + + + + If you already have a Log Analytics Workspace, you may skip this step. + -Each log entry sent to the external logging provider will follow the same structure. + Navigate to [Log Analytics Workspaces](https://portal.azure.com/#browse/Microsoft.OperationalInsights%2Fworkspaces) and click **Create**. + + ![azure create law](/images/platform/audit-log-streams/azure-create-law.png) + + Configure your Log Analytics Workspace by providing a **Subscription**, **Resource group**, and a **Name**. Then click **Review + Create**. + + ![azure configure law](/images/platform/audit-log-streams/azure-configure-law.png) + + Once the workspace is deployed, click **Go to resource** to access it. + + ![azure go to resource](/images/platform/audit-log-streams/azure-go-to-resource.png) + + + Within your Log Analytics Workspace, navigate to **Tables** and click **Create**. Select **New custom log (DCR-based)** from the dropdown. + + ![azure new table](/images/platform/audit-log-streams/azure-new-table.png) + + Configure the Custom Log Table: Provide a **Table name** (e.g., `InfisicalLogs`), select the **Data collection endpoint** created in Step 1, and create a new **Data collection rule** as illustrated in the image below. Then, click **Next**. + + ![azure configure table](/images/platform/audit-log-streams/azure-configure-table.png) + + On the **Schema and transformation** page, you'll be prompted to upload a **Log Sample**. Create a `.json` file with the following content and upload it: + + ```json + { + "id": "00000000-0000-0000-0000-000000000000", + "actor": "user", + "actorMetadata": { + "email": "user@example.com", + "userId": "00000000-0000-0000-0000-000000000000", + "username": "user@example.com" + }, + "ipAddress": "0.0.0.0", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", + "userAgentType": "web", + "eventType": "get-secrets", + "eventMetadata": {}, + "projectName": "MyProject", + "orgId": "00000000-0000-0000-0000-000000000000", + "projectId": "00000000-0000-0000-0000-000000000000", + "TimeGenerated": "2025-01-01T00:00:00.000Z" + } + ``` + + Optionally, you can add **Transformations** to further destructure the data. For example, to extract actor email and userId: + + ``` + source + | extend + ActorEmail = tostring(actorMetadata.email), + ActorUserId = tostring(actorMetadata.userId) + ``` + + On the final step, click **Create**. + + + It may take a few minutes for your Custom Log Table to be created and appear under Tables. + + + + After creating your Data Collection Rule, you'll need its **Immutable ID**. + + Navigate to [Data collection rules](https://portal.azure.com/#view/HubsExtension/BrowseResource.ReactView/resourceType/microsoft.insights%2Fdatacollectionrules). Click on your newly created DCR and copy its **Immutable ID** for the next step. + + ![azure dcr](/images/platform/audit-log-streams/azure-dcr.png) + + + In Infisical, create a new audit log stream and select the **Azure** provider. Input the following details: + + - **Tenant ID**: Your Tenant ID + - **Client ID**: The Client ID of an App Registration + - **Client Secret**: The Client Secret of an App Registration + - **Data Collection Endpoint URL**: Obtained from Step 1 + - **Data Collection Rule Immutable ID**: Obtained from Step 4 + - **Custom Log Table Name**: Defined in Step 3 + + ![azure create als](/images/platform/audit-log-streams/azure-create-als.png) + + + The App Registration used for authentication must have the **Monitoring Metrics Publisher** role assigned on the **Data Collection Rule** created in Step 3. [See Microsoft Guide](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/tutorial-logs-ingestion-portal#assign-permissions-to-the-dcr). + + + + + + You can stream to Better Stack using a **Custom** log stream. + + + + On Better Stack, select **Connect Source** and click **Create source** after providing a name. + + ![better stack connect source](/images/platform/audit-log-streams/betterstack-create-source.png) + + Once your source is created, take note of the **endpoint** and **Source token** for the next step. + + ![better stack connect](/images/platform/audit-log-streams/betterstack-source-details.png) + + + On Infisical, create a new audit log stream and select the **Custom** option. + + ![select custom](/images/platform/audit-log-streams/select-custom.png) + + 1. Fill in the endpoint URL with your Better Stack source endpoint + 2. Create a new header with key `Authorization` and set the value as `Bearer ` + + ![custom provider](/images/platform/audit-log-streams/custom-provider.png) + + Once you're finished, click **Create Log Stream**. + + + + + Stream Infisical audit logs to Cribl Stream for centralized processing and routing. Infisical supports Cribl as a provider for seamless integration. + + + + In Cribl Stream, navigate to **Worker Groups** and select your Worker Group. Take note of the **Ingress Address** for later steps. + + ![cribl ingress address](/images/platform/audit-log-streams/cribl-ingress-address.png) + + Within your Worker Group, navigate to **Data > Sources > HTTP** and click **Add Source**. + + ![cribl add source](/images/platform/audit-log-streams/cribl-add-source.png) + + Configure the **Input ID**, **Port**, and **Cribl HTTP event API** path (e.g., `/infisical`). Then, generate an **Auth Token**. + + You can optionally configure TLS in the **TLS Settings** tab and add a pipeline in the **Pre-Processing** tab. + + + Ensure that you're using a port that's open on your instance. + + + ![cribl general settings](/images/platform/audit-log-streams/cribl-general-settings.png) + + Once you've configured the Data Source, click **Save** and deploy your changes. + + + On Infisical, create a new audit log stream and select the **Cribl** provider option. + + Input the following credentials: + - **Cribl Stream URL**: Your HTTP source endpoint composed of `http://://_bulk` + - **Cribl Stream Token**: The authentication token from Step 1 + + + If you configured TLS for your Data Source, use the `https://` protocol. + + + ![cribl details](/images/platform/audit-log-streams/cribl-details.png) + + Once you're finished, click **Create Log Stream**. + + + + + You can stream to Datadog using the **Datadog** provider log stream. + + + + ![api key create](/images/platform/audit-log-streams/datadog-api-sidebar.png) + + + ![api key form](/images/platform/audit-log-streams/data-create-api-key.png) + ![api key form](/images/platform/audit-log-streams/data-dog-api-key.png) + + + On Infisical, create a new audit log stream and select the **Datadog** provider option. + + Input your **Datadog Region** and the **Token** obtained from step 2. + + ![datadog details](/images/platform/audit-log-streams/datadog-details.png) + + Once you're finished, click **Create Log Stream**. + + + + + You can stream to Splunk using the **Splunk** provider log stream. + + + + Navigate to **Settings** > **Data Inputs**. + + ![splunk data inputs](/images/platform/audit-log-streams/splunk-data-inputs.png) + + Click on **HTTP Event Collector**. + + ![splunk http collector](/images/platform/audit-log-streams/splunk-http-collector.png) + + Click on **New Token** in the top left. + + ![splunk new token](/images/platform/audit-log-streams/splunk-new-token.png) + + Provide a name and click **Next**. + + ![splunk name](/images/platform/audit-log-streams/splunk-name.png) + + On the next page, click **Review** and then **Submit** at the top. On the final page you'll see your token. + + Copy the **Token Value** and your Splunk hostname from the URL to be used for later. + + ![splunk credentials](/images/platform/audit-log-streams/splunk-credentials.png) + + + On Infisical, create a new audit log stream and select the **Splunk** provider option. + + Input your **Splunk Hostname** and the **Token** obtained from step 1. + + ![splunk details](/images/platform/audit-log-streams/splunk-details.png) + + Once you're finished, click **Create Log Stream**. + + + + ### Example Log Entry @@ -117,106 +318,109 @@ Each log entry sent to the external logging provider will follow the same struct ``` ### Audit Logs Structure + + + Streamed audit log structure **varies based on provider**, but they all share the audit log fields shown below. + + - The unique identifier for the log entry. + The unique identifier for the log entry. - The entity responsible for performing or causing the event; this can be a user or service. + The entity responsible for performing or causing the event; this can be a user or service. - The metadata associated with the actor. This varies based on the actor type. + The metadata associated with the actor. This varies based on the actor type. - - This metadata is present when the `actor` field is set to `user`. + + + This metadata is present when the `actor` field is set to `user`. - - The unique identifier for the actor. - - - The email address of the actor. - - - The username of the actor. - - + + The unique identifier for the actor. + + + The email address of the actor. + + + The username of the actor. + + + + This metadata is present when the `actor` field is set to `identity`. - - This metadata is present when the `actor` field is set to `identity`. + + The unique identifier for the identity. + + + The name of the identity. + + + + This metadata is present when the `actor` field is set to `service`. - - The unique identifier for the identity. - - - The name of the identity. - - - - - This metadata is present when the `actor` field is set to `service`. - - - The unique identifier for the service. - - - The name of the service. - - - - - - If the `actor` field is set to `platform`, `scimClient`, or `unknownUser`, the `actorMetadata` field will be an empty object. - + + The unique identifier for the service. + + + The name of the service. + + + + + If the `actor` field is set to `platform`, `scimClient`, or `unknownUser`, the `actorMetadata` field will be an empty object. + - The IP address of the actor. + The IP address of the actor. - The type of event that occurred. Below you can see a list of possible event types. More event types will be added in the future as we expand our audit logs further. + The type of event that occurred. Below you can see a list of possible event types. More event types will be added in the future as we expand our audit logs further. - `get-secrets`, `delete-secrets`, `get-secret`, `create-secret`, `update-secret`, `delete-secret`, `get-workspace-key`, `authorize-integration`, `update-integration-auth`, `unauthorize-integration`, `create-integration`, `delete-integration`, `add-trusted-ip`, `update-trusted-ip`, `delete-trusted-ip`, `create-service-token`, `delete-service-token`, `create-identity`, `update-identity`, `delete-identity`, `login-identity-universal-auth`, `add-identity-universal-auth`, `update-identity-universal-auth`, `get-identity-universal-auth`, `create-identity-universal-auth-client-secret`, `revoke-identity-universal-auth-client-secret`, `get-identity-universal-auth-client-secret`, `create-environment`, `update-environment`, `delete-environment`, `add-workspace-member`, `remove-workspace-member`, `create-folder`, `update-folder`, `delete-folder`, `create-webhook`, `update-webhook-status`, `delete-webhook`, `webhook-triggered`, `get-secret-imports`, `create-secret-import`, `update-secret-import`, `delete-secret-import`, `update-user-workspace-role`, `update-user-workspace-denied-permissions`, `create-certificate-authority`, `get-certificate-authority`, `update-certificate-authority`, `delete-certificate-authority`, `get-certificate-authority-csr`, `get-certificate-authority-cert`, `sign-intermediate`, `import-certificate-authority-cert`, `get-certificate-authority-crl`, `issue-cert`, `get-cert`, `delete-cert`, `revoke-cert`, `get-cert-body`, `create-pki-alert`, `get-pki-alert`, `update-pki-alert`, `delete-pki-alert`, `create-pki-collection`, `get-pki-collection`, `update-pki-collection`, `delete-pki-collection`, `get-pki-collection-items`, `add-pki-collection-item`, `delete-pki-collection-item`, `org-admin-accessed-project`, `create-certificate-template`, `update-certificate-template`, `delete-certificate-template`, `get-certificate-template`, `create-certificate-template-est-config`, `update-certificate-template-est-config`, `get-certificate-template-est-config`, `update-project-slack-config`, `get-project-slack-config`, `integration-synced`, `create-shared-secret`, `delete-shared-secret`, `read-shared-secret`. + `get-secrets`, `delete-secrets`, `get-secret`, `create-secret`, `update-secret`, `delete-secret`, `get-workspace-key`, `authorize-integration`, `update-integration-auth`, `unauthorize-integration`, `create-integration`, `delete-integration`, `add-trusted-ip`, `update-trusted-ip`, `delete-trusted-ip`, `create-service-token`, `delete-service-token`, `create-identity`, `update-identity`, `delete-identity`, `login-identity-universal-auth`, `add-identity-universal-auth`, `update-identity-universal-auth`, `get-identity-universal-auth`, `create-identity-universal-auth-client-secret`, `revoke-identity-universal-auth-client-secret`, `get-identity-universal-auth-client-secret`, `create-environment`, `update-environment`, `delete-environment`, `add-workspace-member`, `remove-workspace-member`, `create-folder`, `update-folder`, `delete-folder`, `create-webhook`, `update-webhook-status`, `delete-webhook`, `webhook-triggered`, `get-secret-imports`, `create-secret-import`, `update-secret-import`, `delete-secret-import`, `update-user-workspace-role`, `update-user-workspace-denied-permissions`, `create-certificate-authority`, `get-certificate-authority`, `update-certificate-authority`, `delete-certificate-authority`, `get-certificate-authority-csr`, `get-certificate-authority-cert`, `sign-intermediate`, `import-certificate-authority-cert`, `get-certificate-authority-crl`, `issue-cert`, `get-cert`, `delete-cert`, `revoke-cert`, `get-cert-body`, `create-pki-alert`, `get-pki-alert`, `update-pki-alert`, `delete-pki-alert`, `create-pki-collection`, `get-pki-collection`, `update-pki-collection`, `delete-pki-collection`, `get-pki-collection-items`, `add-pki-collection-item`, `delete-pki-collection-item`, `org-admin-accessed-project`, `create-certificate-template`, `update-certificate-template`, `delete-certificate-template`, `get-certificate-template`, `create-certificate-template-est-config`, `update-certificate-template-est-config`, `get-certificate-template-est-config`, `update-project-slack-config`, `get-project-slack-config`, `integration-synced`, `create-shared-secret`, `delete-shared-secret`, `read-shared-secret`. - The metadata associated with the event. This varies based on the event type. + The metadata associated with the event. This varies based on the event type. - The user agent of the actor, if applicable. + The user agent of the actor, if applicable. - The type of user agent. + The type of user agent. - The expiration date of the log entry. When this date is reached, the log entry will be deleted from Infisical. + The expiration date of the log entry. When this date is reached, the log entry will be deleted from Infisical. - The creation date of the log entry. + The creation date of the log entry. - The last update date of the log entry. This is unlikely to be out of sync with the `createdAt` field, as we do not update log entries after they've been created. + The last update date of the log entry. This is unlikely to be out of sync with the `createdAt` field, as we do not update log entries after they've been created. - The unique identifier for the organization where the event occurred. + The unique identifier for the organization where the event occurred. - The unique identifier for the project where the event occurred. + The unique identifier for the project where the event occurred. - The `projectId` field will only be present if the event occurred at the project level, not the organization level. + The `projectId` field will only be present if the event occurred at the project level, not the organization level. - The name of the project where the event occurred. + The name of the project where the event occurred. - The `projectName` field will only be present if the event occurred at the project level, not the organization level. + The `projectName` field will only be present if the event occurred at the project level, not the organization level. diff --git a/docs/documentation/platform/auth-methods/email-password.mdx b/docs/documentation/platform/auth-methods/email-password.mdx index db23026b8..f5cb0e6f5 100644 --- a/docs/documentation/platform/auth-methods/email-password.mdx +++ b/docs/documentation/platform/auth-methods/email-password.mdx @@ -7,8 +7,39 @@ description: "Learn how to authenticate into Infisical with email and password." It is currently possible to use the **Email and Password** auth method to authenticate into the Web Dashboard and Infisical CLI. +### Emergency Kit Every **Email and Password** is accompanied by an emergency kit given to users during signup. If the password is lost or forgotten, emergency kit is only way to retrieve the access to your account. It is possible to generate a new emergency kit with the following steps: 1. Open the `Personal Settings` menu. ![open personal settings](../../../images/auth-methods/access-personal-settings.png) 2. Scroll down to the `Emergency Kit` section. 3. Enter your current password and click `Save`. + +### Change Password +You can update your account password at any time: +1. Open the `Personal Settings` menu. +![open personal settings](../../../images/auth-methods/access-personal-settings.png) +2. Navigate to the `Authentication` tab. +![open authentication tab](../../../images/auth-methods/personal-settings-authentication-tab.png) +3. In the `Change Password` section, enter your current password and new password. +![change password section](../../../images/auth-methods/personal-settings-authentication-change-email-password.png) +4. Click `Save` to save your new password. + +### Change Email +You can update your account email address: +1. Open the `Personal Settings` menu. +2. Navigate to the `Authentication` tab. +3. In the `Change Email` section, enter your new email address. +![change email section](../../../images/auth-methods/personal-settings-authentication-change-email-password.png) +4. Click `Send Verification Code` to receive an 6-digit verification code at your new email address. +5. Check your new email inbox and enter the verification code. +![change email section](../../../images/auth-methods/personal-settings-authentication-change-email-confirmation.png) +6. Click `Confirm Email Change` to complete the process. +7. You will be logged out and need to sign in again with your new email address. + + +Changing your email will remove all connected external authentication methods and terminate all active sessions for security. + + + +Email changes are disabled if SCIM is enabled for any of your organizations. Contact your organization administrator if you need to change your email address in a SCIM-enabled environment. + \ No newline at end of file diff --git a/docs/documentation/platform/gateways-deprecated/gateway-security.mdx b/docs/documentation/platform/gateways-deprecated/gateway-security.mdx new file mode 100644 index 000000000..93a7f662f --- /dev/null +++ b/docs/documentation/platform/gateways-deprecated/gateway-security.mdx @@ -0,0 +1,91 @@ +--- +title: "Gateway Security Architecture" +sidebarTitle: "Architecture" +description: "Understand the security model and tenant isolation of Infisical's Gateway" +--- + +# Gateway Security Architecture + +The Infisical Gateway enables Infisical Cloud to securely interact with private resources using mutual TLS authentication and private PKI (Public Key Infrastructure) system to ensure secure, isolated communication between multiple tenants. +This document explains the internal security architecture and how tenant isolation is maintained. + +## Security Model Overview + +### Private PKI System +Each organization (tenant) in Infisical has its own private PKI system consisting of: + +1. **Root CA**: The ultimate trust anchor for the organization +2. **Intermediate CAs**: + - Client CA: Issues certificates for cloud components + - Gateway CA: Issues certificates for gateway instances + +This hierarchical structure ensures complete isolation between organizations as each has its own independent certificate chain. + +### Certificate Hierarchy +``` +Root CA (Organization Specific) +├── Client CA +│ └── Client Certificates (Cloud Components) +└── Gateway CA + └── Gateway Certificates (Gateway Instances) +``` + +## Communication Security + +### 1. Gateway Registration +When a gateway is first deployed: + +1. Establishes initial connection using machine identity token +2. Allocates a relay address for communication +3. Exchanges certificates through a secure handshake: + - Gateway receives a unique certificate signed by organization's Gateway CA along with certificate chain for verification + +### 2. Mutual TLS Authentication +All communication between gateway and cloud uses mutual TLS (mTLS): + +- **Gateway Authentication**: + - Presents certificate signed by organization's Gateway CA + - Certificate contains unique identifiers (Organization ID, Gateway ID) + - Cloud validates complete certificate chain + +- **Cloud Authentication**: + - Presents certificate signed by organization's Client CA + - Certificate includes required organizational unit ("gateway-client") + - Gateway validates certificate chain back to organization's root CA + +### 3. Relay Communication +The relay system provides secure tunneling: + +1. **Connection Establishment**: + - Uses QUIC protocol over UDP for efficient, secure communication + - Provides built-in encryption, congestion control, and multiplexing + - Enables faster connection establishment and reduced latency + - Each organization's traffic is isolated using separate relay sessions + +2. **Traffic Isolation**: + - Each gateway gets unique relay credentials + - Traffic is end-to-end encrypted using QUIC's TLS 1.3 + - Organization's private keys never leave their environment + +## Tenant Isolation + +### Certificate-Based Isolation +- Each organization has unique root CA and intermediate CAs +- Certificates contain organization-specific identifiers +- Cross-tenant communication is cryptographically impossible + +### Gateway-Project Mapping +- Gateways are explicitly mapped to specific projects +- Access controls enforce organization boundaries +- Project-level permissions determine resource accessibility + +### Resource Access Control +1. **Project Verification**: + - Gateway verifies project membership + - Validates organization ownership + - Enforces project-level permissions + +2. **Resource Restrictions**: + - Gateways only accept connections to approved resources + - Each connection requires explicit project authorization + - Resources remain private to their assigned organization diff --git a/docs/documentation/platform/gateways/images/gateway-highlevel-diagram.png b/docs/documentation/platform/gateways-deprecated/images/gateway-highlevel-diagram.png similarity index 100% rename from docs/documentation/platform/gateways/images/gateway-highlevel-diagram.png rename to docs/documentation/platform/gateways-deprecated/images/gateway-highlevel-diagram.png diff --git a/docs/documentation/platform/gateways-deprecated/networking.mdx b/docs/documentation/platform/gateways-deprecated/networking.mdx new file mode 100644 index 000000000..6acdc1993 --- /dev/null +++ b/docs/documentation/platform/gateways-deprecated/networking.mdx @@ -0,0 +1,168 @@ +--- +title: "Networking" +description: "Network configuration and firewall requirements for Infisical Gateway" +--- + +The Infisical Gateway requires outbound network connectivity to establish secure communication with Infisical's relay infrastructure. +This page outlines the required ports, protocols, and firewall configurations needed for optimal gateway usage. + +## Network Architecture + +The gateway uses a relay-based architecture to establish secure connections: + +1. **Gateway** connects outbound to **Relay Servers** using UDP/QUIC protocol +2. **Relay Servers** facilitate secure communication between Gateway and Infisical Cloud +3. All traffic is end-to-end encrypted using mutual TLS over QUIC + +## Required Network Connectivity + +### Outbound Connections (Required) + +The gateway requires the following outbound connectivity: + +| Protocol | Destination | Ports | Purpose | +|----------|-------------|-------|---------| +| UDP | Relay Servers | 49152-65535 | Allocated relay communication (TLS) | +| TCP | app.infisical.com / eu.infisical.com | 443 | API communication and relay allocation | + +### Relay Server IP Addresses + +Your firewall must allow outbound connectivity to the following Infisical relay servers on dynamically allocated ports. + + + + ``` + 54.235.197.91:49152-65535 + 18.215.196.229:49152-65535 + 3.222.120.233:49152-65535 + 34.196.115.157:49152-65535 + ``` + + + ``` + 3.125.237.40:49152-65535 + 52.28.157.98:49152-65535 + 3.125.176.90:49152-65535 + ``` + + + Please contact your Infisical account manager for dedicated relay server IP addresses. + + + + + These IP addresses are static and managed by Infisical. Any changes will be communicated with 60-day advance notice. + + +## Protocol Details + +### QUIC over UDP + +The gateway uses QUIC (Quick UDP Internet Connections) for primary communication: + +- **Port 5349**: STUN/TURN over TLS (secure relay communication) +- **Built-in features**: Connection migration, multiplexing, reduced latency +- **Encryption**: TLS 1.3 with certificate pinning + +## Understanding Firewall Behavior with UDP + +Unlike TCP connections, UDP is a stateless protocol, and depending on your organization's firewall configuration, you may need to adjust network rules accordingly. +When the gateway sends UDP packets to a relay server, the return responses need to be allowed back through the firewall. +Modern firewalls handle this through "connection tracking" (also called "stateful inspection"), but the behavior can vary depending on your firewall configuration. + + +### Connection Tracking + +Modern firewalls automatically track UDP connections and allow return responses. This is the preferred configuration as it: +- Automatically handles return responses +- Reduces firewall rule complexity +- Avoids the need for manual IP whitelisting + +In the event that your firewall does not support connection tracking, you will need to whitelist the relay IPs to explicitly define return traffic manually. + +## Common Network Scenarios + +### Corporate Firewalls + +For corporate environments with strict egress filtering: + +1. **Whitelist relay IP addresses** (listed above) +2. **Allow UDP port 5349** outbound +3. **Configure connection tracking** for UDP return traffic +4. **Allow ephemeral port range** 49152-65535 for return traffic if connection tracking is disabled + +### Cloud Environments (AWS/GCP/Azure) + +Configure security groups to allow: +- **Outbound UDP** to relay IPs on port 5349 +- **Outbound HTTPS** to app.infisical.com/eu.infisical.com on port 443 +- **Inbound UDP** on ephemeral ports (if not using stateful rules) + +## Frequently Asked Questions + + +The gateway is designed to handle network interruptions gracefully: + +- **Automatic reconnection**: The gateway will automatically attempt to reconnect to relay servers every 5 seconds if the connection is lost +- **Connection retry logic**: Built-in retry mechanisms handle temporary network outages without manual intervention +- **Multiple relay servers**: If one relay server is unavailable, the gateway can connect to alternative relay servers +- **Persistent sessions**: Existing connections are maintained where possible during brief network interruptions +- **Graceful degradation**: The gateway logs connection issues and continues attempting to restore connectivity + +No manual intervention is typically required during network interruptions. + + + +QUIC (Quick UDP Internet Connections) provides several advantages over traditional TCP for gateway communication: + +- **Faster connection establishment**: QUIC combines transport and security handshakes, reducing connection setup time +- **Built-in encryption**: TLS 1.3 is integrated into the protocol, ensuring all traffic is encrypted by default +- **Connection migration**: QUIC connections can survive IP address changes (useful for NAT rebinding) +- **Reduced head-of-line blocking**: Multiple data streams can be multiplexed without blocking each other +- **Better performance over unreliable networks**: Advanced congestion control and packet loss recovery +- **Lower latency**: Optimized for real-time communication between gateway and cloud services + +While TCP is stateful and easier for firewalls to track, QUIC's performance benefits outweigh the additional firewall configuration requirements. + + + +No inbound ports need to be opened. The gateway only makes outbound connections: + +- **Outbound UDP** to relay servers on ports 49152-65535 +- **Outbound HTTPS** to Infisical API endpoints +- **Return responses** are handled by connection tracking or explicit IP whitelisting + +This design maintains security by avoiding the need for inbound firewall rules that could expose your network to external threats. + + + +If your firewall has strict UDP restrictions: + +1. **Work with your network team** to allow outbound UDP to the specific relay IP addresses +2. **Use explicit IP whitelisting** if connection tracking is disabled +3. **Consider network policy exceptions** for the gateway host +4. **Monitor firewall logs** to identify which specific rules are blocking traffic + +The gateway requires UDP connectivity to function - TCP-only configurations are not supported. + + + +The gateway connects to **one relay server at a time**: + +- **Single active connection**: Only one relay connection is established per gateway instance +- **Automatic failover**: If the current relay becomes unavailable, the gateway will connect to an alternative relay +- **Load distribution**: Different gateway instances may connect to different relay servers for load balancing +- **No manual selection**: The Infisical API automatically assigns the optimal relay server based on availability and proximity + +You should whitelist all relay IP addresses to ensure proper failover functionality. + + +No, relay servers cannot decrypt any traffic passing through them: + +- **End-to-end encryption**: All traffic between the gateway and Infisical Cloud is encrypted using mutual TLS with certificate pinning +- **Relay acts as a tunnel**: The relay server only forwards encrypted packets - it has no access to encryption keys +- **No data storage**: Relay servers do not store any traffic or network-identifiable information +- **Certificate isolation**: Each organization has its own private PKI system, ensuring complete tenant isolation + +The relay infrastructure is designed as a secure forwarding mechanism, similar to a VPN tunnel, where the relay provider cannot see the contents of the traffic flowing through it. + \ No newline at end of file diff --git a/docs/documentation/platform/gateways-deprecated/overview.mdx b/docs/documentation/platform/gateways-deprecated/overview.mdx new file mode 100644 index 000000000..f81809f7b --- /dev/null +++ b/docs/documentation/platform/gateways-deprecated/overview.mdx @@ -0,0 +1,352 @@ +--- +title: "Gateway" +sidebarTitle: "Overview" +description: "How to access private network resources from Infisical" +--- + +![Alt text](/documentation/platform/gateways-deprecated/images/gateway-highlevel-diagram.png) + +The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment. +This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases. +Common use cases include generating dynamic credentials or rotating credentials for private databases. + + + Gateway is a paid feature available under the Enterprise Tier for Infisical + Cloud users. Self-hosted Infisical users can contact + [sales@infisical.com](mailto:sales@infisical.com) to purchase an enterprise + license. + + +## How It Works + +The Gateway serves as a secure intermediary that facilitates direct communication between the Infisical server and your private network. +It’s a lightweight daemon packaged within the Infisical CLI, making it easy to deploy and manage. Once set up, the Gateway establishes a connection with a relay server, ensuring that all communication between Infisical and your Gateway is fully end-to-end encrypted. +This setup guarantees that only the platform and your Gateway can decrypt the transmitted information, keeping communication with your resources secure, private and isolated. + +## Deployment + +The Infisical Gateway is seamlessly integrated into the Infisical CLI under the `gateway` command, making it simple to deploy and manage. +You can install the Gateway in all the same ways you install the Infisical CLI—whether via npm, Docker, or a binary. +For detailed installation instructions, refer to the Infisical [CLI Installation instructions](/cli/overview). + +To function, the Gateway must authenticate with Infisical. This requires a machine identity configured with the appropriate permissions to create and manage a Gateway. +Once authenticated, the Gateway establishes a secure connection with Infisical to allow your private resources to be reachable. + +### Get started + + + + 1. Navigate to **Organization Access Control** in your Infisical dashboard. + 2. Create a dedicated machine identity for your Gateway. + 3. **Best Practice:** Assign a unique identity to each Gateway for better security and management. + ![Create Gateway Identity](../../../images/platform/gateways/create-identity-for-gateway.png) + + + + You'll need to choose an authentication method to initiate communication with Infisical. View the available machine identity authentication methods [here](/documentation/platform/identities/machine-identities). + + + + Use the Infisical CLI to deploy the Gateway. You can run it directly or install it as a systemd service for production: + + + + For production deployments on Linux, install the Gateway as a systemd service: + ```bash + sudo infisical gateway install --token --domain + sudo systemctl start infisical-gateway + ``` + This will install and start the Gateway as a secure systemd service that: + - Runs with restricted privileges: + - Runs as root user (required for secure token management) + - Restricted access to home directories + - Private temporary directory + - Automatically restarts on failure + - Starts on system boot + - Manages token and domain configuration securely in `/etc/infisical/gateway.conf` + + + The install command requires: + - Linux operating system + - Root/sudo privileges + - Systemd + + + + + + The Gateway can be installed via [Helm](https://helm.sh/). Helm is a package manager for Kubernetes that allows you to define, install, and upgrade Kubernetes applications. + + For production deployments on Kubernetes, install the Gateway using the Infisical Helm chart: + + ### Install the latest Helm Chart repository + ```bash + helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' + ``` + + ### Update the Helm Chart repository + ```bash + helm repo update + ``` + + ### Create a Kubernetes Secret containing gateway environment variables + + The gateway supports all identity authentication methods through the use of environment variables. + The environment variables must be set in the `infisical-gateway-environment` Kubernetes secret. + + + #### Supported authentication methods + + + + The Universal Auth method is a simple and secure way to authenticate with Infisical. It requires a client ID and a client secret to authenticate with Infisical. + + + + + Your machine identity client ID. + + + Your machine identity client secret. + + + The authentication method to use. Must be `universal-auth` when using Universal Auth. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=universal-auth --from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_ID= --from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET= + ``` + + + + The Native Kubernetes method is used to authenticate with Infisical when running in a Kubernetes environment. It requires a service account token to authenticate with Infisical. + + + + + Your machine identity ID. + + + Path to the Kubernetes service account token to use. Default: `/var/run/secrets/kubernetes.io/serviceaccount/token`. + + + The authentication method to use. Must be `kubernetes` when using Native Kubernetes. + + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=kubernetes --from-literal=INFISICAL_MACHINE_IDENTITY_ID= + ``` + + + + The Native Azure method is used to authenticate with Infisical when running in an Azure environment. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `azure` when using Native Azure. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=azure --from-literal=INFISICAL_MACHINE_IDENTITY_ID= + ``` + + + The Native GCP ID Token method is used to authenticate with Infisical when running in a GCP environment. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `gcp-id-token` when using Native GCP ID Token. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=gcp-id-token --from-literal=INFISICAL_MACHINE_IDENTITY_ID= + ``` + + + + The GCP IAM method is used to authenticate with Infisical with a GCP service account key. + + + + + Your machine identity ID. + + + Path to your GCP service account key file _(Must be in JSON format!)_ + + + The authentication method to use. Must be `gcp-iam` when using GCP IAM. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=gcp-iam --from-literal=INFISICAL_MACHINE_IDENTITY_ID= --from-literal=INFISICAL_GCP_SERVICE_ACCOUNT_KEY_FILE_PATH= + ``` + + + + + The AWS IAM method is used to authenticate with Infisical with an AWS IAM role while running in an AWS environment like EC2, Lambda, etc. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `aws-iam` when using Native AWS IAM. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=aws-iam --from-literal=INFISICAL_MACHINE_IDENTITY_ID= + ``` + + + + The OIDC Auth method is used to authenticate with Infisical via identity tokens with OIDC. + + + + + Your machine identity ID. + + + The OIDC JWT from the identity provider. + + + The authentication method to use. Must be `oidc-auth` when using OIDC Auth. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=oidc-auth --from-literal=INFISICAL_MACHINE_IDENTITY_ID= --from-literal=INFISICAL_JWT= + ``` + + + + The JWT Auth method is used to authenticate with Infisical via a JWT token. + + + + + The JWT token to use for authentication. + + + Your machine identity ID. + + + The authentication method to use. Must be `jwt-auth` when using JWT Auth. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=jwt-auth --from-literal=INFISICAL_JWT= --from-literal=INFISICAL_MACHINE_IDENTITY_ID= + ``` + + + You can use the `INFISICAL_TOKEN` environment variable to authenticate with Infisical with a raw machine identity access token. + + + + + The machine identity access token to use for authentication. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_TOKEN= + ``` + + + + + #### Other environment variables + + + + The API URL to use for the gateway. By default, `INFISICAL_API_URL` is set to `https://app.infisical.com`. + + + + + ### Install the Infisical Gateway Helm Chart + ```bash + helm install infisical-gateway infisical-helm-charts/infisical-gateway + ``` + + ### Check the gateway logs + After installing the gateway, you can check the logs to ensure it's running as expected. + + ```bash + kubectl logs deployment/infisical-gateway + ``` + + You should see the following output which indicates the gateway is running as expected. + ```bash + $ kubectl logs deployment/infisical-gateway + INF Provided relay port 5349. Using TLS + INF Connected with relay + INF 10.0.101.112:56735 + INF Starting relay connection health check + INF Gateway started successfully + INF New connection from: 10.0.1.8:34051 + INF Gateway is reachable by Infisical + ``` + + + + + For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command: + ```bash + infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain) + ``` + + Alternatively, if you already have the token, use it directly with the `--token` flag: + ```bash + infisical gateway --token + ``` + + Or set it as an environment variable: + ```bash + export INFISICAL_TOKEN= + infisical gateway + ``` + + + + For detailed information about the gateway command and its options, see the [gateway command documentation](/cli/commands/gateway). + + + Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical. + + + + + + To confirm your Gateway is working, check the deployment status by looking for the message **"Gateway started successfully"** in the Gateway logs. This indicates the Gateway is running properly. Next, verify its registration by opening your Infisical dashboard, navigating to **Organization Access Control**, and selecting the **Gateways** tab. Your newly deployed Gateway should appear in the list. + ![Gateway List](../../../images/platform/gateways/gateway-list.png) + + diff --git a/docs/documentation/platform/gateways/gateway-security.mdx b/docs/documentation/platform/gateways/gateway-security.mdx index 93a7f662f..6962c627e 100644 --- a/docs/documentation/platform/gateways/gateway-security.mdx +++ b/docs/documentation/platform/gateways/gateway-security.mdx @@ -6,86 +6,133 @@ description: "Understand the security model and tenant isolation of Infisical's # Gateway Security Architecture -The Infisical Gateway enables Infisical Cloud to securely interact with private resources using mutual TLS authentication and private PKI (Public Key Infrastructure) system to ensure secure, isolated communication between multiple tenants. +The Infisical Gateway enables secure access to private resources using SSH reverse tunnels, certificate-based authentication, and a comprehensive PKI (Public Key Infrastructure) system. The architecture provides end-to-end encryption and complete tenant isolation through multiple certificate authorities. This document explains the internal security architecture and how tenant isolation is maintained. ## Security Model Overview -### Private PKI System -Each organization (tenant) in Infisical has its own private PKI system consisting of: +### Certificate Architecture -1. **Root CA**: The ultimate trust anchor for the organization -2. **Intermediate CAs**: - - Client CA: Issues certificates for cloud components - - Gateway CA: Issues certificates for gateway instances +The gateway system uses multiple certificate authorities depending on deployment configuration: -This hierarchical structure ensures complete isolation between organizations as each has its own independent certificate chain. +**For Organizations Using Infisical-Managed Relays:** + +- **Instance relay SSH Client CA & Server CA** - Gateway ↔ Infisical Relay Server authentication +- **Instance relay PKI Client CA & Server CA** - Platform ↔ Infisical Relay Server authentication +- **Organization Gateway Client CA & Server CA** - Platform ↔ Gateway authentication + +**For Organizations Using Customer-Deployed Relays:** + +- **Organization relay SSH Client CA & Server CA** - Gateway ↔ Customer Relay Server authentication +- **Organization relay PKI Client CA & Server CA** - Platform ↔ Customer Relay Server authentication +- **Organization Gateway Client CA & Server CA** - Platform ↔ Gateway authentication ### Certificate Hierarchy + ``` -Root CA (Organization Specific) -├── Client CA -│ └── Client Certificates (Cloud Components) -└── Gateway CA - └── Gateway Certificates (Gateway Instances) +Instance Level (Shared Relays): +├── Instance Relay SSH CA (Gateway ↔ Relay) +├── Instance Relay PKI CA (Platform ↔ Relay) + +Organization Level: +├── Organization Relay SSH CA (Gateway ↔ Org Relay) +├── Organization Relay PKI CA (Platform ↔ Org Relay) +└── Organization Gateway CA (Platform ↔ Gateway) ``` ## Communication Security ### 1. Gateway Registration + When a gateway is first deployed: -1. Establishes initial connection using machine identity token -2. Allocates a relay address for communication -3. Exchanges certificates through a secure handshake: - - Gateway receives a unique certificate signed by organization's Gateway CA along with certificate chain for verification +1. Authenticates with Infisical using machine identity token +2. Receives SSH certificates for relay server authentication +3. Establishes SSH reverse tunnel to assigned relay server +4. Certificate issuance varies by relay configuration: + - **Infisical-managed relay**: Receives Instance relay SSH client certificate + Instance relay SSH Server CA + - **Customer-deployed relay**: Receives Organization relay SSH client certificate + Organization relay SSH Server CA -### 2. Mutual TLS Authentication -All communication between gateway and cloud uses mutual TLS (mTLS): +### 2. SSH Tunnel Authentication + +Gateway ↔ Relay Server communication uses SSH certificate authentication: - **Gateway Authentication**: - - Presents certificate signed by organization's Gateway CA - - Certificate contains unique identifiers (Organization ID, Gateway ID) - - Cloud validates complete certificate chain -- **Cloud Authentication**: - - Presents certificate signed by organization's Client CA - - Certificate includes required organizational unit ("gateway-client") - - Gateway validates certificate chain back to organization's root CA + - Presents SSH client certificate (Instance or Organization relay SSH Client CA) + - Certificate contains gateway identification and permissions + - Relay server validates certificate against appropriate SSH Client CA -### 3. Relay Communication -The relay system provides secure tunneling: +- **Relay Server Authentication**: + - Presents SSH server certificate (Instance or Organization relay SSH Server CA) + - Gateway validates certificate against appropriate SSH Server CA + - Ensures gateway connects to legitimate relay infrastructure -1. **Connection Establishment**: - - Uses QUIC protocol over UDP for efficient, secure communication - - Provides built-in encryption, congestion control, and multiplexing - - Enables faster connection establishment and reduced latency - - Each organization's traffic is isolated using separate relay sessions +### 3. Platform-to-Gateway Direct Connection -2. **Traffic Isolation**: - - Each gateway gets unique relay credentials - - Traffic is end-to-end encrypted using QUIC's TLS 1.3 - - Organization's private keys never leave their environment +The platform establishes secure direct connections with gateways through a **TLS-pinned tunnel** mechanism: + +1. **TLS-Pinned Tunnel Establishment**: + + - Gateway initiates outbound connection to platform through SSH reverse tunnel + - Platform establishes direct mTLS connection with gateway using Organization Gateway certificates + - TLS certificate pinning ensures the connection is bound to the specific gateway identity + - No inbound connections required - all communication flows through the outbound tunnel + +2. **Connection Flow**: + + ``` + Platform ←→ [SSH Reverse Tunnel] ←→ Gateway + ``` + + - Gateway maintains persistent outbound SSH tunnel to relay server + - Platform connects directly to gateway through this tunnel + - TLS handshake occurs over the SSH tunnel, establishing mTLS connection + - Application traffic flows through the TLS-pinned tunnel + +3. **Security Benefits**: + + - **No inbound connections**: Gateway never needs to accept incoming connections + - **Certificate-based authentication**: Uses Organization Gateway certificates for mutual TLS + - **Double encryption**: TLS traffic within SSH tunnel provides layered security + - **Relay server isolation**: Relay cannot decrypt either TLS or application data + - **Tenant isolation**: Each organization's traffic flows through separate authenticated channels ## Tenant Isolation -### Certificate-Based Isolation -- Each organization has unique root CA and intermediate CAs -- Certificates contain organization-specific identifiers -- Cross-tenant communication is cryptographically impossible +### Multi-Layer Certificate Isolation -### Gateway-Project Mapping -- Gateways are explicitly mapped to specific projects -- Access controls enforce organization boundaries -- Project-level permissions determine resource accessibility +The architecture provides tenant isolation through multiple certificate authority layers: + +- **Instance-level CAs**: Shared relay infrastructure uses instance-level certificates +- **Organization-level CAs**: Each organization has unique certificate authorities +- **Relay deployment flexibility**: Organizations can choose shared or dedicated relay infrastructure +- **Cryptographic separation**: Cross-tenant communication is cryptographically impossible + +### Authentication Flows by Deployment Type + +**Infisical-Managed Relay Deployments:** + +- Gateway authenticates with relay using Instance relay SSH certificates +- Platform authenticates with relay using Instance relay PKI certificates +- Platform authenticates with gateway using Organization Gateway certificates + +**Customer-Deployed Relay Deployments:** + +- Gateway authenticates with relay using Organization relay SSH certificates +- Platform authenticates with relay using Organization relay PKI certificates +- Platform authenticates with gateway using Organization Gateway certificates ### Resource Access Control -1. **Project Verification**: - - Gateway verifies project membership - - Validates organization ownership - - Enforces project-level permissions -2. **Resource Restrictions**: - - Gateways only accept connections to approved resources - - Each connection requires explicit project authorization - - Resources remain private to their assigned organization +1. **Certificate Validation**: + + - All connections require valid certificates from appropriate CAs + - Embedded certificate details control access permissions + - Ephemeral certificate validation ensures time-bound access + +2. **Network Isolation**: + + - Each organization's traffic flows through isolated certificate-authenticated channels + - Relay servers route traffic based on certificate validation without content access + - Gateway validates all incoming connections against Organization Gateway Client CA diff --git a/docs/documentation/platform/gateways/networking.mdx b/docs/documentation/platform/gateways/networking.mdx index 6acdc1993..2b068a535 100644 --- a/docs/documentation/platform/gateways/networking.mdx +++ b/docs/documentation/platform/gateways/networking.mdx @@ -3,16 +3,17 @@ title: "Networking" description: "Network configuration and firewall requirements for Infisical Gateway" --- -The Infisical Gateway requires outbound network connectivity to establish secure communication with Infisical's relay infrastructure. +The Infisical Gateway requires outbound network connectivity to establish secure SSH reverse tunnels with relay servers. This page outlines the required ports, protocols, and firewall configurations needed for optimal gateway usage. ## Network Architecture -The gateway uses a relay-based architecture to establish secure connections: +The gateway uses SSH reverse tunnels to establish secure connections with end-to-end encryption: -1. **Gateway** connects outbound to **Relay Servers** using UDP/QUIC protocol -2. **Relay Servers** facilitate secure communication between Gateway and Infisical Cloud -3. All traffic is end-to-end encrypted using mutual TLS over QUIC +1. **Gateway** connects outbound to **Relay Servers** using SSH over TCP +2. **Infisical platform** establishes mTLS connections with gateways for application traffic +3. **Relay Servers** route the doubly-encrypted traffic (mTLS payload within SSH tunnels) between the platform and gateways +4. **Double encryption** ensures relay servers cannot access application data - only the platform and gateway can decrypt traffic ## Required Network Connectivity @@ -20,65 +21,70 @@ The gateway uses a relay-based architecture to establish secure connections: The gateway requires the following outbound connectivity: -| Protocol | Destination | Ports | Purpose | -|----------|-------------|-------|---------| -| UDP | Relay Servers | 49152-65535 | Allocated relay communication (TLS) | -| TCP | app.infisical.com / eu.infisical.com | 443 | API communication and relay allocation | +| Protocol | Destination | Ports | Purpose | +| -------- | ------------------------------------ | ----- | ------------------------------------------ | +| TCP | Relay Servers | 2222 | SSH reverse tunnel establishment | +| TCP | app.infisical.com / eu.infisical.com | 443 | API communication and certificate requests | -### Relay Server IP Addresses +### Relay Server Connectivity -Your firewall must allow outbound connectivity to the following Infisical relay servers on dynamically allocated ports. +**For Instance Relays (Infisical Cloud):** Your firewall must allow outbound connectivity to Infisical-managed relay servers. + +**For Organization Relays:** Your firewall must allow outbound connectivity to your own relay server IP addresses or hostnames. + +**For Self-hosted Instance Relays:** Your firewall must allow outbound connectivity to relay servers configured by your instance administrator. - - ``` - 54.235.197.91:49152-65535 - 18.215.196.229:49152-65535 - 3.222.120.233:49152-65535 - 34.196.115.157:49152-65535 - ``` + + Infisical provides multiple managed relay servers with static IP addresses. + You can whitelist these IPs ahead of time based on which relay server you + choose to connect to. **Firewall requirements:** Allow outbound TCP + connections to the desired relay server IP on port 2222. - - ``` - 3.125.237.40:49152-65535 - 52.28.157.98:49152-65535 - 3.125.176.90:49152-65535 - ``` + + You control the relay server IP addresses or hostnames when deploying your + own organization relays. **Firewall requirements:** Allow outbound TCP + connections to your relay server IP or hostname on port 2222. For example, + if your relay is at `203.0.113.100` or `relay.example.com`, allow TCP to + `203.0.113.100:2222` or `relay.example.com:2222`. - - Please contact your Infisical account manager for dedicated relay server IP addresses. + + Contact your instance administrator for the relay server IP addresses or + hostnames configured for your deployment. **Firewall requirements:** Allow + outbound TCP connections to instance relay servers on port 2222. - - These IP addresses are static and managed by Infisical. Any changes will be communicated with 60-day advance notice. - - ## Protocol Details -### QUIC over UDP +### SSH over TCP -The gateway uses QUIC (Quick UDP Internet Connections) for primary communication: +The gateway uses SSH reverse tunnels for primary communication: -- **Port 5349**: STUN/TURN over TLS (secure relay communication) -- **Built-in features**: Connection migration, multiplexing, reduced latency -- **Encryption**: TLS 1.3 with certificate pinning +- **Port 2222**: SSH connection to relay servers +- **Built-in features**: Automatic reconnection, certificate-based authentication, encrypted tunneling +- **Encryption**: SSH with certificate-based authentication and key exchange -## Understanding Firewall Behavior with UDP +## Firewall Configuration for SSH -Unlike TCP connections, UDP is a stateless protocol, and depending on your organization's firewall configuration, you may need to adjust network rules accordingly. -When the gateway sends UDP packets to a relay server, the return responses need to be allowed back through the firewall. -Modern firewalls handle this through "connection tracking" (also called "stateful inspection"), but the behavior can vary depending on your firewall configuration. +The gateway uses standard SSH over TCP, making firewall configuration straightforward. +### TCP Connection Handling -### Connection Tracking +SSH connections over TCP are stateful and handled seamlessly by all modern firewalls: -Modern firewalls automatically track UDP connections and allow return responses. This is the preferred configuration as it: -- Automatically handles return responses -- Reduces firewall rule complexity -- Avoids the need for manual IP whitelisting +- **Established connections** are automatically tracked +- **Return traffic** is allowed for established outbound connections +- **No special configuration** needed for connection tracking +- **Standard SSH protocol** that enterprise firewalls handle well -In the event that your firewall does not support connection tracking, you will need to whitelist the relay IPs to explicitly define return traffic manually. +### Simplified Firewall Rules + +Since SSH uses TCP, you only need simple outbound rules: + +1. **Allow outbound TCP** to relay servers (IP addresses or hostnames) on port 2222 +2. **Allow outbound HTTPS** to Infisical API endpoints on port 443 +3. **No inbound rules required** - all connections are outbound only ## Common Network Scenarios @@ -86,83 +92,87 @@ In the event that your firewall does not support connection tracking, you will n For corporate environments with strict egress filtering: -1. **Whitelist relay IP addresses** (listed above) -2. **Allow UDP port 5349** outbound -3. **Configure connection tracking** for UDP return traffic -4. **Allow ephemeral port range** 49152-65535 for return traffic if connection tracking is disabled +1. **Allow outbound TCP** to relay servers (IP addresses or hostnames) on port 2222 +2. **Allow outbound HTTPS** to the Infisical API server on port 443 +3. **No inbound rules required** - all connections are outbound only +4. **Standard TCP rules** - simple and straightforward configuration ### Cloud Environments (AWS/GCP/Azure) Configure security groups to allow: -- **Outbound UDP** to relay IPs on port 5349 + +- **Outbound TCP** to relay servers (IP addresses or hostnames) on port 2222 - **Outbound HTTPS** to app.infisical.com/eu.infisical.com on port 443 -- **Inbound UDP** on ephemeral ports (if not using stateful rules) +- **No inbound rules required** - SSH reverse tunnels are outbound only ## Frequently Asked Questions The gateway is designed to handle network interruptions gracefully: -- **Automatic reconnection**: The gateway will automatically attempt to reconnect to relay servers every 5 seconds if the connection is lost +- **Automatic reconnection**: The gateway will automatically attempt to reconnect to relay servers if the SSH connection is lost - **Connection retry logic**: Built-in retry mechanisms handle temporary network outages without manual intervention -- **Multiple relay servers**: If one relay server is unavailable, the gateway can connect to alternative relay servers -- **Persistent sessions**: Existing connections are maintained where possible during brief network interruptions +- **Persistent SSH tunnels**: SSH connections are automatically re-established when connectivity is restored +- **Certificate rotation**: The gateway handles certificate renewal automatically during reconnection - **Graceful degradation**: The gateway logs connection issues and continues attempting to restore connectivity No manual intervention is typically required during network interruptions. + - -QUIC (Quick UDP Internet Connections) provides several advantages over traditional TCP for gateway communication: + +SSH over TCP provides several advantages for enterprise gateway communication: -- **Faster connection establishment**: QUIC combines transport and security handshakes, reducing connection setup time -- **Built-in encryption**: TLS 1.3 is integrated into the protocol, ensuring all traffic is encrypted by default -- **Connection migration**: QUIC connections can survive IP address changes (useful for NAT rebinding) -- **Reduced head-of-line blocking**: Multiple data streams can be multiplexed without blocking each other -- **Better performance over unreliable networks**: Advanced congestion control and packet loss recovery -- **Lower latency**: Optimized for real-time communication between gateway and cloud services +- **Firewall-friendly**: TCP is stateful and handled seamlessly by all enterprise firewalls +- **Standard protocol**: SSH is a well-established protocol that network teams are familiar with +- **Certificate-based security**: Uses SSH certificates for strong authentication without shared secrets +- **Automatic tunneling**: SSH reverse tunnels handle all the complexity of secure communication +- **Enterprise compatibility**: Works reliably across all enterprise network configurations + +TCP's reliability and firewall compatibility make it ideal for enterprise environments where network policies are strictly managed. -While TCP is stateful and easier for firewalls to track, QUIC's performance benefits outweigh the additional firewall configuration requirements. No inbound ports need to be opened. The gateway only makes outbound connections: -- **Outbound UDP** to relay servers on ports 49152-65535 -- **Outbound HTTPS** to Infisical API endpoints -- **Return responses** are handled by connection tracking or explicit IP whitelisting +- **Outbound SSH** to relay servers on port 2222 +- **Outbound HTTPS** to Infisical API endpoints on port 443 +- **SSH reverse tunnels** handle all communication - no return traffic configuration needed This design maintains security by avoiding the need for inbound firewall rules that could expose your network to external threats. + - -If your firewall has strict UDP restrictions: + +If your firewall has strict outbound restrictions: -1. **Work with your network team** to allow outbound UDP to the specific relay IP addresses -2. **Use explicit IP whitelisting** if connection tracking is disabled -3. **Consider network policy exceptions** for the gateway host +1. **Work with your network team** to allow outbound TCP connections on port 2222 to relay servers (IP addresses or hostnames) +2. **Allow standard SSH traffic** - most enterprises already have SSH policies in place +3. **Consider network policy exceptions** for the gateway host if needed 4. **Monitor firewall logs** to identify which specific rules are blocking traffic -The gateway requires UDP connectivity to function - TCP-only configurations are not supported. -The gateway connects to **one relay server at a time**: +The gateway connects to **one relay server**: -- **Single active connection**: Only one relay connection is established per gateway instance -- **Automatic failover**: If the current relay becomes unavailable, the gateway will connect to an alternative relay -- **Load distribution**: Different gateway instances may connect to different relay servers for load balancing -- **No manual selection**: The Infisical API automatically assigns the optimal relay server based on availability and proximity +- **Single SSH connection**: Each gateway establishes one SSH reverse tunnel to its assigned relay server +- **Named relay assignment**: Gateways connect to the specific relay server specified by `--relay` +- **Automatic reconnection**: If the relay connection is lost, the gateway automatically reconnects to the same relay +- **Certificate-based authentication**: Each connection uses SSH certificates issued by Infisical for secure authentication -You should whitelist all relay IP addresses to ensure proper failover functionality. -No, relay servers cannot decrypt any traffic passing through them: +No, relay servers cannot decrypt any traffic passing through them due to end-to-end encryption: -- **End-to-end encryption**: All traffic between the gateway and Infisical Cloud is encrypted using mutual TLS with certificate pinning -- **Relay acts as a tunnel**: The relay server only forwards encrypted packets - it has no access to encryption keys -- **No data storage**: Relay servers do not store any traffic or network-identifiable information -- **Certificate isolation**: Each organization has its own private PKI system, ensuring complete tenant isolation +- **Client-to-Gateway mTLS (via TLS-pinned tunnel)**: Clients connect via a proxy that establishes a TLS-pinned tunnel to the gateway; mTLS between the client and gateway is negotiated inside this tunnel, encrypting all application traffic +- **SSH tunnel encryption**: The mTLS-encrypted traffic is then transmitted through SSH reverse tunnels to relay servers +- **Double encryption**: Traffic is encrypted twice - once by client mTLS and again by SSH tunnels +- **Relay only routes traffic**: The relay server only routes the doubly-encrypted traffic without access to either encryption layer +- **No data storage**: Relay servers do not store any traffic or sensitive information +- **Certificate isolation**: Each connection uses unique certificates, ensuring complete tenant isolation -The relay infrastructure is designed as a secure forwarding mechanism, similar to a VPN tunnel, where the relay provider cannot see the contents of the traffic flowing through it. - \ No newline at end of file +The relay infrastructure is designed as a secure routing mechanism where only the client and gateway can decrypt the actual application traffic. + + diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx index 127e544b7..b8ea0102a 100644 --- a/docs/documentation/platform/gateways/overview.mdx +++ b/docs/documentation/platform/gateways/overview.mdx @@ -4,33 +4,53 @@ sidebarTitle: "Overview" description: "How to access private network resources from Infisical" --- -![Alt text](/documentation/platform/gateways/images/gateway-highlevel-diagram.png) +![Architecture Overview](../../../images/platform/gateways/gateway-highlevel-diagram.png) + +The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment. This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases. + +**Architecture Components:** + +- **Gateway**: Lightweight agent deployed within your VPCs that provides access to private resources +- **Relay**: Infrastructure that routes encrypted traffic (instance-wide or organization-specific) -The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment. -This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases. Common use cases include generating dynamic credentials or rotating credentials for private databases. - **Note:** Gateway is a paid feature. - **Infisical Cloud users:** Gateway is - available under the **Enterprise Tier**. - **Self-Hosted Infisical:** Please - contact [sales@infisical.com](mailto:sales@infisical.com) to purchase an - enterprise license. + Gateway is a paid feature available under the Enterprise Tier for Infisical + Cloud users. Self-hosted Infisical users can contact + [sales@infisical.com](mailto:sales@infisical.com) to purchase an enterprise + license. ## How It Works -The Gateway serves as a secure intermediary that facilitates direct communication between the Infisical server and your private network. -It’s a lightweight daemon packaged within the Infisical CLI, making it easy to deploy and manage. Once set up, the Gateway establishes a connection with a relay server, ensuring that all communication between Infisical and your Gateway is fully end-to-end encrypted. -This setup guarantees that only the platform and your Gateway can decrypt the transmitted information, keeping communication with your resources secure, private and isolated. +The Gateway system uses SSH reverse tunnels for secure, firewall-friendly connectivity: + +1. **Gateway Registration**: The gateway establishes an outbound SSH reverse tunnel to a relay server using SSH certificates issued by Infisical +2. **Relay Routing**: The relay server routes encrypted traffic between the Infisical platform and gateways +3. **Resource Access**: The Infisical platform connects to your private resources through the established gateway connections + +**Key Benefits:** + +- **No inbound firewall rules needed** - all connections are outbound from your network +- **Firewall-friendly** - uses standard SSH over TCP +- **Certificate-based authentication** provides enhanced security +- **Automatic reconnection** if connections are lost ## Deployment -The Infisical Gateway is seamlessly integrated into the Infisical CLI under the `gateway` command, making it simple to deploy and manage. +The Infisical Gateway is integrated into the Infisical CLI under the `gateway` command, making it simple to deploy and manage. You can install the Gateway in all the same ways you install the Infisical CLI—whether via npm, Docker, or a binary. For detailed installation instructions, refer to the Infisical [CLI Installation instructions](/cli/overview). -To function, the Gateway must authenticate with Infisical. This requires a machine identity configured with the appropriate permissions to create and manage a Gateway. -Once authenticated, the Gateway establishes a secure connection with Infisical to allow your private resources to be reachable. +**Prerequisites:** + +1. **Relay Server**: Before deploying gateways, you need a running relay server: + - **Infisical Cloud**: Instance relays are already available - no setup needed + - **Self-hosted**: Instance admin must set up shared instance relays, or organizations can deploy their own +2. **Machine Identity**: Configure a machine identity with appropriate permissions to create and manage gateways + +Once authenticated, the Gateway establishes an SSH reverse tunnel to the specified relay server, allowing secure access to your private resources. ### Get started @@ -46,14 +66,51 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t You'll need to choose an authentication method to initiate communication with Infisical. View the available machine identity authentication methods [here](/documentation/platform/identities/machine-identities). + + You have two options for relay infrastructure: + + + + **Infisical Cloud:** Instance relays are already running and available - **no setup required**. You can immediately proceed to deploy gateways using these shared relays. + + **Self-hosted:** If your instance admin has set up shared instance relays, you can use them directly. If not, the instance admin can set them up: + ```bash + # Instance admin sets up shared relay (one-time setup) + export INFISICAL_RELAY_AUTH_SECRET= + infisical relay start --type=instance --ip= --name= + ``` + + + **Available for all users:** Deploy your own dedicated relay infrastructure for enhanced control: + ```bash + # Deploy organization-specific relay + infisical relay start --type=org --ip= --name= --auth-method=universal-auth --client-id= --client-secret= + ``` + + **When to choose this:** + - You need lower latency (deploy closer to your resources) + - Enhanced security requirements + - Compliance needs (data sovereignty, air-gapped environments) + - Custom network policies + + + + Use the Infisical CLI to deploy the Gateway. You can run it directly or install it as a systemd service for production: For production deployments on Linux, install the Gateway as a systemd service: + + + **Gateway v2:** The `infisical gateway systemd install` command deploys the new Gateway v2 component. + + If you are migrating from Gateway v1 (legacy `infisical gateway install` command), this is not in-place. Gateway v2 provisions new gateway instances with new gateway IDs. Update any resources that reference a gateway ID (for example: dynamic secret configs, app connections, or other gateway-bound resources) to use the new Gateway v2 gateway ID. + + ```bash - sudo infisical gateway install --token --domain + sudo infisical gateway systemd install --token --domain --name --relay sudo systemctl start infisical-gateway ``` This will install and start the Gateway as a secure systemd service that: @@ -81,7 +138,7 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t ### Install the latest Helm Chart repository ```bash - helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' + helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' ``` ### Update the Helm Chart repository @@ -116,7 +173,12 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t ```bash - kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=universal-auth --from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_ID= --from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET= + kubectl create secret generic infisical-gateway-environment \ + --from-literal=INFISICAL_AUTH_METHOD=universal-auth \ + --from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_ID= \ + --from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET= \ + --from-literal=INFISICAL_RELAY_NAME= \ + --from-literal=INFISICAL_GATEWAY_NAME= ``` @@ -283,6 +345,29 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t + #### Required environment variables + + In addition to the authentication method above, you **must** include these required variables: + + + + The name of the relay server that this gateway should connect to. + + + The name of this gateway instance. + + + + **Complete example with required variables:** + ```bash + kubectl create secret generic infisical-gateway-environment \ + --from-literal=INFISICAL_AUTH_METHOD=universal-auth \ + --from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_ID= \ + --from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET= \ + --from-literal=INFISICAL_RELAY_NAME= \ + --from-literal=INFISICAL_GATEWAY_NAME= + ``` + #### Other environment variables @@ -291,8 +376,13 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t - ### Install the Infisical Gateway Helm Chart + + **Version mapping:** Helm chart versions `>= 1.0.0` contain the new Gateway v2 component. Helm chart versions `<= 0.0.5` contain the legacy Gateway v1 component. + + If you are moving from Gateway v1 (chart `<= 0.0.5`) to Gateway v2 (chart `>= 1.0.0`), this is not in-place. Gateway v2 provisions new gateway instances with new gateway IDs. Update any resources that reference a gateway ID (for example: dynamic secret configs, app connections, or other gateway-bound resources) to use the new Gateway v2 gateway ID. + + ```bash helm install infisical-gateway infisical-helm-charts/infisical-gateway ``` @@ -306,14 +396,18 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t You should see the following output which indicates the gateway is running as expected. ```bash - $ kubectl logs deployment/infisical-gateway - INF Provided relay port 5349. Using TLS - INF Connected with relay - INF 10.0.101.112:56735 - INF Starting relay connection health check - INF Gateway started successfully - INF New connection from: 10.0.1.8:34051 - INF Gateway is reachable by Infisical + $ kubectl logs deployment/infisical-gateway + 12:43AM INF Starting gateway + 12:43AM INF Starting gateway certificate renewal goroutine + 12:43AM INF Successfully registered gateway and received certificates + 12:43AM INF Connecting to relay server infisical-start on 152.42.218.156:2222... + 12:43AM INF Relay connection established for gateway + 12:43AM INF Received incoming connection, starting TLS handshake + 12:43AM INF TLS handshake completed successfully + 12:43AM INF Negotiated ALPN protocol: infisical-ping + 12:43AM INF Starting ping handler + 12:43AM INF Ping handler completed + 12:43AM INF Gateway is reachable by Infisical ``` @@ -321,27 +415,31 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command: ```bash - infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain) + infisical gateway start --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain) --relay= --name= ``` Alternatively, if you already have the token, use it directly with the `--token` flag: ```bash - infisical gateway --token + infisical gateway start --token --relay= --name= ``` Or set it as an environment variable: ```bash export INFISICAL_TOKEN= - infisical gateway + infisical gateway start --relay= --name= ``` - For detailed information about the gateway command and its options, see the [gateway command documentation](/cli/commands/gateway). + For detailed information about the gateway commands and their options, see the [gateway command documentation](/cli/commands/gateway). - Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical. + **Requirements:** + - Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical + - The gateway must be able to reach the relay server (outbound connection only) + - Replace `` with the name of your relay server and `` with a unique name for this gateway + diff --git a/docs/documentation/platform/identities/ldap-auth/general.mdx b/docs/documentation/platform/identities/ldap-auth/general.mdx index 01395b68c..f49643474 100644 --- a/docs/documentation/platform/identities/ldap-auth/general.mdx +++ b/docs/documentation/platform/identities/ldap-auth/general.mdx @@ -34,24 +34,33 @@ To create and manage LDAP auth templates, see our [Machine Identity Auth Templat To configure LDAP auth for your identity, press the **Add Auth Method** button on the identity's page. ![Add auth method](/images/platform/identities/ldap/identities-org-add-auth-method.png) - + Now select **LDAP Auth** from the list of available auth methods for the identity. ![Select LDAP auth](/images/platform/identities/ldap/identities-org-add-auth-method-modal.png) - - + + After selecting **LDAP Auth**, you'll see the form you need to fill out to configure LDAP auth for your identity. The following fields are available: + **Configuration Tab** - `URL`: The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` _(for connection over SSL/TLS)_, etc. - `Bind DN`: The DN to bind to the LDAP server with. - `Bind Pass`: The password to bind to the LDAP server with. - `Search Base / DN`: Base DN under which to perform user search such as `ou=Users,dc=acme,dc=com`. - `User Search Filter`: Template used to construct the LDAP user search filter such as `(uid={{username}})`; use literal `{{username}}` to have the given username used in the search. The default is `(uid={{username}})` which is compatible with several common directory schemas. - `Required Attributes`: A key/value pair of attributes that must be present in the LDAP user entry for them to be authenticated. As an example, if you set key `uid` to value `user1,user2,user3`, then only users with `uid` of `user1`, `user2`, or `user3` will be able to login with this identity. Each value is a comma separated list of attributes. - - `CA Certificate`: The CA certificate to use when verifying the LDAP server certificate. This field is optional but recommended. - `Access Token TTL` _(default is 2592000 equivalent to 30 days)_: The lifetime for an access token in seconds. This value will be referenced at renewal time. - `Access Token Max TTL` _(default is 2592000 equivalent to 30 days)_: The maximum lifetime for an access token in seconds. This value will be referenced at renewal time. - `Access Token Max Number of Uses` _(default is 0)_: The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses. + + **Lockout Tab** + - `Lockout` _(enabled by default)_: The lockout feature will temporarily block login attempts after X consecutive login failures. + - `Lockout Threshold` _(default is 3)_: The amount of times login must fail before locking the identity auth method. + - `Lockout Duration` _(default is 5 minutes)_: How long an identity auth method lockout lasts. + - `Lockout Counter Reset` _(default is 30 seconds)_: How long to wait from the most recent failed login until resetting the lockout counter. + + **Advanced Tab** + - `CA Certificate`: The CA certificate to use when verifying the LDAP server certificate. This field is optional but recommended. - `Access Token Trusted IPs`: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address. Once you've filled out the form, press **Add** to save your changes. @@ -91,3 +100,13 @@ To create and manage LDAP auth templates, see our [Machine Identity Auth Templat + +**FAQ** + + + + You can reset (remove) all lockouts for an identity auth method by clicking into the auth method and pressing **Reset All Lockouts**. + + ![ldap reset lockouts](/images/platform/identities/ldap-reset-lockouts.png) + + diff --git a/docs/documentation/platform/identities/machine-identities.mdx b/docs/documentation/platform/identities/machine-identities.mdx index b4a18708c..dd5cbbbcf 100644 --- a/docs/documentation/platform/identities/machine-identities.mdx +++ b/docs/documentation/platform/identities/machine-identities.mdx @@ -38,6 +38,16 @@ To interact with various resources in Infisical, Machine Identities can authenti - [GCP Auth](/documentation/platform/identities/gcp-auth): A GCP-native authentication method for GCP resources (e.g. Compute Engine, App Engine, Cloud Run, Google Kubernetes Engine, IAM service accounts, etc.). - [OIDC Auth](/documentation/platform/identities/oidc-auth): A platform-agnostic, JWT-based authentication method for workloads using an OpenID Connect identity provider. +## Identity Lockout + +Lockout is a feature that prevents brute-force attacks on identity login endpoints. Auth methods that support lockout include: [Universal Auth](/documentation/platform/identities/universal-auth). + +Supported auth methods have lockout enabled by default. If triggered, lockout temporarily disables the login endpoint for 5 minutes after 3 consecutive failed login attempts within a 30-second window. Lockout can be configured and disabled in the identity auth method settings. + + + When Lockout is enabled, a rate limit of approximately 10 requests per second is enforced on relevant authentication endpoints. This security measure employs a protective lock to mitigate parallel login attacks. If this rate limitation interferes with your operational requirements, you may consider disabling Lockout. + + ## FAQ @@ -51,15 +61,15 @@ You can learn more about how to do this in the CLI quickstart [here](/cli/usage) A service token is a project-level authentication method that is being deprecated in favor of identities. The service token method will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys). - + Amongst many differences, identities provide broader access over the Infisical API, utilizes the same permission system as user identities, and come with a significantly larger number of configurable authentication and security features. - + If you're looking for a simple authentication method, similar to service tokens, that can be bound onto an identity, we recommend checking out [Token Auth](/documentation/platform/identities/token-auth). There are a few reasons for why this might happen: - + - You have insufficient organization permissions to create, read, update, delete identities. - The identity you are trying to read, update, or delete is more privileged than yourself. - The role you are trying to create an identity for or update an identity to is more privileged than yours. diff --git a/docs/documentation/platform/identities/universal-auth.mdx b/docs/documentation/platform/identities/universal-auth.mdx index 51721f7bf..3a87f6da9 100644 --- a/docs/documentation/platform/identities/universal-auth.mdx +++ b/docs/documentation/platform/identities/universal-auth.mdx @@ -4,7 +4,7 @@ description: "Learn how to authenticate to Infisical from any platform or enviro --- **Universal Auth** is a platform-agnostic authentication method that can be configured for a [machine identity](/documentation/platform/identities/machine-identities) to authenticate from any platform/environment using a Client ID and Client Secret. -This authentication method supports setting token periods, which can help [overcome secret zero](#solving-secret-zero-with-periodic-tokens). +This authentication method supports setting token periods, which can help [overcome secret zero](#solving-secret-zero-with-periodic-tokens). ## Diagram @@ -65,17 +65,32 @@ using the Universal Auth authentication method. By default, the identity has been configured with Universal Auth. If you wish, you can edit the Universal Auth configuration details by pressing to edit the **Authentication** section. + Here's some guidance on each field: + + **Configuration Tab** + ![identities organization create universal auth method 1](/images/platform/identities/identities-org-create-universal-auth-method-1.png) + + - Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an access token in seconds. This value will be referenced at renewal time. + - Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an access token in seconds. This value will be referenced at renewal time. + - Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses. + - Access Token Period (optional, default is `0`): If set, the access token becomes a renewable, non-expiring token for the specified period (in seconds). TTL and Max TTL are ignored when this is set. This is ideal for "secret zero" scenarios, where a workload needs to bootstrap itself securely without hard-coded static secrets. + + **Lockout Tab** + ![identities organization create universal auth method 2](/images/platform/identities/identities-org-create-universal-auth-method-2.png) - Here's some more guidance on each field: + - Lockout (enabled by default): The lockout feature will temporarily block login attempts after X consecutive login failures. + - Lockout Threshold (default is `3`): The amount of times login must fail before locking the identity auth method. + - Lockout Duration (default is `5 minutes`): How long an identity auth method lockout lasts. + - Lockout Counter Reset (default is `30 seconds`): How long to wait from the most recent failed login until resetting the lockout counter. + + **Advanced Tab** + + ![identities organization create universal auth method 3](/images/platform/identities/identities-org-create-universal-auth-method-3.png) - - Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an acccess token in seconds. This value will be referenced at renewal time. - - Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time. - - Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses. - Client Secret Trusted IPs: The IPs or CIDR ranges that the **Client Secret** can be used from together with the **Client ID** to get back an access token. By default, **Client Secrets** are given the `0.0.0.0/0`, allowing usage from any network address. - Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address. - - Access Token Period (optional, default is `0`): If set, the access token becomes a renewable, non-expiring token for the specified period (in seconds). TTL and Max TTL are ignored when this is set. This is ideal for "secret zero" scenarios, where a workload needs to bootstrap itself securely without hard-coded static secrets. Restricting **Client Secret** and access token usage to specific trusted IPs is a paid feature. @@ -202,6 +217,10 @@ This approach allows your workload to securely bootstrap and maintain access to A token can be renewed any number of times where each call to renew it can extend the token's lifetime by increments of the access token's TTL. Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation. + + + You can reset (remove) all lockouts for an identity auth method by clicking into the auth method and pressing **Reset All Lockouts**. + ![ua reset lockouts](/images/platform/identities/ua-reset-lockouts.png) diff --git a/docs/documentation/platform/secret-versioning.mdx b/docs/documentation/platform/secret-versioning.mdx index 6a0efb8b8..7ff8694f0 100644 --- a/docs/documentation/platform/secret-versioning.mdx +++ b/docs/documentation/platform/secret-versioning.mdx @@ -8,6 +8,7 @@ Every time a secret change is performed, a new version of the same secret is cre Such versions can be accessed visually by opening up the [secret sidebar](/documentation/platform/project#drawer) (as seen below) or [retrieved via API](/api-reference/endpoints/secrets/read) by specifying the `version` query parameter. +![secret versioning overview](../../images/platform/secret-versioning-overview.png) ![secret versioning](../../images/platform/secret-versioning.png) The secret versioning functionality is heavily connected to [Point-in-time Recovery](/documentation/platform/pit-recovery) of secrets in Infisical. diff --git a/docs/images/app-connections/general/add-connection.png b/docs/images/app-connections/general/add-connection.png index ad9d54716..b6dce69ac 100644 Binary files a/docs/images/app-connections/general/add-connection.png and b/docs/images/app-connections/general/add-connection.png differ diff --git a/docs/images/auth-methods/access-personal-settings.png b/docs/images/auth-methods/access-personal-settings.png index a5e1989c1..31f6f96c7 100644 Binary files a/docs/images/auth-methods/access-personal-settings.png and b/docs/images/auth-methods/access-personal-settings.png differ diff --git a/docs/images/auth-methods/personal-settings-authentication-change-email-confirmation.png b/docs/images/auth-methods/personal-settings-authentication-change-email-confirmation.png new file mode 100644 index 000000000..1a5986c3c Binary files /dev/null and b/docs/images/auth-methods/personal-settings-authentication-change-email-confirmation.png differ diff --git a/docs/images/auth-methods/personal-settings-authentication-change-email-password.png b/docs/images/auth-methods/personal-settings-authentication-change-email-password.png new file mode 100644 index 000000000..78945e286 Binary files /dev/null and b/docs/images/auth-methods/personal-settings-authentication-change-email-password.png differ diff --git a/docs/images/auth-methods/personal-settings-authentication-tab.png b/docs/images/auth-methods/personal-settings-authentication-tab.png new file mode 100644 index 000000000..5429e97cd Binary files /dev/null and b/docs/images/auth-methods/personal-settings-authentication-tab.png differ diff --git a/docs/images/platform/audit-log-streams/azure-configure-dce.png b/docs/images/platform/audit-log-streams/azure-configure-dce.png new file mode 100644 index 000000000..b0b32f0b2 Binary files /dev/null and b/docs/images/platform/audit-log-streams/azure-configure-dce.png differ diff --git a/docs/images/platform/audit-log-streams/azure-configure-law.png b/docs/images/platform/audit-log-streams/azure-configure-law.png new file mode 100644 index 000000000..11ec8d86f Binary files /dev/null and b/docs/images/platform/audit-log-streams/azure-configure-law.png differ diff --git a/docs/images/platform/audit-log-streams/azure-configure-table.png b/docs/images/platform/audit-log-streams/azure-configure-table.png new file mode 100644 index 000000000..7dba237b9 Binary files /dev/null and b/docs/images/platform/audit-log-streams/azure-configure-table.png differ diff --git a/docs/images/platform/audit-log-streams/azure-create-als.png b/docs/images/platform/audit-log-streams/azure-create-als.png new file mode 100644 index 000000000..bdfb2f8d6 Binary files /dev/null and b/docs/images/platform/audit-log-streams/azure-create-als.png differ diff --git a/docs/images/platform/audit-log-streams/azure-create-dce.png b/docs/images/platform/audit-log-streams/azure-create-dce.png new file mode 100644 index 000000000..00a546c30 Binary files /dev/null and b/docs/images/platform/audit-log-streams/azure-create-dce.png differ diff --git a/docs/images/platform/audit-log-streams/azure-create-law.png b/docs/images/platform/audit-log-streams/azure-create-law.png new file mode 100644 index 000000000..1150f64ff Binary files /dev/null and b/docs/images/platform/audit-log-streams/azure-create-law.png differ diff --git a/docs/images/platform/audit-log-streams/azure-dce-url.png b/docs/images/platform/audit-log-streams/azure-dce-url.png new file mode 100644 index 000000000..d8c1dc96d Binary files /dev/null and b/docs/images/platform/audit-log-streams/azure-dce-url.png differ diff --git a/docs/images/platform/audit-log-streams/azure-dcr.png b/docs/images/platform/audit-log-streams/azure-dcr.png new file mode 100644 index 000000000..441808947 Binary files /dev/null and b/docs/images/platform/audit-log-streams/azure-dcr.png differ diff --git a/docs/images/platform/audit-log-streams/azure-go-to-resource.png b/docs/images/platform/audit-log-streams/azure-go-to-resource.png new file mode 100644 index 000000000..ef197517d Binary files /dev/null and b/docs/images/platform/audit-log-streams/azure-go-to-resource.png differ diff --git a/docs/images/platform/audit-log-streams/azure-new-table.png b/docs/images/platform/audit-log-streams/azure-new-table.png new file mode 100644 index 000000000..829af3e53 Binary files /dev/null and b/docs/images/platform/audit-log-streams/azure-new-table.png differ diff --git a/docs/images/platform/audit-log-streams/cribl-add-source.png b/docs/images/platform/audit-log-streams/cribl-add-source.png new file mode 100644 index 000000000..95546a456 Binary files /dev/null and b/docs/images/platform/audit-log-streams/cribl-add-source.png differ diff --git a/docs/images/platform/audit-log-streams/cribl-details.png b/docs/images/platform/audit-log-streams/cribl-details.png new file mode 100644 index 000000000..4aa3cfdf5 Binary files /dev/null and b/docs/images/platform/audit-log-streams/cribl-details.png differ diff --git a/docs/images/platform/audit-log-streams/cribl-general-settings.png b/docs/images/platform/audit-log-streams/cribl-general-settings.png new file mode 100644 index 000000000..8f3d20c0c Binary files /dev/null and b/docs/images/platform/audit-log-streams/cribl-general-settings.png differ diff --git a/docs/images/platform/audit-log-streams/cribl-ingress-address.png b/docs/images/platform/audit-log-streams/cribl-ingress-address.png new file mode 100644 index 000000000..cb1842d67 Binary files /dev/null and b/docs/images/platform/audit-log-streams/cribl-ingress-address.png differ diff --git a/docs/images/platform/audit-log-streams/custom-provider.png b/docs/images/platform/audit-log-streams/custom-provider.png new file mode 100644 index 000000000..e860e2e90 Binary files /dev/null and b/docs/images/platform/audit-log-streams/custom-provider.png differ diff --git a/docs/images/platform/audit-log-streams/datadog-details.png b/docs/images/platform/audit-log-streams/datadog-details.png new file mode 100644 index 000000000..29b1464be Binary files /dev/null and b/docs/images/platform/audit-log-streams/datadog-details.png differ diff --git a/docs/images/platform/audit-log-streams/datadog-logging-endpoint.png b/docs/images/platform/audit-log-streams/datadog-logging-endpoint.png deleted file mode 100644 index 7960b1145..000000000 Binary files a/docs/images/platform/audit-log-streams/datadog-logging-endpoint.png and /dev/null differ diff --git a/docs/images/platform/audit-log-streams/datadog-source-details.png b/docs/images/platform/audit-log-streams/datadog-source-details.png deleted file mode 100644 index 5ae25b0b3..000000000 Binary files a/docs/images/platform/audit-log-streams/datadog-source-details.png and /dev/null differ diff --git a/docs/images/platform/audit-log-streams/select-custom.png b/docs/images/platform/audit-log-streams/select-custom.png new file mode 100644 index 000000000..7a55b24a7 Binary files /dev/null and b/docs/images/platform/audit-log-streams/select-custom.png differ diff --git a/docs/images/platform/audit-log-streams/select-provider.png b/docs/images/platform/audit-log-streams/select-provider.png new file mode 100644 index 000000000..c289fd3af Binary files /dev/null and b/docs/images/platform/audit-log-streams/select-provider.png differ diff --git a/docs/images/platform/audit-log-streams/splunk-credentials.png b/docs/images/platform/audit-log-streams/splunk-credentials.png new file mode 100644 index 000000000..8b8624700 Binary files /dev/null and b/docs/images/platform/audit-log-streams/splunk-credentials.png differ diff --git a/docs/images/platform/audit-log-streams/splunk-data-inputs.png b/docs/images/platform/audit-log-streams/splunk-data-inputs.png new file mode 100644 index 000000000..3446f89f3 Binary files /dev/null and b/docs/images/platform/audit-log-streams/splunk-data-inputs.png differ diff --git a/docs/images/platform/audit-log-streams/splunk-details.png b/docs/images/platform/audit-log-streams/splunk-details.png new file mode 100644 index 000000000..b7ece41fc Binary files /dev/null and b/docs/images/platform/audit-log-streams/splunk-details.png differ diff --git a/docs/images/platform/audit-log-streams/splunk-http-collector.png b/docs/images/platform/audit-log-streams/splunk-http-collector.png new file mode 100644 index 000000000..d8090095c Binary files /dev/null and b/docs/images/platform/audit-log-streams/splunk-http-collector.png differ diff --git a/docs/images/platform/audit-log-streams/splunk-name.png b/docs/images/platform/audit-log-streams/splunk-name.png new file mode 100644 index 000000000..2c382539e Binary files /dev/null and b/docs/images/platform/audit-log-streams/splunk-name.png differ diff --git a/docs/images/platform/audit-log-streams/splunk-new-token.png b/docs/images/platform/audit-log-streams/splunk-new-token.png new file mode 100644 index 000000000..d4c53569d Binary files /dev/null and b/docs/images/platform/audit-log-streams/splunk-new-token.png differ diff --git a/docs/images/platform/audit-log-streams/stream-create.png b/docs/images/platform/audit-log-streams/stream-create.png index 949278e3d..1244fa558 100644 Binary files a/docs/images/platform/audit-log-streams/stream-create.png and b/docs/images/platform/audit-log-streams/stream-create.png differ diff --git a/docs/images/platform/audit-log-streams/stream-inputs.png b/docs/images/platform/audit-log-streams/stream-inputs.png deleted file mode 100644 index 6b9d7c57b..000000000 Binary files a/docs/images/platform/audit-log-streams/stream-inputs.png and /dev/null differ diff --git a/docs/images/platform/audit-log-streams/stream-list.png b/docs/images/platform/audit-log-streams/stream-list.png index c5cc5598b..ad355c3b0 100644 Binary files a/docs/images/platform/audit-log-streams/stream-list.png and b/docs/images/platform/audit-log-streams/stream-list.png differ diff --git a/docs/images/platform/gateways/gateway-highlevel-diagram.png b/docs/images/platform/gateways/gateway-highlevel-diagram.png new file mode 100644 index 000000000..5f942bcf0 Binary files /dev/null and b/docs/images/platform/gateways/gateway-highlevel-diagram.png differ diff --git a/docs/images/platform/identities/identities-org-create-universal-auth-method-1.png b/docs/images/platform/identities/identities-org-create-universal-auth-method-1.png index d3fe6fbe0..eaef0c10c 100644 Binary files a/docs/images/platform/identities/identities-org-create-universal-auth-method-1.png and b/docs/images/platform/identities/identities-org-create-universal-auth-method-1.png differ diff --git a/docs/images/platform/identities/identities-org-create-universal-auth-method-2.png b/docs/images/platform/identities/identities-org-create-universal-auth-method-2.png index ea0fc9671..9aae59d04 100644 Binary files a/docs/images/platform/identities/identities-org-create-universal-auth-method-2.png and b/docs/images/platform/identities/identities-org-create-universal-auth-method-2.png differ diff --git a/docs/images/platform/identities/identities-org-create-universal-auth-method-3.png b/docs/images/platform/identities/identities-org-create-universal-auth-method-3.png new file mode 100644 index 000000000..c966afdb4 Binary files /dev/null and b/docs/images/platform/identities/identities-org-create-universal-auth-method-3.png differ diff --git a/docs/images/platform/identities/ldap-reset-lockouts.png b/docs/images/platform/identities/ldap-reset-lockouts.png new file mode 100644 index 000000000..c8aae2eff Binary files /dev/null and b/docs/images/platform/identities/ldap-reset-lockouts.png differ diff --git a/docs/images/platform/identities/ua-reset-lockouts.png b/docs/images/platform/identities/ua-reset-lockouts.png new file mode 100644 index 000000000..40993da1a Binary files /dev/null and b/docs/images/platform/identities/ua-reset-lockouts.png differ diff --git a/docs/images/platform/secret-versioning-overview.png b/docs/images/platform/secret-versioning-overview.png new file mode 100644 index 000000000..94a50bd49 Binary files /dev/null and b/docs/images/platform/secret-versioning-overview.png differ diff --git a/docs/images/platform/secret-versioning.png b/docs/images/platform/secret-versioning.png index 593e8c96f..60b57cad9 100644 Binary files a/docs/images/platform/secret-versioning.png and b/docs/images/platform/secret-versioning.png differ diff --git a/docs/images/sdks/languages/php.svg b/docs/images/sdks/languages/php.svg new file mode 100644 index 000000000..37a5e6fe7 --- /dev/null +++ b/docs/images/sdks/languages/php.svg @@ -0,0 +1,96 @@ + + + Official PHP Logo + + + + image/svg+xml + + Official PHP Logo + + + Colin Viebrock + + + + + + + + + + + + Copyright Colin Viebrock 1997 - All rights reserved. + + + 1997 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/integrations/app-connections/1password.mdx b/docs/integrations/app-connections/1password.mdx index 394d8bc23..bcd7e8ff7 100644 --- a/docs/integrations/app-connections/1password.mdx +++ b/docs/integrations/app-connections/1password.mdx @@ -53,7 +53,7 @@ Infisical supports the use of [Service Accounts](https://developer.1password.com - In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -72,7 +72,7 @@ Infisical supports the use of [Service Accounts](https://developer.1password.com ![1Password Connection Modal](/images/app-connections/1password/app-connection-modal.png) - After clicking Create, your **1Password Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **1Password Connection** is established and ready to use with your Infisical project. ![1Password Connection Created](/images/app-connections/1password/app-connection-created.png) @@ -90,6 +90,7 @@ Infisical supports the use of [Service Accounts](https://developer.1password.com --data '{ "name": "my-1password-connection", "method": "api-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "instanceUrl": "https://1pass.example.com", "apiToken": "" @@ -104,6 +105,7 @@ Infisical supports the use of [Service Accounts](https://developer.1password.com "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-1password-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/auth0.mdx b/docs/integrations/app-connections/auth0.mdx index 42e78cb66..91c5f5e60 100644 --- a/docs/integrations/app-connections/auth0.mdx +++ b/docs/integrations/app-connections/auth0.mdx @@ -42,7 +42,7 @@ Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-st - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **Auth0 Connection** option. @@ -67,6 +67,7 @@ Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-st --data '{ "name": "my-auth0-connection", "method": "client-credentials", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "domain": "xxx-xxxxxxxxx.us.auth0.com", "clientId": "...", @@ -83,6 +84,7 @@ Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-st "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-auth0-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/aws.mdx b/docs/integrations/app-connections/aws.mdx index 195f98247..08ae05be7 100644 --- a/docs/integrations/app-connections/aws.mdx +++ b/docs/integrations/app-connections/aws.mdx @@ -184,7 +184,7 @@ Infisical supports two methods for connecting to AWS. - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **AWS Connection** option. @@ -209,6 +209,7 @@ Infisical supports two methods for connecting to AWS. --data '{ "name": "my-aws-connection", "method": "assume-role", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "roleArn": "...", } @@ -222,6 +223,7 @@ Infisical supports two methods for connecting to AWS. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-aws-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 123, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", @@ -361,7 +363,7 @@ Infisical supports two methods for connecting to AWS. - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **AWS Connection** option. @@ -386,6 +388,7 @@ Infisical supports two methods for connecting to AWS. --data '{ "name": "my-aws-connection", "method": "access-key", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "accessKeyId": "...", "secretKey": "..." @@ -400,6 +403,7 @@ Infisical supports two methods for connecting to AWS. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-aws-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 123, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/azure-app-configuration.mdx b/docs/integrations/app-connections/azure-app-configuration.mdx index 4efd19f30..cd9c707be 100644 --- a/docs/integrations/app-connections/azure-app-configuration.mdx +++ b/docs/integrations/app-connections/azure-app-configuration.mdx @@ -83,7 +83,7 @@ Infisical currently only supports two methods for connecting to Azure, which are - Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/azure-client-secrets.mdx b/docs/integrations/app-connections/azure-client-secrets.mdx index 37e7d49b0..cb25fb596 100644 --- a/docs/integrations/app-connections/azure-client-secrets.mdx +++ b/docs/integrations/app-connections/azure-client-secrets.mdx @@ -94,7 +94,7 @@ Infisical currently only supports two methods for connecting to Azure, which are - Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/azure-devops.mdx b/docs/integrations/app-connections/azure-devops.mdx index 4744a35c6..7eabff84b 100644 --- a/docs/integrations/app-connections/azure-devops.mdx +++ b/docs/integrations/app-connections/azure-devops.mdx @@ -117,7 +117,7 @@ Infisical currently supports three methods for connecting to Azure DevOps, which - Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/azure-key-vault.mdx b/docs/integrations/app-connections/azure-key-vault.mdx index 866a1de82..b2989efae 100644 --- a/docs/integrations/app-connections/azure-key-vault.mdx +++ b/docs/integrations/app-connections/azure-key-vault.mdx @@ -83,7 +83,7 @@ Infisical currently only supports two methods for connecting to Azure, which are - Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/bitbucket.mdx b/docs/integrations/app-connections/bitbucket.mdx index be4fdbee7..3b3385202 100644 --- a/docs/integrations/app-connections/bitbucket.mdx +++ b/docs/integrations/app-connections/bitbucket.mdx @@ -78,7 +78,7 @@ Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbuck - In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -95,7 +95,7 @@ Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbuck ![Bitbucket Connection Modal](/images/app-connections/bitbucket/step-6.png) - After clicking Create, your **Bitbucket Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **Bitbucket Connection** is established and ready to use with your Infisical project. ![Bitbucket Connection Created](/images/app-connections/bitbucket/step-7.png) @@ -113,6 +113,7 @@ Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbuck --data '{ "name": "my-bitbucket-connection", "method": "api-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "email": "user@example.com", "apiToken": "" @@ -127,6 +128,7 @@ Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbuck "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-bitbucket-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/camunda.mdx b/docs/integrations/app-connections/camunda.mdx index 68084cea3..7ad0f8ef9 100644 --- a/docs/integrations/app-connections/camunda.mdx +++ b/docs/integrations/app-connections/camunda.mdx @@ -50,8 +50,7 @@ Infisical supports connecting to Camunda APIs using [client credentials](https:/ - Navigate to the **App Connections** tab on the **Organization Settings** - page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/checkly.mdx b/docs/integrations/app-connections/checkly.mdx index 38234744d..943a470c2 100644 --- a/docs/integrations/app-connections/checkly.mdx +++ b/docs/integrations/app-connections/checkly.mdx @@ -37,7 +37,7 @@ Infisical supports the use of [API Keys](https://app.checklyhq.com/settings/user - In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -55,7 +55,7 @@ Infisical supports the use of [API Keys](https://app.checklyhq.com/settings/user ![Checkly Connection Modal](/images/app-connections/checkly/checkly-app-connection-form.png) - After submitting the form, your **Checkly Connection** will be successfully created and ready to use with your Infisical projects. + After submitting the form, your **Checkly Connection** will be successfully created and ready to use with your Infisical project. ![Checkly Connection Created](/images/app-connections/checkly/checkly-app-connection-generated.png) @@ -75,6 +75,7 @@ Infisical supports the use of [API Keys](https://app.checklyhq.com/settings/user --data '{ "name": "my-checkly-connection", "method": "api-key", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "apiKey": "[API KEY]" } @@ -88,6 +89,7 @@ Infisical supports the use of [API Keys](https://app.checklyhq.com/settings/user "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-checkly-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/cloudflare.mdx b/docs/integrations/app-connections/cloudflare.mdx index 241c737bc..33a9a992c 100644 --- a/docs/integrations/app-connections/cloudflare.mdx +++ b/docs/integrations/app-connections/cloudflare.mdx @@ -88,8 +88,7 @@ Infisical supports connecting to Cloudflare using API tokens and Account ID for - Navigate to the **App Connections** tab on the **Organization Settings** - page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/databricks.mdx b/docs/integrations/app-connections/databricks.mdx index 38d125ea6..e0b861184 100644 --- a/docs/integrations/app-connections/databricks.mdx +++ b/docs/integrations/app-connections/databricks.mdx @@ -43,8 +43,7 @@ Infisical supports the use of [service principals](https://docs.databricks.com/e - Navigate to the **App Connections** tab on the **Organization Settings** - page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/digital-ocean.mdx b/docs/integrations/app-connections/digital-ocean.mdx index 5b047f017..ff2eccfc1 100644 --- a/docs/integrations/app-connections/digital-ocean.mdx +++ b/docs/integrations/app-connections/digital-ocean.mdx @@ -45,7 +45,7 @@ Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/accoun - In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -63,7 +63,7 @@ Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/accoun ![DigitalOcean Connection Modal](/images/app-connections/digital-ocean/app-connection-form.png) - After submitting the form, your **DigitalOcean Connection** will be successfully created and ready to use with your Infisical projects. + After submitting the form, your **DigitalOcean Connection** will be successfully created and ready to use with your Infisical project. ![DigitalOcean Connection Created](/images/app-connections/digital-ocean/app-connection-generated.png) @@ -82,6 +82,7 @@ Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/accoun --data '{ "name": "my-digitalocean-connection", "method": "api-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "apiToken": "[API TOKEN]" } @@ -95,6 +96,7 @@ Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/accoun "appConnection": { "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "name": "my-digitalocean-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "abcdef12-3456-7890-abcd-ef1234567890", diff --git a/docs/integrations/app-connections/flyio.mdx b/docs/integrations/app-connections/flyio.mdx index e42756254..bf36ccc9b 100644 --- a/docs/integrations/app-connections/flyio.mdx +++ b/docs/integrations/app-connections/flyio.mdx @@ -30,7 +30,7 @@ Infisical supports the use of [Access Tokens](https://fly.io/docs/security/token - In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -48,7 +48,7 @@ Infisical supports the use of [Access Tokens](https://fly.io/docs/security/token ![Fly.io Connection Modal](/images/app-connections/flyio/app-connection-modal.png) - After clicking Create, your **Fly.io Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **Fly.io Connection** is established and ready to use with your Infisical project. ![Fly.io Connection Created](/images/app-connections/flyio/app-connection-created.png) @@ -66,6 +66,7 @@ Infisical supports the use of [Access Tokens](https://fly.io/docs/security/token --data '{ "name": "my-flyio-connection", "method": "access-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "accessToken": "[PRIVATE TOKEN]" } @@ -79,6 +80,7 @@ Infisical supports the use of [Access Tokens](https://fly.io/docs/security/token "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-flyio-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/gcp.mdx b/docs/integrations/app-connections/gcp.mdx index 129c26c2a..9458339af 100644 --- a/docs/integrations/app-connections/gcp.mdx +++ b/docs/integrations/app-connections/gcp.mdx @@ -82,8 +82,7 @@ Infisical supports [service account impersonation](https://cloud.google.com/iam/ - Navigate to the **App Connections** tab on the **Organization Settings** - page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/github-radar.mdx b/docs/integrations/app-connections/github-radar.mdx index 376973efd..491070a8b 100644 --- a/docs/integrations/app-connections/github-radar.mdx +++ b/docs/integrations/app-connections/github-radar.mdx @@ -97,7 +97,7 @@ Infisical supports GitHub App installation for creating a GitHub Radar Connectio - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/github.mdx b/docs/integrations/app-connections/github.mdx index 9a952f815..e44fc405a 100644 --- a/docs/integrations/app-connections/github.mdx +++ b/docs/integrations/app-connections/github.mdx @@ -85,7 +85,7 @@ Infisical supports two methods for connecting to GitHub. - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -156,7 +156,7 @@ Infisical supports two methods for connecting to GitHub. - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/gitlab.mdx b/docs/integrations/app-connections/gitlab.mdx index 60e9236ca..4f7223d93 100644 --- a/docs/integrations/app-connections/gitlab.mdx +++ b/docs/integrations/app-connections/gitlab.mdx @@ -70,7 +70,7 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -193,7 +193,7 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/hashicorp-vault.mdx b/docs/integrations/app-connections/hashicorp-vault.mdx index c49b53ab8..7d5502fc5 100644 --- a/docs/integrations/app-connections/hashicorp-vault.mdx +++ b/docs/integrations/app-connections/hashicorp-vault.mdx @@ -131,7 +131,7 @@ Infisical supports two methods for connecting to Hashicorp Vault. - In your Infisical dashboard, go to **Organization Settings** and select the **App Connections** tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -184,6 +184,7 @@ Infisical supports two methods for connecting to Hashicorp Vault. --data '{ "name": "my-vault-connection", "method": "app-role", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "instanceUrl": "https://vault.example.com", "roleId": "4797c4fa-7794-71f0-c8b1-7c87759df5bf", @@ -199,6 +200,7 @@ Infisical supports two methods for connecting to Hashicorp Vault. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-vault-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2025-04-01T05:31:56Z", diff --git a/docs/integrations/app-connections/heroku.mdx b/docs/integrations/app-connections/heroku.mdx index fc2d1fbcc..9c3397e02 100644 --- a/docs/integrations/app-connections/heroku.mdx +++ b/docs/integrations/app-connections/heroku.mdx @@ -51,7 +51,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -93,7 +93,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To ![Heroku API Token](/images/app-connections/heroku/heroku-api-token.png) - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/humanitec.mdx b/docs/integrations/app-connections/humanitec.mdx index 570d3ba5d..669798eeb 100644 --- a/docs/integrations/app-connections/humanitec.mdx +++ b/docs/integrations/app-connections/humanitec.mdx @@ -53,7 +53,7 @@ Infisical supports connecting to Humanitec using a service user. ![Humanitec Connection Created](/images/app-connections/humanitec/humanitec-user-added.png) - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/ldap.mdx b/docs/integrations/app-connections/ldap.mdx index db0b596ce..f0afa1157 100644 --- a/docs/integrations/app-connections/ldap.mdx +++ b/docs/integrations/app-connections/ldap.mdx @@ -33,7 +33,7 @@ Depending on how you intend to use your LDAP connection, there may be additional - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **LDAP Connection** option. @@ -58,6 +58,7 @@ Depending on how you intend to use your LDAP connection, there may be additional --data '{ "name": "my-ldap-connection", "method": "simple-bind", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "provider": "active-directory", "url": "ldaps://domain-or-ip:636", @@ -76,6 +77,7 @@ Depending on how you intend to use your LDAP connection, there may be additional "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-ldap-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/mssql.mdx b/docs/integrations/app-connections/mssql.mdx index 7e940804d..77e8e676d 100644 --- a/docs/integrations/app-connections/mssql.mdx +++ b/docs/integrations/app-connections/mssql.mdx @@ -62,7 +62,7 @@ Infisical supports connecting to Microsoft SQL Server using database principals. - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **Microsoft SQL Server Connection** option. @@ -96,6 +96,7 @@ Infisical supports connecting to Microsoft SQL Server using database principals. --data '{ "name": "my-mssql-connection", "method": "username-and-password", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "isPlatformManagedCredentials": true, "credentials": { "host": "123.4.5.6", @@ -115,7 +116,8 @@ Infisical supports connecting to Microsoft SQL Server using database principals. { "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", - "name": "my-pg-connection", + "name": "my-mssql-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/mysql.mdx b/docs/integrations/app-connections/mysql.mdx index 38a8a4e97..6055d77cc 100644 --- a/docs/integrations/app-connections/mysql.mdx +++ b/docs/integrations/app-connections/mysql.mdx @@ -52,7 +52,7 @@ Infisical supports connecting to MySQL using a database role. - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **MySQL Connection** option. @@ -88,6 +88,7 @@ Infisical supports connecting to MySQL using a database role. "name": "my-mysql-connection", "method": "username-and-password", "isPlatformManagedCredentials": true, + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "host": "123.4.5.6", "port": 3306, @@ -107,6 +108,7 @@ Infisical supports connecting to MySQL using a database role. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-mysql-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/netlify.mdx b/docs/integrations/app-connections/netlify.mdx index cd4dfb1b5..d2a62113c 100644 --- a/docs/integrations/app-connections/netlify.mdx +++ b/docs/integrations/app-connections/netlify.mdx @@ -35,7 +35,7 @@ Infisical supports the use of [Personal Access Tokens](https://docs.netlify.com/ - In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -53,7 +53,7 @@ Infisical supports the use of [Personal Access Tokens](https://docs.netlify.com/ ![Netlify Connection Modal](/images/app-connections/netlify/app-connection-form.png) - After submitting the form, your **Netlify Connection** will be successfully created and ready to use with your Infisical projects. + After submitting the form, your **Netlify Connection** will be successfully created and ready to use with your Infisical project. ![Netlify Connection Created](/images/app-connections/netlify/app-connection-generated.png) @@ -72,6 +72,7 @@ Infisical supports the use of [Personal Access Tokens](https://docs.netlify.com/ --data '{ "name": "my-netlify-connection", "method": "access-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "accessToken": "[ACCESS TOKEN]" } @@ -86,6 +87,7 @@ Infisical supports the use of [Personal Access Tokens](https://docs.netlify.com/ "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "name": "my-netlify-connection", "description": null, + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "abcdef12-3456-7890-abcd-ef1234567890", "createdAt": "2025-07-19T10:15:00.000Z", diff --git a/docs/integrations/app-connections/oci.mdx b/docs/integrations/app-connections/oci.mdx index 58fb3c1d3..10d5fabaa 100644 --- a/docs/integrations/app-connections/oci.mdx +++ b/docs/integrations/app-connections/oci.mdx @@ -117,7 +117,7 @@ Infisical supports the use of [API Signing Key Authentication](https://docs.orac - In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -139,7 +139,7 @@ Infisical supports the use of [API Signing Key Authentication](https://docs.orac ![OCI Connection Modal](/images/app-connections/oci/app-connection-modal.png) - After clicking Create, your **OCI Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **OCI Connection** is established and ready to use with your Infisical project. ![OCI Connection Created](/images/app-connections/oci/app-connection-created.png) @@ -157,6 +157,7 @@ Infisical supports the use of [API Signing Key Authentication](https://docs.orac --data '{ "name": "my-oci-connection", "method": "access-key", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "userOcid": "ocid1.user.oc1..aaaaaaaagrp35tbkvvad4y2j7sug7xonua7dl2gfp4at2u5i5xj4ghnitg3a", "tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaotfma465m4zumfe2ua64mj2m5dwmlw2llh4g4dnfttnakiifonta", @@ -174,6 +175,7 @@ Infisical supports the use of [API Signing Key Authentication](https://docs.orac "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-oci-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/okta.mdx b/docs/integrations/app-connections/okta.mdx index 3c1295cf8..cb5edecbd 100644 --- a/docs/integrations/app-connections/okta.mdx +++ b/docs/integrations/app-connections/okta.mdx @@ -31,7 +31,7 @@ Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guide - In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -48,7 +48,7 @@ Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guide ![Connection Modal](/images/app-connections/okta/step-4.png) - After clicking Create, your **Okta Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **Okta Connection** is established and ready to use with your Infisical project. ![Connection Created](/images/app-connections/okta/step-5.png) @@ -66,6 +66,7 @@ Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guide --data '{ "name": "my-okta-connection", "method": "api-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "instanceUrl": "https://example.okta.com", "apiToken": "" @@ -80,6 +81,7 @@ Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guide "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-okta-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/oracledb.mdx b/docs/integrations/app-connections/oracledb.mdx index 8cab6371b..47aa33356 100644 --- a/docs/integrations/app-connections/oracledb.mdx +++ b/docs/integrations/app-connections/oracledb.mdx @@ -62,7 +62,7 @@ Infisical supports connecting to OracleDB using a database user. - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **OracleDB Connection** option. @@ -98,6 +98,7 @@ Infisical supports connecting to OracleDB using a database user. "name": "my-oracledb-connection", "method": "username-and-password", "isPlatformManagedCredentials": true, + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "host": "123.4.5.6", "port": 1521, @@ -117,6 +118,7 @@ Infisical supports connecting to OracleDB using a database user. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-oracledb-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/overview.mdx b/docs/integrations/app-connections/overview.mdx index e698c4302..8b1032e7d 100644 --- a/docs/integrations/app-connections/overview.mdx +++ b/docs/integrations/app-connections/overview.mdx @@ -3,12 +3,16 @@ sidebarTitle: "Overview" description: "Learn how to manage and configure third-party app connections with Infisical." --- -App Connections enable your organization to integrate Infisical with third-party services in a secure and versatile way. +App Connections enable you to integrate your Infisical projects with third-party services in a secure and versatile way. + + + App connections can also be created and managed independently in projects now. + ## Concept -App Connections are an organization-level resource used to establish connections with third-party applications -that can be used across Infisical projects. Example use cases include syncing secrets, generating dynamic secrets, and more. +App Connections can be used to establish connections with third-party applications +that can be used across multiple features. Example use cases include syncing secrets, rotating credentials, scanning repositories for secret leaks, and more.
diff --git a/docs/integrations/app-connections/postgres.mdx b/docs/integrations/app-connections/postgres.mdx index 239608905..dcb6c76d8 100644 --- a/docs/integrations/app-connections/postgres.mdx +++ b/docs/integrations/app-connections/postgres.mdx @@ -60,7 +60,7 @@ Infisical supports connecting to PostgreSQL using a database role. - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **PostgreSQL Connection** option. @@ -95,6 +95,7 @@ Infisical supports connecting to PostgreSQL using a database role. "name": "my-pg-connection", "method": "username-and-password", "isPlatformManagedCredentials": true, + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "host": "123.4.5.6", "port": 5432, @@ -114,6 +115,7 @@ Infisical supports connecting to PostgreSQL using a database role. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-pg-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/railway.mdx b/docs/integrations/app-connections/railway.mdx index 7b53d02ad..b88b3fbf1 100644 --- a/docs/integrations/app-connections/railway.mdx +++ b/docs/integrations/app-connections/railway.mdx @@ -96,7 +96,7 @@ Infisical supports the use of [API Tokens](https://docs.railway.com/guides/publi - In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -115,7 +115,7 @@ Infisical supports the use of [API Tokens](https://docs.railway.com/guides/publi ![Railway Connection Modal](/images/app-connections/railway/railway-app-connection-form.png)
- After submitting the form, your **Railway Connection** will be successfully created and ready to use with your Infisical projects. + After submitting the form, your **Railway Connection** will be successfully created and ready to use with your Infisical project. ![Railway Connection Created](/images/app-connections/railway/railway-app-connection-generated.png) @@ -134,6 +134,7 @@ Infisical supports the use of [API Tokens](https://docs.railway.com/guides/publi --data '{ "name": "my-railway-connection", "method": "team-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "apiToken": "[TEAM TOKEN]" } @@ -147,6 +148,7 @@ Infisical supports the use of [API Tokens](https://docs.railway.com/guides/publi "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-railway-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/render.mdx b/docs/integrations/app-connections/render.mdx index 580ec953e..78e050135 100644 --- a/docs/integrations/app-connections/render.mdx +++ b/docs/integrations/app-connections/render.mdx @@ -33,8 +33,7 @@ Infisical supports connecting to Render using API keys for secure access to your - Navigate to the **App Connections** tab on the **Organization Settings** - page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/supabase.mdx b/docs/integrations/app-connections/supabase.mdx index 9716b1526..80290cec9 100644 --- a/docs/integrations/app-connections/supabase.mdx +++ b/docs/integrations/app-connections/supabase.mdx @@ -34,7 +34,7 @@ Infisical supports the use of [Personal Access Tokens](https://supabase.com/dash - In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -53,7 +53,7 @@ Infisical supports the use of [Personal Access Tokens](https://supabase.com/dash ![Supabase Connection Modal](/images/app-connections/supabase/app-connection-form.png) - After submitting the form, your **Supabase Connection** will be successfully created and ready to use with your Infisical projects. + After submitting the form, your **Supabase Connection** will be successfully created and ready to use with your Infisical project. ![Supabase Connection Created](/images/app-connections/supabase/app-connection-generated.png) @@ -73,6 +73,7 @@ Infisical supports the use of [Personal Access Tokens](https://supabase.com/dash --data '{ "name": "my-supabase-connection", "method": "access-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "accessToken": "[Access Token]", "instanceUrl": "https://api.supabase.com" @@ -87,6 +88,7 @@ Infisical supports the use of [Personal Access Tokens](https://supabase.com/dash "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-supabase-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/teamcity.mdx b/docs/integrations/app-connections/teamcity.mdx index 889355954..311326f07 100644 --- a/docs/integrations/app-connections/teamcity.mdx +++ b/docs/integrations/app-connections/teamcity.mdx @@ -51,7 +51,7 @@ Infisical supports connecting to TeamCity using Access Tokens. 1. Navigate to App Connections - In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Add Connection @@ -68,7 +68,7 @@ Infisical supports connecting to TeamCity using Access Tokens. ![TeamCity Connection Modal](/images/app-connections/teamcity/teamcity-app-connection-modal.png) 4. Connection Created - After clicking Create, your **TeamCity Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **TeamCity Connection** is established and ready to use with your Infisical project. ![TeamCity Connection Created](/images/app-connections/teamcity/teamcity-app-connection-created.png) @@ -84,6 +84,7 @@ Infisical supports connecting to TeamCity using Access Tokens. --data '{ "name": "my-teamcity-connection", "method": "access-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "accessToken": "...", "instanceUrl": "https://yourcompany.teamcity.com" @@ -98,6 +99,7 @@ Infisical supports connecting to TeamCity using Access Tokens. "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-teamcity-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/terraform-cloud.mdx b/docs/integrations/app-connections/terraform-cloud.mdx index 02deb22cc..bc3da7810 100644 --- a/docs/integrations/app-connections/terraform-cloud.mdx +++ b/docs/integrations/app-connections/terraform-cloud.mdx @@ -30,7 +30,7 @@ Infisical supports connecting to Terraform Cloud using a service user. - 1. Navigate to the **App Connections** tab on the **Organization Settings** page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **Terraform Cloud Connection** option from the connection options modal. ![Select Terraform Cloud Connection](/images/app-connections/terraform-cloud/terraform-cloud-app-connection-option.png) @@ -52,6 +52,7 @@ Infisical supports connecting to Terraform Cloud using a service user. --data '{ "name": "my-terraform-cloud-connection", "method": "api-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "apiToken": "...", } @@ -65,6 +66,7 @@ Infisical supports connecting to Terraform Cloud using a service user. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-terraform-cloud-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 123, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/vercel.mdx b/docs/integrations/app-connections/vercel.mdx index 7ab7bea1b..17973db9b 100644 --- a/docs/integrations/app-connections/vercel.mdx +++ b/docs/integrations/app-connections/vercel.mdx @@ -37,7 +37,7 @@ Infisical supports connecting to Vercel using API Tokens. 1. Navigate to App Connections - In your Infisical dashboard, go to **Organization Settings** and select the **App Connections** tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Add Connection @@ -52,7 +52,7 @@ Infisical supports connecting to Vercel using API Tokens. ![Vercel Connection Modal](/images/app-connections/vercel/vercel-app-connection-modal.png) 4. Connection Created - After clicking Create, your **Vercel Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **Vercel Connection** is established and ready to use with your Infisical project. ![Vercel Connection Created](/images/app-connections/vercel/vercel-app-connection-created.png) @@ -67,6 +67,7 @@ Infisical supports connecting to Vercel using API Tokens. --header 'Content-Type: application/json' \ --data '{ "name": "my-vercel-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "method": "api-token", "credentials": { "apiToken": "...", @@ -81,6 +82,7 @@ Infisical supports connecting to Vercel using API Tokens. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-vercel-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 123, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2025-04-01T05:31:56Z", diff --git a/docs/integrations/app-connections/windmill.mdx b/docs/integrations/app-connections/windmill.mdx index 5cab9fa38..d90c83a1b 100644 --- a/docs/integrations/app-connections/windmill.mdx +++ b/docs/integrations/app-connections/windmill.mdx @@ -47,7 +47,8 @@ Ensure the user generating the access token has the required role and permission - In your Infisical dashboard, go to **Organization Settings** and select the **App Connections** tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. + ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -82,6 +83,7 @@ Ensure the user generating the access token has the required role and permission --data '{ "name": "my-windmill-connection", "method": "access-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "token": "...", "instanceUrl": "https://app.windmill.dev" @@ -96,6 +98,7 @@ Ensure the user generating the access token has the required role and permission "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-windmill-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 123, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2025-04-01T05:31:56Z", diff --git a/docs/integrations/app-connections/zabbix.mdx b/docs/integrations/app-connections/zabbix.mdx index c4d47b22e..45141e827 100644 --- a/docs/integrations/app-connections/zabbix.mdx +++ b/docs/integrations/app-connections/zabbix.mdx @@ -31,7 +31,7 @@ Infisical supports the use of [API Tokens](https://www.zabbix.com/documentation/ - In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -50,7 +50,7 @@ Infisical supports the use of [API Tokens](https://www.zabbix.com/documentation/ ![Zabbix Connection Modal](/images/app-connections/zabbix/zabbix-app-connection-form.png) - After clicking Create, your **Zabbix Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **Zabbix Connection** is established and ready to use with your Infisical project. ![Zabbix Connection Created](/images/app-connections/zabbix/zabbix-app-connection-generated.png) @@ -68,6 +68,7 @@ Infisical supports the use of [API Tokens](https://www.zabbix.com/documentation/ --data '{ "name": "my-zabbix-connection", "method": "api-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "apiToken": "[API TOKEN]", "instanceUrl": "https://zabbix.example.com" @@ -82,6 +83,7 @@ Infisical supports the use of [API Tokens](https://www.zabbix.com/documentation/ "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-zabbix-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx index 76dd24cf7..848da4055 100644 --- a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx @@ -223,7 +223,9 @@ spec: spec: dynamicSecret: secretName: - projectId: + # Use either projectId OR projectSlug, not both + projectId: # Either projectId or projectSlug is required + # projectSlug: environmentSlug: secretsPath: ``` @@ -238,8 +240,21 @@ spec: The project ID of where the dynamic secret is stored in Infisical. + + + Please note that you can only use either `projectId` or `projectSlug` in the `dynamicSecret` field. + + + The project slug of where the dynamic secret is stored in Infisical. + + + Please note that you can only use either `projectId` or `projectSlug` in the `dynamicSecret` field. + + + + {" "} diff --git a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx index d5a9c1ef4..0affe7841 100644 --- a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx @@ -44,7 +44,8 @@ Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes sec deletionPolicy: Delete # If set to delete, the secret(s) inside Infisical managed by the operator, will be deleted if the InfisicalPushSecret CRD is deleted. destination: - projectId: + projectId: # Either projectId or projectSlug is required + projectSlug: environmentSlug: secretsPath: @@ -203,6 +204,18 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y The project ID where you want to create the secrets in Infisical. + + + Please note that you can only use either `projectId` or `projectSlug` in the `destination` field. + + + + + The project slug where you want to create the secrets in Infisical. + + + Please note that you can only use either `projectId` or `projectSlug` in the `destination` field. + diff --git a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx index d0e403b79..d72d58957 100644 --- a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx @@ -142,7 +142,10 @@ spec: authentication: universalAuth: secretsScope: + # either projectSlug or projectId is required projectSlug: # <-- project slug + projectId: # <-- project id + envSlug: # "dev", "staging", "prod", etc.. secretsPath: "" # Root is "/" credentialsRef: @@ -496,9 +499,11 @@ spec: Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`projectSlug`_, or project ID _`projectId`_, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below. + + Please note that you can only use either `projectSlug` or `projectId` in the `secretsScope` field. ## Example @@ -545,9 +550,11 @@ spec: Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`projectSlug`_, or project ID _`projectId`_, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below. + + Please note that you can only use either `projectSlug` or `projectId` in the `secretsScope` field. ## Example @@ -588,9 +595,11 @@ spec: Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`projectSlug`_, or project ID _`projectId`_, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below. + + Please note that you can only use either `projectSlug` or `projectId` in the `secretsScope` field. ## Example @@ -631,9 +640,11 @@ spec: Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`projectSlug`_, or project ID _`projectId`_, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below. + + Please note that you can only use either `projectSlug` or `projectId` in the `secretsScope` field. ## Example @@ -675,9 +686,11 @@ spec: Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`projectSlug`_, or project ID _`projectId`_, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below. + + Please note that you can only use either `projectSlug` or `projectId` in the `secretsScope` field. ## Example @@ -730,9 +743,11 @@ spec: Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`projectSlug`_, or project ID _`projectId`_, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below. + + Please note that you can only use either `projectSlug` or `projectId` in the `secretsScope` field. ## Example diff --git a/docs/internals/permissions/organization-permissions.mdx b/docs/internals/permissions/organization-permissions.mdx index 5f5fc962f..d20ea7def 100644 --- a/docs/internals/permissions/organization-permissions.mdx +++ b/docs/internals/permissions/organization-permissions.mdx @@ -9,7 +9,7 @@ Infisical's organization permissions system follows a role-based access control Each permission consists of: -- **Subject**: The resource the permission applies to (e.g., workspaces, members, billing) +- **Subject**: The resource the permission applies to (e.g., project, members, billing) - **Action**: The operation that can be performed (e.g., read, create, edit, delete) Some organization-level resources—specifically `app-connections`—support conditional permissions and permission inversion for more granular access control. @@ -18,13 +18,13 @@ Some organization-level resources—specifically `app-connections`—support con Below is a comprehensive list of all available organization-level subjects and their supported actions, organized by functional area. -### Workspace Management +### Project Management -#### Subject: `workspace` +#### Subject: `project` (formerly workspace) -| Action | Description | -| -------- | --------------------- | -| `create` | Create new workspaces | +| Action | Description | +| -------- | ------------------ | +| `create` | Create new project | ### Role Management diff --git a/docs/internals/permissions/project-permissions.mdx b/docs/internals/permissions/project-permissions.mdx index c823cf4a7..3a4dd11e6 100644 --- a/docs/internals/permissions/project-permissions.mdx +++ b/docs/internals/permissions/project-permissions.mdx @@ -86,7 +86,7 @@ Below is a comprehensive list of all available project-level subjects and their | `edit` | Modify existing tags | | `delete` | Remove tags from the project | -#### Subject: `workspace` +#### Subject: `project` | Action | Description | | -------- | ------------------------- | @@ -135,6 +135,18 @@ Below is a comprehensive list of all available project-level subjects and their | `edit` | Modify token properties | | `delete` | Revoke or remove service tokens | +#### Subject: `app-connections` + +Supports conditions and permission inversion + +| Action | Description | +| ------------------------- | ---------------------------------- | +| `read-app-connections` | View app connection configurations | +| `create-app-connections` | Create new app connections | +| `edit-app-connections` | Modify existing app connections | +| `delete-app-connections` | Remove app connections | +| `connect-app-connections` | Use app connections | + ### Secrets Management #### Subject: `secrets` diff --git a/docs/sdks/languages/php.mdx b/docs/sdks/languages/php.mdx new file mode 100644 index 000000000..e3db5e5cd --- /dev/null +++ b/docs/sdks/languages/php.mdx @@ -0,0 +1,204 @@ +--- +title: "Infisical PHP SDK" +sidebarTitle: "PHP" +icon: "/images/sdks/languages/php.svg" +--- + +If you're working with PHP, the official Infisical PHP SDK package is the easiest way to fetch and work with secrets for your application. + +## Installation + +```bash +composer require infisical/php-sdk +``` + +## Getting Started + +```php +auth()->universalAuth()->login( + "your-machine-identity-client-id", + "your-machine-identity-client-secret" +); + +// List secrets +$params = new \Infisical\SDK\Models\ListSecretsParameters( + environment: "dev", + secretPath: "/", + projectId: "your-project-id" +); + +$secrets = $sdk->secrets()->list($params); +echo "Fetched secrets: " . count($secrets) . "\n"; +``` + +## Core Methods + +The SDK methods are organized into the following high-level categories: + +1. `auth`: Handles authentication methods. +2. `secrets`: Manages CRUD operations for secrets. + +### `Auth` + +The `auth` component provides methods for authentication: + +#### Universal Auth + +**Authenticating** +```php +$response = $sdk->auth()->universal_auth()->login( + "your-machine-identity-client-id", + "your-machine-identity-client-secret" +); +``` + +**Parameters:** +- `clientId` (string): The client ID of your Machine Identity. +- `clientSecret` (string): The client secret of your Machine Identity. + + + We do not recommend hardcoding your [Machine Identity Tokens](/documentation/platform/identities/overview). Setting them as environment variables would be best. + + +### `Secrets` + +This sub-class handles operations related to secrets: + +#### List Secrets + +```php +use Infisical\SDK\Models\ListSecretsParameters; + +$params = new ListSecretsParameters( + environment: "dev", + secretPath: "/", + projectId: "your-project-id", + tagSlugs: ["tag1", "tag2"], // Optional + recursive: true, // Optional + expandSecretReferences: true, // Optional + attachToProcessEnv: false, // Optional + skipUniqueValidation: false // Optional +); + +$secrets = $sdk->secrets()->list($params); +``` + +**Parameters:** +- `environment` (string): The environment in which to list secrets (e.g., "dev"). +- `projectId` (string): The ID of your project. +- `secretPath` (string, optional): The path to the secrets. +- `tagSlugs` (array, optional): Tags to filter secrets. +- `recursive` (bool, optional): Whether to list secrets recursively. +- `expandSecretReferences` (bool, optional): Whether to expand secret references. +- `attachToProcessEnv` (bool, optional): Whether to attach secrets to process environment variables. +- `skipUniqueValidation` (bool, optional): Whether to skip unique validation. + +**Returns:** +- `Secret[]`: An array of secret objects. + +#### Create Secret + +```php +use Infisical\SDK\Models\CreateSecretParameters; + +$params = new CreateSecretParameters( + secretKey: "SECRET_NAME", + secretValue: "SECRET_VALUE", + environment: "dev", + secretPath: "/", + projectId: "your-project-id" +); + +$createdSecret = $sdk->secrets()->create($params); +``` + +**Parameters:** +- `secretKey` (string): The name of the secret to create. +- `secretValue` (string): The value of the secret. +- `environment` (string): The environment in which to create the secret. +- `projectId` (string): The ID of your project. +- `secretPath` (string, optional): The path to the secret. + +**Returns:** +- `Secret`: The created secret object. + +#### Get Secret + +```php +use Infisical\SDK\Models\GetSecretParameters; + +$params = new GetSecretParameters( + secretKey: "SECRET_NAME", + environment: "dev", + secretPath: "/", + projectId: "your-project-id" +); + +$secret = $sdk->secrets()->get($params); +``` + +**Parameters:** +- `secretKey` (string): The name of the secret to retrieve. +- `environment` (string): The environment in which to retrieve the secret. +- `projectId` (string): The ID of your project. +- `secretPath` (string, optional): The path to the secret. + +**Returns:** +- `Secret`: The retrieved secret object. + +#### Update Secret + +```php +use Infisical\SDK\Models\UpdateSecretParameters; + +$params = new UpdateSecretParameters( + secretKey: "SECRET_NAME", + newSecretValue: "UPDATED_SECRET_VALUE", + environment: "dev", + secretPath: "/", + projectId: "your-project-id" +); + +$updatedSecret = $sdk->secrets()->update($params); +``` + +**Parameters:** +- `secretKey` (string): The name of the secret to update. +- `newSecretValue` (string): The new value of the secret. +- `environment` (string): The environment in which to update the secret. +- `projectId` (string): The ID of your project. +- `secretPath` (string, optional): The path to the secret. + +**Returns:** +- `Secret`: The updated secret object. + +#### Delete Secret + +```php +use Infisical\SDK\Models\DeleteSecretParameters; + +$params = new DeleteSecretParameters( + secretKey: "SECRET_NAME", + environment: "dev", + secretPath: "/", + projectId: "your-project-id" +); + +$deletedSecret = $sdk->secrets()->delete($params); +``` + +**Parameters:** +- `secretKey` (string): The name of the secret to delete. +- `environment` (string): The environment in which to delete the secret. +- `projectId` (string): The ID of your project. +- `secretPath` (string, optional): The path to the secret. + +**Returns:** +- `Secret`: The deleted secret object. \ No newline at end of file diff --git a/docs/sdks/overview.mdx b/docs/sdks/overview.mdx index 0b6949108..f9a8fcac3 100644 --- a/docs/sdks/overview.mdx +++ b/docs/sdks/overview.mdx @@ -32,6 +32,9 @@ From local development to production, Infisical SDKs provide the easiest way for Manage secrets for your Go application on demand + + + Manage secrets for your PHP application on demand Manage secrets for your Ruby application on demand diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 53adf95a4..8d351d7f2 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -142,7 +142,7 @@ DB_READ_REPLICAS=[{"DB_CONNECTION_URI":""}] Redis is used for caching and background tasks. You can use either a standalone Redis instance or a Redis Sentinel setup. - + Redis connection string. @@ -173,6 +173,49 @@ Redis is used for caching and background tasks. You can use either a standalone Authentication password for Redis Sentinel + + Authentication username for Redis Node + + + Authentication password for Redis Node + + + + + Comma-separated list of Redis Cluster host:port pairs. ``` + 192.168.65.254:26379,192.168.65.254:26380 ``` + + + Enable Redis TLS encryption on connection. + + + Enable this if you are using AWS encrypt on transit for Elasticache cluster. For more information refer ![here](https://github.com/redis/ioredis?tab=readme-ov-file#special-note-aws-elasticache-clusters-with-tls). + + + Authentication username for Redis Node + + + Authentication password for Redis Node + + + + + Comma-separated list of Redis read replicas host:port pairs. ``` + 192.168.65.254:26379,192.168.65.254:26380 ``` + + + The paramters like username, password, tls, redis type of the primary instance will be inherited. + diff --git a/docs/self-hosting/guides/releases.mdx b/docs/self-hosting/guides/releases.mdx index d85da1a78..3cc22ea41 100644 --- a/docs/self-hosting/guides/releases.mdx +++ b/docs/self-hosting/guides/releases.mdx @@ -19,8 +19,9 @@ Infisical provides two distinct release channels with different update frequenci - **Update Frequency**: Daily builds during weekdays (Monday-Friday) - - **Version Tags**: `vX.Y.Z-nightly-YYYYMMDD` (e.g., `v0.146.0-nightly-20250423`) - - **Multiple Daily Builds**: If multiple nightly builds are created on the same day, they are numbered incrementally: `vX.Y.Z-nightly-YYYYMMDD.1`, `vX.Y.Z-nightly-YYYYMMDD.2`, etc. + - **Versioning Strategy**: Nightly releases provide daily patches and features while making its way towards the next stable release + - **Version Format**: `vX.Y.0-nightly-YYYYMMDD` where X.Y represents the next minor version increment from the latest stable release + - **Multiple Daily Builds**: If multiple nightly builds are created on the same day, they are numbered incrementally: `vX.Y.0-nightly-YYYYMMDD.1`, `vX.Y.0-nightly-YYYYMMDD.2`, etc. - **Stability**: Latest features with standard CI/CD testing - **Release Process**: Built from main branch after all automated tests pass - **Intended Audience**: Development environments & early adopters @@ -31,6 +32,7 @@ Infisical provides two distinct release channels with different update frequenci **Characteristics:** - Access to latest features immediately + - Pre-release versions of upcoming stable releases - Faster security patch delivery - Higher update frequency (daily) diff --git a/docs/self-hosting/guides/upgrading-infisical.mdx b/docs/self-hosting/guides/upgrading-infisical.mdx index 60c6edbff..cae8193bf 100644 --- a/docs/self-hosting/guides/upgrading-infisical.mdx +++ b/docs/self-hosting/guides/upgrading-infisical.mdx @@ -54,4 +54,62 @@ Now, migrations run automatically during boot-up. This improvement streamlines t - Once the migration is complete, all instances will operate with the updated schema. 5. **Verify the Upgrade:** - - Review the logs for any migration errors or warnings. + - Review the logs for any migration errors or warnings. + - Test basic functionality to ensure the upgrade was successful. + +## Troubleshooting + +### UI Caching Issues After Upgrade + +After upgrading your Infisical instance, you may encounter UI-related issues such as: +- Strange loading behavior +- Components not rendering correctly +- Unexpected errors in the browser console +- Features appearing broken or unresponsive + +These issues are often caused by browser caching of the previous version's static assets. + +**Solution:** +1. **Try a private/incognito browser window first** - This is the quickest way to test if the issue is cache-related. +2. **Clear your browser cache** if the private window works correctly: + - **Chrome/Edge:** Press `Ctrl+Shift+Delete` (Windows/Linux) or `Cmd+Shift+Delete` (Mac) + - **Firefox:** Press `Ctrl+Shift+Delete` (Windows/Linux) or `Cmd+Shift+Delete` (Mac) + - **Safari:** Press `Cmd+Option+E` or go to Develop menu > Empty Caches +3. **Hard refresh the page** by pressing `Ctrl+F5` (Windows/Linux) or `Cmd+Shift+R` (Mac) + + + Caching issues are temporary and typically resolve themselves as the cache expires, but manually clearing the cache provides immediate resolution. + + +## Downgrade Considerations + +While we recommend staying up-to-date with the latest version, there may be scenarios where you need to downgrade your Infisical instance. + + + **Database Compatibility:** Downgrading can be complex due to database schema changes. Always ensure you have proper backups before attempting any version changes. + + +### Safe Downgrade Process + +1. **Prepare Database Snapshot:** + - Create a database snapshot/backup **before** upgrading to the target version + - Ensure the snapshot is from a version compatible with your desired downgrade target + +2. **Stop Infisical Services:** + - Gracefully shut down all Infisical instances to prevent data corruption + +3. **Restore Database:** + - Restore your database from the pre-upgrade snapshot + - **Critical:** Do not attempt to downgrade with a database that has run migrations from a newer version + +4. **Deploy Previous Version:** + - Deploy the previous Infisical version + - Verify that the version matches the database schema in your snapshot + +5. **Verify Functionality:** + - Test critical functionality to ensure the downgrade was successful + - Monitor logs for any compatibility issues + + + The safest approach for downgrades is to restore to a known good state (both application and database) rather than attempting to reverse individual migrations. + diff --git a/docs/snippets/AppConnectionsBrowser.jsx b/docs/snippets/AppConnectionsBrowser.jsx index 50aec75b3..72ad40cff 100644 --- a/docs/snippets/AppConnectionsBrowser.jsx +++ b/docs/snippets/AppConnectionsBrowser.jsx @@ -4,7 +4,7 @@ export const AppConnectionsBrowser = () => { const [searchTerm, setSearchTerm] = useState(''); const [selectedCategory, setSelectedCategory] = useState('All'); - const categories = ['All', 'Cloud Providers', 'Databases', 'CI/CD', 'Monitoring', 'Directory Services', 'Identity & Auth']; + const categories = ['All', 'Cloud Providers', 'Databases', 'CI/CD', 'Monitoring', 'Directory Services', 'Identity & Auth', 'Data Analytics', 'Hosting', 'DevOps Tools', 'Security']; const connections = [ {"name": "AWS", "slug": "aws", "path": "/integrations/app-connections/aws", "description": "Learn how to connect your AWS applications to pull secrets from Infisical.", "category": "Cloud Providers"}, @@ -14,15 +14,15 @@ export const AppConnectionsBrowser = () => { {"name": "Azure DevOps", "slug": "azure-devops", "path": "/integrations/app-connections/azure-devops", "description": "Learn how to connect your Azure DevOps to pull secrets from Infisical.", "category": "CI/CD"}, {"name": "Azure ADCS", "slug": "azure-adcs", "path": "/integrations/app-connections/azure-adcs", "description": "Learn how to connect your Azure ADCS to pull secrets from Infisical.", "category": "Cloud Providers"}, {"name": "GCP", "slug": "gcp", "path": "/integrations/app-connections/gcp", "description": "Learn how to connect your GCP applications to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "HashiCorp Vault", "slug": "hashicorp-vault", "path": "/integrations/app-connections/hashicorp-vault", "description": "Learn how to connect your HashiCorp Vault to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "1Password", "slug": "1password", "path": "/integrations/app-connections/1password", "description": "Learn how to connect your 1Password to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Vercel", "slug": "vercel", "path": "/integrations/app-connections/vercel", "description": "Learn how to connect your Vercel application to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Netlify", "slug": "netlify", "path": "/integrations/app-connections/netlify", "description": "Learn how to connect your Netlify application to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Railway", "slug": "railway", "path": "/integrations/app-connections/railway", "description": "Learn how to connect your Railway application to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Fly.io", "slug": "flyio", "path": "/integrations/app-connections/flyio", "description": "Learn how to connect your Fly.io application to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Render", "slug": "render", "path": "/integrations/app-connections/render", "description": "Learn how to connect your Render application to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Heroku", "slug": "heroku", "path": "/integrations/app-connections/heroku", "description": "Learn how to connect your Heroku application to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "DigitalOcean", "slug": "digital-ocean", "path": "/integrations/app-connections/digital-ocean", "description": "Learn how to connect your DigitalOcean application to pull secrets from Infisical.", "category": "Cloud Providers"}, + {"name": "HashiCorp Vault", "slug": "hashicorp-vault", "path": "/integrations/app-connections/hashicorp-vault", "description": "Learn how to connect your HashiCorp Vault to pull secrets from Infisical.", "category": "Security"}, + {"name": "1Password", "slug": "1password", "path": "/integrations/app-connections/1password", "description": "Learn how to connect your 1Password to pull secrets from Infisical.", "category": "Security"}, + {"name": "Vercel", "slug": "vercel", "path": "/integrations/app-connections/vercel", "description": "Learn how to connect your Vercel application to pull secrets from Infisical.", "category": "Hosting"}, + {"name": "Netlify", "slug": "netlify", "path": "/integrations/app-connections/netlify", "description": "Learn how to connect your Netlify application to pull secrets from Infisical.", "category": "Hosting"}, + {"name": "Railway", "slug": "railway", "path": "/integrations/app-connections/railway", "description": "Learn how to connect your Railway application to pull secrets from Infisical.", "category": "Hosting"}, + {"name": "Fly.io", "slug": "flyio", "path": "/integrations/app-connections/flyio", "description": "Learn how to connect your Fly.io application to pull secrets from Infisical.", "category": "Hosting"}, + {"name": "Render", "slug": "render", "path": "/integrations/app-connections/render", "description": "Learn how to connect your Render application to pull secrets from Infisical.", "category": "Hosting"}, + {"name": "Heroku", "slug": "heroku", "path": "/integrations/app-connections/heroku", "description": "Learn how to connect your Heroku application to pull secrets from Infisical.", "category": "Hosting"}, + {"name": "DigitalOcean", "slug": "digital-ocean", "path": "/integrations/app-connections/digital-ocean", "description": "Learn how to connect your DigitalOcean application to pull secrets from Infisical.", "category": "Hosting"}, {"name": "Supabase", "slug": "supabase", "path": "/integrations/app-connections/supabase", "description": "Learn how to connect your Supabase application to pull secrets from Infisical.", "category": "Databases"}, {"name": "Checkly", "slug": "checkly", "path": "/integrations/app-connections/checkly", "description": "Learn how to connect your Checkly application to pull secrets from Infisical.", "category": "Monitoring"}, {"name": "GitHub", "slug": "github", "path": "/integrations/app-connections/github", "description": "Learn how to connect your GitHub application to pull secrets from Infisical.", "category": "CI/CD"}, @@ -30,12 +30,12 @@ export const AppConnectionsBrowser = () => { {"name": "GitLab", "slug": "gitlab", "path": "/integrations/app-connections/gitlab", "description": "Learn how to connect your GitLab application to pull secrets from Infisical.", "category": "CI/CD"}, {"name": "TeamCity", "slug": "teamcity", "path": "/integrations/app-connections/teamcity", "description": "Learn how to connect your TeamCity to pull secrets from Infisical.", "category": "CI/CD"}, {"name": "Bitbucket", "slug": "bitbucket", "path": "/integrations/app-connections/bitbucket", "description": "Learn how to connect your Bitbucket to pull secrets from Infisical.", "category": "CI/CD"}, - {"name": "Terraform Cloud", "slug": "terraform-cloud", "path": "/integrations/app-connections/terraform-cloud", "description": "Learn how to connect your Terraform Cloud to pull secrets from Infisical.", "category": "Cloud Providers"}, + {"name": "Terraform Cloud", "slug": "terraform-cloud", "path": "/integrations/app-connections/terraform-cloud", "description": "Learn how to connect your Terraform Cloud to pull secrets from Infisical.", "category": "DevOps Tools"}, {"name": "Cloudflare", "slug": "cloudflare", "path": "/integrations/app-connections/cloudflare", "description": "Learn how to connect your Cloudflare application to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Databricks", "slug": "databricks", "path": "/integrations/app-connections/databricks", "description": "Learn how to connect your Databricks to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Windmill", "slug": "windmill", "path": "/integrations/app-connections/windmill", "description": "Learn how to connect your Windmill to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Camunda", "slug": "camunda", "path": "/integrations/app-connections/camunda", "description": "Learn how to connect your Camunda to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Humanitec", "slug": "humanitec", "path": "/integrations/app-connections/humanitec", "description": "Learn how to connect your Humanitec to pull secrets from Infisical.", "category": "Cloud Providers"}, + {"name": "Databricks", "slug": "databricks", "path": "/integrations/app-connections/databricks", "description": "Learn how to connect your Databricks to pull secrets from Infisical.", "category": "Data Analytics"}, + {"name": "Windmill", "slug": "windmill", "path": "/integrations/app-connections/windmill", "description": "Learn how to connect your Windmill to pull secrets from Infisical.", "category": "DevOps Tools"}, + {"name": "Camunda", "slug": "camunda", "path": "/integrations/app-connections/camunda", "description": "Learn how to connect your Camunda to pull secrets from Infisical.", "category": "DevOps Tools"}, + {"name": "Humanitec", "slug": "humanitec", "path": "/integrations/app-connections/humanitec", "description": "Learn how to connect your Humanitec to pull secrets from Infisical.", "category": "DevOps Tools"}, {"name": "OCI", "slug": "oci", "path": "/integrations/app-connections/oci", "description": "Learn how to connect your OCI applications to pull secrets from Infisical.", "category": "Cloud Providers"}, {"name": "Zabbix", "slug": "zabbix", "path": "/integrations/app-connections/zabbix", "description": "Learn how to connect your Zabbix to pull secrets from Infisical.", "category": "Monitoring"}, {"name": "MySQL", "slug": "mysql", "path": "/integrations/app-connections/mysql", "description": "Learn how to connect your MySQL database to pull secrets from Infisical.", "category": "Databases"}, diff --git a/docs/snippets/DynamicSecretsBrowser.jsx b/docs/snippets/DynamicSecretsBrowser.jsx index 37a26c3c8..6438deea0 100644 --- a/docs/snippets/DynamicSecretsBrowser.jsx +++ b/docs/snippets/DynamicSecretsBrowser.jsx @@ -4,7 +4,7 @@ export const DynamicSecretsBrowser = () => { const [searchTerm, setSearchTerm] = useState(''); const [selectedCategory, setSelectedCategory] = useState('All'); - const categories = ['All', 'Databases', 'Cloud Providers', 'Message Queues', 'Caches']; + const categories = ['All', 'Databases', 'Cloud Providers', 'Message Queues', 'Caches', 'Directory Services', 'CI/CD', 'Container Orchestration', 'Authentication']; const dynamicSecrets = [ {"name": "AWS IAM", "slug": "aws-iam", "path": "/documentation/platform/dynamic-secrets/aws-iam", "description": "Learn how to generate dynamic AWS IAM credentials on-demand.", "category": "Cloud Providers"}, @@ -26,10 +26,10 @@ export const DynamicSecretsBrowser = () => { {"name": "Redis", "slug": "redis", "path": "/documentation/platform/dynamic-secrets/redis", "description": "Learn how to generate dynamic Redis credentials on-demand.", "category": "Caches"}, {"name": "ElasticSearch", "slug": "elasticsearch", "path": "/documentation/platform/dynamic-secrets/elastic-search", "description": "Learn how to generate dynamic ElasticSearch credentials on-demand.", "category": "Databases"}, {"name": "RabbitMQ", "slug": "rabbitmq", "path": "/documentation/platform/dynamic-secrets/rabbit-mq", "description": "Learn how to generate dynamic RabbitMQ credentials on-demand.", "category": "Message Queues"}, - {"name": "LDAP", "slug": "ldap", "path": "/documentation/platform/dynamic-secrets/ldap", "description": "Learn how to generate dynamic LDAP credentials on-demand.", "category": "Cloud Providers"}, - {"name": "GitHub", "slug": "github", "path": "/documentation/platform/dynamic-secrets/github", "description": "Learn how to generate dynamic GitHub credentials on-demand.", "category": "Cloud Providers"}, - {"name": "Kubernetes", "slug": "kubernetes", "path": "/documentation/platform/dynamic-secrets/kubernetes", "description": "Learn how to generate dynamic Kubernetes credentials on-demand.", "category": "Cloud Providers"}, - {"name": "TOTP", "slug": "totp", "path": "/documentation/platform/dynamic-secrets/totp", "description": "Learn how to generate dynamic TOTP codes on-demand.", "category": "Cloud Providers"} + {"name": "LDAP", "slug": "ldap", "path": "/documentation/platform/dynamic-secrets/ldap", "description": "Learn how to generate dynamic LDAP credentials on-demand.", "category": "Directory Services"}, + {"name": "GitHub", "slug": "github", "path": "/documentation/platform/dynamic-secrets/github", "description": "Learn how to generate dynamic GitHub credentials on-demand.", "category": "CI/CD"}, + {"name": "Kubernetes", "slug": "kubernetes", "path": "/documentation/platform/dynamic-secrets/kubernetes", "description": "Learn how to generate dynamic Kubernetes credentials on-demand.", "category": "Container Orchestration"}, + {"name": "TOTP", "slug": "totp", "path": "/documentation/platform/dynamic-secrets/totp", "description": "Learn how to generate dynamic TOTP codes on-demand.", "category": "Authentication"} ].sort(function(a, b) { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); }); diff --git a/docs/snippets/SecretSyncsBrowser.jsx b/docs/snippets/SecretSyncsBrowser.jsx index 01688a6f3..d32f5ac04 100644 --- a/docs/snippets/SecretSyncsBrowser.jsx +++ b/docs/snippets/SecretSyncsBrowser.jsx @@ -4,7 +4,7 @@ export const SecretSyncsBrowser = () => { const [searchTerm, setSearchTerm] = useState(''); const [selectedCategory, setSelectedCategory] = useState('All'); - const categories = ['All', 'Cloud Providers', 'Databases', 'CI/CD', 'Monitoring']; + const categories = ['All', 'Cloud Providers', 'Databases', 'CI/CD', 'Monitoring', 'Data Analytics', 'Hosting', 'DevOps Tools', 'Security']; const syncs = [ {"name": "AWS Parameter Store", "slug": "aws-parameter-store", "path": "/integrations/secret-syncs/aws-parameter-store", "description": "Learn how to sync secrets from Infisical to AWS Parameter Store.", "category": "Cloud Providers"}, @@ -13,28 +13,28 @@ export const SecretSyncsBrowser = () => { {"name": "Azure App Configuration", "slug": "azure-app-configuration", "path": "/integrations/secret-syncs/azure-app-configuration", "description": "Learn how to sync secrets from Infisical to Azure App Configuration.", "category": "Cloud Providers"}, {"name": "Azure DevOps", "slug": "azure-devops", "path": "/integrations/secret-syncs/azure-devops", "description": "Learn how to sync secrets from Infisical to Azure DevOps.", "category": "CI/CD"}, {"name": "GCP Secret Manager", "slug": "gcp-secret-manager", "path": "/integrations/secret-syncs/gcp-secret-manager", "description": "Learn how to sync secrets from Infisical to GCP Secret Manager.", "category": "Cloud Providers"}, - {"name": "HashiCorp Vault", "slug": "hashicorp-vault", "path": "/integrations/secret-syncs/hashicorp-vault", "description": "Learn how to sync secrets from Infisical to HashiCorp Vault.", "category": "Cloud Providers"}, - {"name": "1Password", "slug": "1password", "path": "/integrations/secret-syncs/1password", "description": "Learn how to sync secrets from Infisical to 1Password.", "category": "Cloud Providers"}, - {"name": "Vercel", "slug": "vercel", "path": "/integrations/secret-syncs/vercel", "description": "Learn how to sync secrets from Infisical to Vercel.", "category": "Cloud Providers"}, - {"name": "Netlify", "slug": "netlify", "path": "/integrations/secret-syncs/netlify", "description": "Learn how to sync secrets from Infisical to Netlify.", "category": "Cloud Providers"}, - {"name": "Railway", "slug": "railway", "path": "/integrations/secret-syncs/railway", "description": "Learn how to sync secrets from Infisical to Railway.", "category": "Cloud Providers"}, - {"name": "Fly.io", "slug": "flyio", "path": "/integrations/secret-syncs/flyio", "description": "Learn how to sync secrets from Infisical to Fly.io.", "category": "Cloud Providers"}, - {"name": "Render", "slug": "render", "path": "/integrations/secret-syncs/render", "description": "Learn how to sync secrets from Infisical to Render.", "category": "Cloud Providers"}, - {"name": "Heroku", "slug": "heroku", "path": "/integrations/secret-syncs/heroku", "description": "Learn how to sync secrets from Infisical to Heroku.", "category": "Cloud Providers"}, - {"name": "DigitalOcean App Platform", "slug": "digital-ocean-app-platform", "path": "/integrations/secret-syncs/digital-ocean-app-platform", "description": "Learn how to sync secrets from Infisical to DigitalOcean App Platform.", "category": "Cloud Providers"}, + {"name": "HashiCorp Vault", "slug": "hashicorp-vault", "path": "/integrations/secret-syncs/hashicorp-vault", "description": "Learn how to sync secrets from Infisical to HashiCorp Vault.", "category": "Security"}, + {"name": "1Password", "slug": "1password", "path": "/integrations/secret-syncs/1password", "description": "Learn how to sync secrets from Infisical to 1Password.", "category": "Security"}, + {"name": "Vercel", "slug": "vercel", "path": "/integrations/secret-syncs/vercel", "description": "Learn how to sync secrets from Infisical to Vercel.", "category": "Hosting"}, + {"name": "Netlify", "slug": "netlify", "path": "/integrations/secret-syncs/netlify", "description": "Learn how to sync secrets from Infisical to Netlify.", "category": "Hosting"}, + {"name": "Railway", "slug": "railway", "path": "/integrations/secret-syncs/railway", "description": "Learn how to sync secrets from Infisical to Railway.", "category": "Hosting"}, + {"name": "Fly.io", "slug": "flyio", "path": "/integrations/secret-syncs/flyio", "description": "Learn how to sync secrets from Infisical to Fly.io.", "category": "Hosting"}, + {"name": "Render", "slug": "render", "path": "/integrations/secret-syncs/render", "description": "Learn how to sync secrets from Infisical to Render.", "category": "Hosting"}, + {"name": "Heroku", "slug": "heroku", "path": "/integrations/secret-syncs/heroku", "description": "Learn how to sync secrets from Infisical to Heroku.", "category": "Hosting"}, + {"name": "DigitalOcean App Platform", "slug": "digital-ocean-app-platform", "path": "/integrations/secret-syncs/digital-ocean-app-platform", "description": "Learn how to sync secrets from Infisical to DigitalOcean App Platform.", "category": "Hosting"}, {"name": "Supabase", "slug": "supabase", "path": "/integrations/secret-syncs/supabase", "description": "Learn how to sync secrets from Infisical to Supabase.", "category": "Databases"}, {"name": "Checkly", "slug": "checkly", "path": "/integrations/secret-syncs/checkly", "description": "Learn how to sync secrets from Infisical to Checkly.", "category": "Monitoring"}, {"name": "GitHub", "slug": "github", "path": "/integrations/secret-syncs/github", "description": "Learn how to sync secrets from Infisical to GitHub.", "category": "CI/CD"}, {"name": "GitLab", "slug": "gitlab", "path": "/integrations/secret-syncs/gitlab", "description": "Learn how to sync secrets from Infisical to GitLab.", "category": "CI/CD"}, {"name": "TeamCity", "slug": "teamcity", "path": "/integrations/secret-syncs/teamcity", "description": "Learn how to sync secrets from Infisical to TeamCity.", "category": "CI/CD"}, {"name": "Bitbucket", "slug": "bitbucket", "path": "/integrations/secret-syncs/bitbucket", "description": "Learn how to sync secrets from Infisical to Bitbucket.", "category": "CI/CD"}, - {"name": "Terraform Cloud", "slug": "terraform-cloud", "path": "/integrations/secret-syncs/terraform-cloud", "description": "Learn how to sync secrets from Infisical to Terraform Cloud.", "category": "Cloud Providers"}, - {"name": "Cloudflare Pages", "slug": "cloudflare-pages", "path": "/integrations/secret-syncs/cloudflare-pages", "description": "Learn how to sync secrets from Infisical to Cloudflare Pages.", "category": "Cloud Providers"}, + {"name": "Terraform Cloud", "slug": "terraform-cloud", "path": "/integrations/secret-syncs/terraform-cloud", "description": "Learn how to sync secrets from Infisical to Terraform Cloud.", "category": "DevOps Tools"}, + {"name": "Cloudflare Pages", "slug": "cloudflare-pages", "path": "/integrations/secret-syncs/cloudflare-pages", "description": "Learn how to sync secrets from Infisical to Cloudflare Pages.", "category": "Hosting"}, {"name": "Cloudflare Workers", "slug": "cloudflare-workers", "path": "/integrations/secret-syncs/cloudflare-workers", "description": "Learn how to sync secrets from Infisical to Cloudflare Workers.", "category": "Cloud Providers"}, - {"name": "Databricks", "slug": "databricks", "path": "/integrations/secret-syncs/databricks", "description": "Learn how to sync secrets from Infisical to Databricks.", "category": "Cloud Providers"}, - {"name": "Windmill", "slug": "windmill", "path": "/integrations/secret-syncs/windmill", "description": "Learn how to sync secrets from Infisical to Windmill.", "category": "Cloud Providers"}, - {"name": "Camunda", "slug": "camunda", "path": "/integrations/secret-syncs/camunda", "description": "Learn how to sync secrets from Infisical to Camunda.", "category": "Cloud Providers"}, - {"name": "Humanitec", "slug": "humanitec", "path": "/integrations/secret-syncs/humanitec", "description": "Learn how to sync secrets from Infisical to Humanitec.", "category": "Cloud Providers"}, + {"name": "Databricks", "slug": "databricks", "path": "/integrations/secret-syncs/databricks", "description": "Learn how to sync secrets from Infisical to Databricks.", "category": "Data Analytics"}, + {"name": "Windmill", "slug": "windmill", "path": "/integrations/secret-syncs/windmill", "description": "Learn how to sync secrets from Infisical to Windmill.", "category": "DevOps Tools"}, + {"name": "Camunda", "slug": "camunda", "path": "/integrations/secret-syncs/camunda", "description": "Learn how to sync secrets from Infisical to Camunda.", "category": "DevOps Tools"}, + {"name": "Humanitec", "slug": "humanitec", "path": "/integrations/secret-syncs/humanitec", "description": "Learn how to sync secrets from Infisical to Humanitec.", "category": "DevOps Tools"}, {"name": "OCI Vault", "slug": "oci-vault", "path": "/integrations/secret-syncs/oci-vault", "description": "Learn how to sync secrets from Infisical to OCI Vault.", "category": "Cloud Providers"}, {"name": "Zabbix", "slug": "zabbix", "path": "/integrations/secret-syncs/zabbix", "description": "Learn how to sync secrets from Infisical to Zabbix.", "category": "Monitoring"} ].sort(function(a, b) { diff --git a/frontend/public/images/integrations/Cribl.png b/frontend/public/images/integrations/Cribl.png new file mode 100644 index 000000000..9e8282368 Binary files /dev/null and b/frontend/public/images/integrations/Cribl.png differ diff --git a/frontend/public/images/integrations/Datadog.png b/frontend/public/images/integrations/Datadog.png new file mode 100644 index 000000000..7c1b33de0 Binary files /dev/null and b/frontend/public/images/integrations/Datadog.png differ diff --git a/frontend/public/images/integrations/Splunk.png b/frontend/public/images/integrations/Splunk.png new file mode 100644 index 000000000..54f13cd25 Binary files /dev/null and b/frontend/public/images/integrations/Splunk.png differ diff --git a/frontend/src/components/app-connections/AppConnectionOption.tsx b/frontend/src/components/app-connections/AppConnectionOption.tsx new file mode 100644 index 000000000..2978dc9f0 --- /dev/null +++ b/frontend/src/components/app-connections/AppConnectionOption.tsx @@ -0,0 +1,45 @@ +import { components, OptionProps } from "react-select"; +import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; +import { faBuilding, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Badge, Tooltip } from "@app/components/v2"; +import { TAvailableAppConnection } from "@app/hooks/api/appConnections"; + +export const AppConnectionOption = ({ + isSelected, + children, + ...props +}: OptionProps) => { + const isCreateOption = props.data.id === "_create"; + + return ( + +
+ {isCreateOption ? ( +
+ + Create New Connection +
+ ) : ( + <> +

{children}

+ {!props.data.projectId && ( + +
+ + + Organization + +
+
+ )} + {isSelected && ( + + )} + + )} +
+
+ ); +}; diff --git a/frontend/src/components/app-connections/index.ts b/frontend/src/components/app-connections/index.ts new file mode 100644 index 000000000..7ced7c907 --- /dev/null +++ b/frontend/src/components/app-connections/index.ts @@ -0,0 +1 @@ +export * from "./AppConnectionOption"; diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index 1283c02d1..b16015680 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, useParams } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; -import { useOrganization, useWorkspace } from "@app/context"; +import { useOrganization, useProject } from "@app/context"; import { useToggle } from "@app/hooks"; import { createNotification } from "../notifications"; @@ -51,7 +51,7 @@ export default function NavHeader({ isProtectedBranch = false, protectionPolicyName }: Props): JSX.Element { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { currentOrg } = useOrganization(); const [isCopied, { timedToggle: toggleIsCopied }] = useToggle(false); @@ -79,7 +79,7 @@ export default function NavHeader({ <>
- {currentWorkspace?.name} + {currentProject?.name}
)} @@ -93,7 +93,7 @@ export default function NavHeader({ {pageName === "Secrets" ? ( {pageName} @@ -129,7 +129,7 @@ export default function NavHeader({ {userAvailableEnvs?.find(({ slug }) => slug === currentEnv)?.name} @@ -192,7 +192,7 @@ export default function NavHeader({ ({ ...query, secretPath: newSecretPath })} diff --git a/frontend/src/components/permissions/AccessTree/hooks/index.ts b/frontend/src/components/permissions/AccessTree/hooks/index.ts index 717bdaf3a..b02e6a9d6 100644 --- a/frontend/src/components/permissions/AccessTree/hooks/index.ts +++ b/frontend/src/components/permissions/AccessTree/hooks/index.ts @@ -3,7 +3,7 @@ import { useFormContext, useWatch } from "react-hook-form"; import { MongoAbility, MongoQuery } from "@casl/ability"; import { Edge, Node, useEdgesState, useNodesState } from "@xyflow/react"; -import { ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionSub, useProject } from "@app/context"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { useListProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/queries"; import { TSecretFolderWithPath } from "@app/hooks/api/secretFolders/types"; @@ -36,15 +36,15 @@ export const useAccessTree = ( searchPath: string, subject: ProjectPermissionSub ) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext(); const { control } = useFormContext(); const metadata = useWatch({ control, name: "metadata" }); const [nodes, setNodes] = useNodesState([]); const [edges, setEdges] = useEdgesState([]); - const [environment, setEnvironment] = useState(currentWorkspace.environments[0]?.slug ?? ""); + const [environment, setEnvironment] = useState(currentProject.environments[0]?.slug ?? ""); const { data: environmentsFolders, isPending } = useListProjectEnvironmentsFolders( - currentWorkspace.id + currentProject.id ); const [levelFolderMap, setLevelFolderMap] = useState({}); @@ -279,7 +279,7 @@ export const useAccessTree = ( environment, setEnvironment, isLoading: isPending, - environments: currentWorkspace.environments, + environments: currentProject.environments, secretName, setSecretName, viewMode, diff --git a/frontend/src/components/permissions/OrgPermissionCan.tsx b/frontend/src/components/permissions/OrgPermissionCan.tsx index d2e698e88..bdb39ba1b 100644 --- a/frontend/src/components/permissions/OrgPermissionCan.tsx +++ b/frontend/src/components/permissions/OrgPermissionCan.tsx @@ -1,7 +1,10 @@ import { FunctionComponent, ReactNode } from "react"; -import { BoundCanProps, Can } from "@casl/react"; +import { AbilityTuple, MongoAbility } from "@casl/ability"; +import { Can } from "@casl/react"; -import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext"; +import { TooltipProps } from "@app/components/v2/Tooltip/Tooltip"; +import { useOrgPermission } from "@app/context/OrgPermissionContext"; +import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types"; import { AccessRestrictedBanner, Tooltip } from "../v2"; @@ -13,22 +16,33 @@ export const OrgPermissionGuardBanner = () => { ); }; -type Props = { +type Props = { label?: ReactNode; // this prop is used when there exist already a tooltip as helper text for users // so when permission is allowed same tooltip will be reused to show helpertext renderTooltip?: boolean; allowedLabel?: string; renderGuardBanner?: boolean; -} & BoundCanProps; + tooltipProps?: Omit; + I: T[0]; + ability?: MongoAbility; + children: ReactNode | ((isAllowed: boolean, ability: T) => ReactNode); + passThrough?: boolean; +} & ( + | { an: T[1] } + | { + a: T[1]; + } +); -export const OrgPermissionCan: FunctionComponent = ({ +export const OrgPermissionCan: FunctionComponent> = ({ label = "Access restricted", children, passThrough = true, renderTooltip, allowedLabel, renderGuardBanner, + tooltipProps, ...props }) => { const { permission } = useOrgPermission(); @@ -38,16 +52,22 @@ export const OrgPermissionCan: FunctionComponent = ({ {(isAllowed, ability) => { // akhilmhdh: This is set as type due to error in casl react type. const finalChild = - typeof children === "function" - ? children(isAllowed, ability as TOrgPermission) - : children; + typeof children === "function" ? children(isAllowed, ability as any) : children; if (!isAllowed && passThrough) { - return {finalChild}; + return ( + + {finalChild} + + ); } if (isAllowed && renderTooltip && allowedLabel) { - return {finalChild}; + return ( + + {finalChild} + + ); } if (!isAllowed && renderGuardBanner) { diff --git a/frontend/src/components/permissions/VariablePermissionCan.tsx b/frontend/src/components/permissions/VariablePermissionCan.tsx new file mode 100644 index 000000000..a9e24e3c7 --- /dev/null +++ b/frontend/src/components/permissions/VariablePermissionCan.tsx @@ -0,0 +1,17 @@ +import { OrgPermissionCan } from "./OrgPermissionCan"; +import { ProjectPermissionCan } from "./ProjectPermissionCan"; + +interface PermissionCanProps { + type: "project" | "org"; + I: any; + a: any; + children: (isAllowed: boolean, ability?: any) => React.ReactNode; +} + +export const VariablePermissionCan = ({ type, children, ...props }: PermissionCanProps) => { + if (type === "project") { + return {children}; + } + + return {children}; +}; diff --git a/frontend/src/components/permissions/index.tsx b/frontend/src/components/permissions/index.tsx index c40079a4f..db6ca3296 100644 --- a/frontend/src/components/permissions/index.tsx +++ b/frontend/src/components/permissions/index.tsx @@ -3,3 +3,4 @@ export { GlobPermissionInfo } from "./GlobPermissionInfo"; export { OrgPermissionCan } from "./OrgPermissionCan"; export { PermissionDeniedBanner } from "./PermissionDeniedBanner"; export { ProjectPermissionCan } from "./ProjectPermissionCan"; +export * from "./VariablePermissionCan"; diff --git a/frontend/src/components/project/ProjectOverviewChangeSection.tsx b/frontend/src/components/project/ProjectOverviewChangeSection.tsx index 0f88ec2e9..852253bb3 100644 --- a/frontend/src/components/project/ProjectOverviewChangeSection.tsx +++ b/frontend/src/components/project/ProjectOverviewChangeSection.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, FormControl, Input, TextArea } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { useUpdateProject } from "@app/hooks/api"; const baseFormSchema = z.object({ @@ -37,35 +37,35 @@ type Props = { }; export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync, isPending } = useUpdateProject(); const { handleSubmit, control, reset, watch } = useForm({ resolver: zodResolver(showSlugField ? formSchemaWithSlug : baseFormSchema) }); - const currentSlug = showSlugField ? watch("slug") : currentWorkspace?.slug; + const currentSlug = showSlugField ? watch("slug") : currentProject?.slug; useEffect(() => { - if (currentWorkspace) { + if (currentProject) { reset({ - name: currentWorkspace.name, - description: currentWorkspace.description ?? "", - ...(showSlugField && { slug: currentWorkspace.slug }) + name: currentProject.name, + description: currentProject.description ?? "", + ...(showSlugField && { slug: currentProject.slug }) }); } - }, [currentWorkspace, showSlugField]); + }, [currentProject, showSlugField]); const onFormSubmit = async (data: BaseFormData | FormDataWithSlug) => { try { - if (!currentWorkspace?.id) return; + if (!currentProject?.id) return; await mutateAsync({ - projectID: currentWorkspace.id, + projectId: currentProject.id, newProjectName: data.name, newProjectDescription: data.description, ...(showSlugField && "slug" in data && { - newSlug: data.slug !== currentWorkspace.slug ? data.slug : undefined + newSlug: data.slug !== currentProject.slug ? data.slug : undefined }) }); @@ -105,7 +105,7 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) = variant="outline_bg" size="sm" onClick={() => { - navigator.clipboard.writeText(currentWorkspace?.id || ""); + navigator.clipboard.writeText(currentProject?.id || ""); createNotification({ text: "Copied project ID to clipboard", type: "success" diff --git a/frontend/src/components/projects/NewProjectModal.tsx b/frontend/src/components/projects/NewProjectModal.tsx index 1bbe9f6f1..dcb11d873 100644 --- a/frontend/src/components/projects/NewProjectModal.tsx +++ b/frontend/src/components/projects/NewProjectModal.tsx @@ -34,10 +34,10 @@ import { useUser } from "@app/context"; import { getProjectHomePage, getProjectLottieIcon } from "@app/helpers/project"; -import { useCreateWorkspace, useGetExternalKmsList, useGetUserWorkspaces } from "@app/hooks/api"; +import { useCreateWorkspace, useGetExternalKmsList, useGetUserProjects } from "@app/hooks/api"; import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; +import { ProjectType } from "@app/hooks/api/projects/types"; import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates"; -import { ProjectType } from "@app/hooks/api/workspace/types"; const formSchema = z.object({ name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"), @@ -89,7 +89,7 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { const { permission } = useOrgPermission(); const { user } = useUser(); const createWs = useCreateWorkspace(); - const { refetch: refetchWorkspaces } = useGetUserWorkspaces(); + const { refetch: refetchWorkspaces } = useGetUserProjects(); const { subscription } = useSubscription(); const canReadProjectTemplates = permission.can( diff --git a/frontend/src/components/projects/RequestProjectAccessModal.tsx b/frontend/src/components/projects/RequestProjectAccessModal.tsx new file mode 100644 index 000000000..6d31bf8e6 --- /dev/null +++ b/frontend/src/components/projects/RequestProjectAccessModal.tsx @@ -0,0 +1,88 @@ +import { useForm } from "react-hook-form"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@app/components/v2"; +import { useRequestProjectAccess } from "@app/hooks/api"; +import { Project } from "@app/hooks/api/projects/types"; + +type ContentProps = { + projectId: string; + onComplete: () => void; +}; + +const Content = ({ projectId, onComplete }: ContentProps) => { + const form = useForm<{ note: string }>(); + + const requestProjectAccess = useRequestProjectAccess(); + + const onFormSubmit = ({ note }: { note: string }) => { + if (requestProjectAccess.isPending) return; + requestProjectAccess.mutate( + { + comment: note, + projectId + }, + { + onSuccess: () => { + createNotification({ + type: "success", + title: "Project Access Request Sent", + text: "Project admins will receive an email of your request" + }); + onComplete(); + } + } + ); + }; + + return ( +
+ + + +
+ + + + +
+
+ ); +}; + +type RequestProjectAccessModalProps = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + project?: Project; + onComplete?: () => void; +}; + +export const RequestProjectAccessModal = ({ + isOpen, + onOpenChange, + project, + onComplete +}: RequestProjectAccessModalProps) => { + if (!project) return null; + + return ( + + + { + onOpenChange(false); + if (onComplete) onComplete(); + }} + projectId={project?.id} + /> + + + ); +}; diff --git a/frontend/src/components/projects/index.tsx b/frontend/src/components/projects/index.tsx index 1fb78225d..a2dc754ad 100644 --- a/frontend/src/components/projects/index.tsx +++ b/frontend/src/components/projects/index.tsx @@ -1 +1,2 @@ export { NewProjectModal } from "./NewProjectModal"; +export * from "./RequestProjectAccessModal"; diff --git a/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx b/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx index f5b9a39b3..41f378b94 100644 --- a/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx +++ b/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx @@ -1,18 +1,20 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate, useRouterState } from "@tanstack/react-router"; import { SecretRotationV2Form } from "@app/components/secret-rotations-v2/forms"; +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; import { SecretRotationV2ModalHeader } from "@app/components/secret-rotations-v2/SecretRotationV2ModalHeader"; import { SecretRotationV2Select } from "@app/components/secret-rotations-v2/SecretRotationV2Select"; import { Modal, ModalContent } from "@app/components/v2"; +import { ProjectEnv } from "@app/hooks/api/projects/types"; import { SecretRotation, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2"; -import { WorkspaceEnv } from "@app/hooks/api/workspace/types"; type SharedProps = { secretPath: string; environment?: string; - environments?: WorkspaceEnv[]; + environments?: ProjectEnv[]; }; type Props = { @@ -24,14 +26,23 @@ type ContentProps = { onComplete: (secretRotation: TSecretRotationV2) => void; selectedRotation: SecretRotation | null; setSelectedRotation: (selectedRotation: SecretRotation | null) => void; + initialFormData?: Partial; + onCancel: () => void; } & SharedProps; -const Content = ({ setSelectedRotation, selectedRotation, ...props }: ContentProps) => { +const Content = ({ + setSelectedRotation, + selectedRotation, + initialFormData, + onCancel, + ...props +}: ContentProps) => { if (selectedRotation) { return ( setSelectedRotation(null)} + onCancel={onCancel} type={selectedRotation} + initialFormData={initialFormData} {...props} /> ); @@ -42,12 +53,60 @@ const Content = ({ setSelectedRotation, selectedRotation, ...props }: ContentPro export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }: Props) => { const [selectedRotation, setSelectedRotation] = useState(null); + const [initialFormData, setInitialFormData] = useState>(); + + const { + location: { + search: { connectionId, connectionName, ...search }, + pathname + } + } = useRouterState(); + + const navigate = useNavigate(); + + useEffect(() => { + if (connectionId && connectionName) { + const storedFormData = localStorage.getItem("secretRotationFormData"); + + if (!storedFormData) return; + + let form: Partial = {}; + try { + form = JSON.parse(storedFormData) as TSecretRotationV2Form; + } catch { + return; + } finally { + localStorage.removeItem("secretRotationFormData"); + } + + onOpenChange(true); + + setSelectedRotation(form.type ?? null); + + setInitialFormData({ + ...form, + connection: { id: connectionId, name: connectionName } + }); + + navigate({ + to: pathname, + search + }); + } + }, [connectionId, connectionName]); + + const handleReset = () => { + setSelectedRotation(null); + setInitialFormData(undefined); + }; return ( { - if (!open) setSelectedRotation(null); + if (!open) { + handleReset(); + } onOpenChange(open); }} > @@ -84,9 +143,11 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }: > { - setSelectedRotation(null); + handleReset(); onOpenChange(false); }} + onCancel={handleReset} + initialFormData={initialFormData} selectedRotation={selectedRotation} setSelectedRotation={setSelectedRotation} {...props} diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ConfigurationFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ConfigurationFields.tsx index 1906b743d..e518752c4 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ConfigurationFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ConfigurationFields.tsx @@ -9,14 +9,14 @@ import { IS_ROTATION_DUAL_CREDENTIALS, SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2"; -import { WorkspaceEnv } from "@app/hooks/api/workspace/types"; +import { ProjectEnv } from "@app/hooks/api/projects/types"; import { TSecretRotationV2Form } from "./schemas"; import { SecretRotationV2ConnectionField } from "./SecretRotationV2ConnectionField"; type Props = { isUpdate: boolean; - environments?: WorkspaceEnv[]; + environments?: ProjectEnv[]; }; export const SecretRotationV2ConfigurationFields = ({ isUpdate, environments }: Props) => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ConnectionField.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ConnectionField.tsx index 665ab05a6..c2064071f 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ConnectionField.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ConnectionField.tsx @@ -1,14 +1,17 @@ import { Controller, useFormContext } from "react-hook-form"; +import { SingleValue } from "react-select"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Link } from "@tanstack/react-router"; +import { AppConnectionOption } from "@app/components/app-connections"; import { FilterableSelect, FormControl } from "@app/components/v2"; -import { OrgPermissionSubjects, useOrgPermission } from "@app/context"; -import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types"; +import { ProjectPermissionSub, useProject, useProjectPermission } from "@app/context"; +import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { SECRET_ROTATION_CONNECTION_MAP } from "@app/helpers/secretRotationsV2"; +import { usePopUp } from "@app/hooks"; import { useListAvailableAppConnections } from "@app/hooks/api/appConnections"; +import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components"; import { TSecretRotationV2Form } from "./schemas"; @@ -18,19 +21,26 @@ type Props = { }; export const SecretRotationV2ConnectionField = ({ onChange: callback, isUpdate }: Props) => { - const { permission } = useOrgPermission(); - const { control, watch } = useFormContext(); + const { permission } = useProjectPermission(); + const { control, watch, setValue } = useFormContext(); + + const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const); const rotationType = watch("type"); const app = SECRET_ROTATION_CONNECTION_MAP[rotationType]; - const { data: availableConnections, isPending } = useListAvailableAppConnections(app); + const { currentProject } = useProject(); + + const { data: availableConnections, isPending } = useListAvailableAppConnections( + app, + currentProject.id + ); const connectionName = APP_CONNECTION_MAP[app].name; const canCreateConnection = permission.can( - OrgPermissionAppConnectionActions.Create, - OrgPermissionSubjects.AppConnections + ProjectPermissionAppConnectionActions.Create, + ProjectPermissionSub.AppConnections ); const appName = APP_CONNECTION_MAP[app].name; @@ -66,37 +76,56 @@ export const SecretRotationV2ConnectionField = ({ onChange: callback, isUpdate } { + if ((newValue as SingleValue<{ id: string; name: string }>)?.id === "_create") { + handlePopUpOpen("addConnection"); + onChange(null); + // store for oauth callback connections + localStorage.setItem("secretRotationFormData", JSON.stringify(watch())); + if (callback) callback(); + return; + } + onChange(newValue); if (callback) callback(); }} isLoading={isPending} - options={availableConnections} + options={[ + ...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []), + ...(availableConnections ?? []) + ]} isDisabled={isUpdate} placeholder="Select connection..." getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} + components={{ Option: AppConnectionOption }} /> )} control={control} name="connection" /> - {!isUpdate && availableConnections?.length === 0 && ( + {!isUpdate && !isPending && !availableConnections?.length && !canCreateConnection && (

- {canCreateConnection ? ( - <> - You do not have access to any {appName} Connections. Create one from the{" "} - - App Connections - {" "} - page. - - ) : ( - `You do not have access to any ${appName} Connections. Contact an admin to create one.` - )} + You do not have access to any {appName} Connections. Contact an admin to create one.

)} + { + // remove form storage, not oauth connection + localStorage.removeItem("secretRotationFormData"); + handlePopUpToggle("addConnection", isOpen); + }} + projectType={currentProject.type} + projectId={currentProject.id} + app={app} + onComplete={(connection) => { + if (connection) { + setValue("connection", connection); + } + }} + /> ); }; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx index d931d9d3c..87869e925 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx @@ -11,8 +11,9 @@ import { SecretRotationV2ParametersFields } from "@app/components/secret-rotatio import { SecretRotationV2ReviewFields } from "@app/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields"; import { SecretRotationV2SecretsMappingFields } from "@app/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields"; import { Button } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { IS_ROTATION_DUAL_CREDENTIALS, SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2"; +import { ProjectEnv } from "@app/hooks/api/projects/types"; import { SecretRotation, TSecretRotationV2, @@ -22,7 +23,6 @@ import { useCreateSecretRotationV2, useUpdateSecretRotationV2 } from "@app/hooks/api/secretRotationsV2/mutations"; -import { WorkspaceEnv } from "@app/hooks/api/workspace/types"; import { SecretRotationV2FormSchema, TSecretRotationV2Form } from "./schemas"; @@ -32,8 +32,9 @@ type Props = { onCancel: () => void; secretPath: string; environment?: string; - environments?: WorkspaceEnv[]; + environments?: ProjectEnv[]; secretRotation?: TSecretRotationV2; + initialFormData?: Partial; }; const FORM_TABS: { name: string; key: string; fields: (keyof TSecretRotationV2Form)[] }[] = [ @@ -64,11 +65,12 @@ export const SecretRotationV2Form = ({ environment: envSlug, secretPath, secretRotation, - environments + environments, + initialFormData }: Props) => { const createSecretRotation = useCreateSecretRotationV2(); const updateSecretRotation = useUpdateSecretRotationV2(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { name: rotationType } = SECRET_ROTATION_MAP[type]; const [selectedTabIndex, setSelectedTabIndex] = useState(0); @@ -80,7 +82,7 @@ export const SecretRotationV2Form = ({ defaultValues: secretRotation ? { ...secretRotation, - environment: currentWorkspace?.environments.find((env) => env.slug === envSlug), + environment: currentProject?.environments.find((env) => env.slug === envSlug), secretPath } : { @@ -91,9 +93,10 @@ export const SecretRotationV2Form = ({ hours: 0, minutes: 0 }, - environment: currentWorkspace?.environments.find((env) => env.slug === envSlug), + environment: currentProject?.environments.find((env) => env.slug === envSlug), secretPath, - ...(rotationOption!.template as object) // can't infer type since we don't know which specific type it is + ...((rotationOption?.template as object) ?? {}), // can't infer type since we don't know which specific type it is + ...(initialFormData as object) }, reValidateMode: "onChange" }); @@ -115,7 +118,7 @@ export const SecretRotationV2Form = ({ connectionId: connection.id, environment: environment.slug, - projectId: currentWorkspace.id + projectId: currentProject.id }); try { const rotation = await mutation; diff --git a/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx b/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx index c95baaae5..fb1081d02 100644 --- a/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx +++ b/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx @@ -1,7 +1,9 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate, useRouterState } from "@tanstack/react-router"; +import { TSecretScanningDataSourceForm } from "@app/components/secret-scanning/forms/schemas"; import { Modal, ModalContent } from "@app/components/v2"; import { SecretScanningDataSource, @@ -21,16 +23,19 @@ type ContentProps = { onComplete: (dataSource: TSecretScanningDataSource) => void; selectedDataSource: SecretScanningDataSource | null; setSelectedDataSource: (selectedDataSource: SecretScanningDataSource | null) => void; + initialFormData?: Partial; + onCancel: () => void; }; -const Content = ({ setSelectedDataSource, selectedDataSource, ...props }: ContentProps) => { +const Content = ({ + setSelectedDataSource, + selectedDataSource, + onCancel, + ...props +}: ContentProps) => { if (selectedDataSource) { return ( - setSelectedDataSource(null)} - type={selectedDataSource} - {...props} - /> + ); } @@ -41,12 +46,60 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p const [selectedDataSource, setSelectedDataSource] = useState( null ); + const [initialFormData, setInitialFormData] = useState>(); + + const { + location: { + search: { connectionId, connectionName, ...search }, + pathname + } + } = useRouterState(); + + const navigate = useNavigate(); + + useEffect(() => { + if (connectionId && connectionName) { + const storedFormData = localStorage.getItem("secretScanningDataSourceFormData"); + + if (!storedFormData) return; + + let form: Partial = {}; + try { + form = JSON.parse(storedFormData) as TSecretScanningDataSourceForm; + } catch { + return; + } finally { + localStorage.removeItem("secretScanningDataSourceFormData"); + } + + onOpenChange(true); + + setSelectedDataSource(form.type ?? null); + + setInitialFormData({ + ...form, + connection: { id: connectionId, name: connectionName } + }); + + navigate({ + to: pathname, + search + }); + } + }, [connectionId, connectionName]); + + const resetModal = () => { + setSelectedDataSource(null); + setInitialFormData(undefined); + }; return ( { - if (!open) setSelectedDataSource(null); + if (!open) { + resetModal(); + } onOpenChange(open); }} > @@ -83,11 +136,13 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p > { - setSelectedDataSource(null); + resetModal(); onOpenChange(false); }} + onCancel={resetModal} selectedDataSource={selectedDataSource} setSelectedDataSource={setSelectedDataSource} + initialFormData={initialFormData} {...props} /> diff --git a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConnectionField.tsx b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConnectionField.tsx index 3f995e1d0..2e9e4a2a5 100644 --- a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConnectionField.tsx +++ b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConnectionField.tsx @@ -1,14 +1,17 @@ import { Controller, useFormContext } from "react-hook-form"; +import { SingleValue } from "react-select"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Link } from "@tanstack/react-router"; +import { AppConnectionOption } from "@app/components/app-connections"; import { FilterableSelect, FormControl } from "@app/components/v2"; -import { OrgPermissionSubjects, useOrgPermission } from "@app/context"; -import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types"; +import { ProjectPermissionSub, useProject, useProjectPermission } from "@app/context"; +import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/helpers/secretScanningV2"; +import { usePopUp } from "@app/hooks"; import { useListAvailableAppConnections } from "@app/hooks/api/appConnections"; +import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components"; import { TSecretScanningDataSourceForm } from "./schemas"; @@ -21,19 +24,26 @@ export const SecretScanningDataSourceConnectionField = ({ onChange: callback, isUpdate }: Props) => { - const { permission } = useOrgPermission(); - const { control, watch } = useFormContext(); + const { permission } = useProjectPermission(); + const { control, watch, setValue } = useFormContext(); + + const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const); const dataSourceType = watch("type"); const app = SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSourceType]; - const { data: availableConnections, isPending } = useListAvailableAppConnections(app); + const { currentProject } = useProject(); + + const { data: availableConnections, isPending } = useListAvailableAppConnections( + app, + currentProject.id + ); const connectionName = APP_CONNECTION_MAP[app].name; const canCreateConnection = permission.can( - OrgPermissionAppConnectionActions.Create, - OrgPermissionSubjects.AppConnections + ProjectPermissionAppConnectionActions.Create, + ProjectPermissionSub.AppConnections ); return ( @@ -67,37 +77,57 @@ export const SecretScanningDataSourceConnectionField = ({ { + if ((newValue as SingleValue<{ id: string; name: string }>)?.id === "_create") { + handlePopUpOpen("addConnection"); + onChange(null); + // store for oauth callback connections + localStorage.setItem("secretScanningDataSourceFormData", JSON.stringify(watch())); + if (callback) callback(); + return; + } + onChange(newValue); if (callback) callback(); }} isLoading={isPending} - options={availableConnections} + options={[ + ...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []), + ...(availableConnections ?? []) + ]} isDisabled={isUpdate} placeholder="Select connection..." getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} + components={{ Option: AppConnectionOption }} /> )} control={control} name="connection" /> - {!isUpdate && availableConnections?.length === 0 && ( + {!isUpdate && !isPending && !availableConnections?.length && !canCreateConnection && (

- {canCreateConnection ? ( - <> - You do not have access to any {connectionName} Connections. Create one from the{" "} - - App Connections - {" "} - page. - - ) : ( - `You do not have access to any ${connectionName} Connections. Contact an admin to create one.` - )} + You do not have access to any {connectionName} Connections. Contact an admin to create + one.

)} + { + // remove form storage, not oauth connection + localStorage.removeItem("secretScanningDataSourceFormData"); + handlePopUpToggle("addConnection", isOpen); + }} + projectType={currentProject.type} + projectId={currentProject.id} + app={app} + onComplete={(connection) => { + if (connection) { + setValue("connection", connection); + } + }} + /> ); }; diff --git a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceForm.tsx b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceForm.tsx index b1ea0b559..515a67436 100644 --- a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceForm.tsx +++ b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceForm.tsx @@ -6,7 +6,7 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { Button } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2"; import { SecretScanningDataSource, @@ -27,6 +27,7 @@ type Props = { type: SecretScanningDataSource; onCancel: () => void; dataSource?: TSecretScanningDataSource; + initialFormData?: Partial; }; const FORM_TABS: { name: string; key: string; fields: (keyof TSecretScanningDataSourceForm)[] }[] = @@ -36,10 +37,16 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TSecretScanningData { name: "Review", key: "review", fields: [] } ]; -export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataSource }: Props) => { +export const SecretScanningDataSourceForm = ({ + type, + onComplete, + onCancel, + dataSource, + initialFormData +}: Props) => { const createDataSource = useCreateSecretScanningDataSource(); const updateDataSource = useUpdateSecretScanningDataSource(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { name: sourceType } = SECRET_SCANNING_DATA_SOURCE_MAP[type]; const [selectedTabIndex, setSelectedTabIndex] = useState(0); @@ -48,7 +55,8 @@ export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataS resolver: zodResolver(SecretScanningDataSourceSchema), defaultValues: dataSource ?? { type, - isAutoScanEnabled: true // scott: this may need to be derived from type in the future + isAutoScanEnabled: true, // scott: this may need to be derived from type in the future + ...(initialFormData as object) }, reValidateMode: "onChange" }); @@ -63,7 +71,7 @@ export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataS : createDataSource.mutateAsync({ ...formData, connectionId: connection?.id, - projectId: currentWorkspace.id + projectId: currentProject.id }); try { const source = await mutation; diff --git a/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx b/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx index 5ff72e810..e504389c0 100644 --- a/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx +++ b/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; import { Modal, ModalContent } from "@app/components/v2"; import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; @@ -11,18 +12,21 @@ type Props = { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; selectSync?: SecretSync | null; + initialFormData?: Partial; }; type ContentProps = { onComplete: (secretSync: TSecretSync) => void; selectedSync: SecretSync | null; setSelectedSync: (selectedSync: SecretSync | null) => void; + initialFormData?: Partial; }; -const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) => { +const Content = ({ onComplete, setSelectedSync, selectedSync, initialFormData }: ContentProps) => { if (selectedSync) { return ( setSelectedSync(null)} destination={selectedSync} @@ -33,7 +37,12 @@ const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) => return ; }; -export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...props }: Props) => { +export const CreateSecretSyncModal = ({ + onOpenChange, + selectSync = null, + initialFormData, + ...props +}: Props) => { const [selectedSync, setSelectedSync] = useState(selectSync); useEffect(() => { @@ -67,6 +76,7 @@ export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...prop }} selectedSync={selectedSync} setSelectedSync={setSelectedSync} + initialFormData={initialFormData} />
diff --git a/frontend/src/components/secret-syncs/SecretSyncStatusBadge.tsx b/frontend/src/components/secret-syncs/SecretSyncStatusBadge.tsx index 53b53d5c0..e0d541138 100644 --- a/frontend/src/components/secret-syncs/SecretSyncStatusBadge.tsx +++ b/frontend/src/components/secret-syncs/SecretSyncStatusBadge.tsx @@ -1,6 +1,7 @@ import { faCheck, faExclamationTriangle, + faHourglass, faRotate, IconDefinition } from "@fortawesome/free-solid-svg-icons"; @@ -29,7 +30,11 @@ export const SecretSyncStatusBadge = ({ status }: Props) => { text = "Synced"; icon = faCheck; break; - case SecretSyncStatus.Pending: // no need to differentiate from user perspective + case SecretSyncStatus.Pending: + variant = "primary"; + text = "Queued"; + icon = faHourglass; + break; case SecretSyncStatus.Running: default: variant = "primary"; @@ -42,11 +47,7 @@ export const SecretSyncStatusBadge = ({ status }: Props) => { {text} diff --git a/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx b/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx index 8a1be69c4..4135a9926 100644 --- a/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx +++ b/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx @@ -8,7 +8,7 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Switch } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SecretSync, @@ -29,6 +29,7 @@ type Props = { onComplete: (secretSync: TSecretSync) => void; destination: SecretSync; onCancel: () => void; + initialFormData?: Partial; }; const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [ @@ -39,14 +40,20 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] { name: "Review", key: "review", fields: [] } ]; -export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Props) => { +export const CreateSecretSyncForm = ({ + destination, + onComplete, + onCancel, + initialFormData +}: Props) => { const createSecretSync = useCreateSecretSync(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { name: destinationName } = SECRET_SYNC_MAP[destination]; const [showConfirmation, setShowConfirmation] = useState(false); - const [selectedTabIndex, setSelectedTabIndex] = useState(0); + // scoot: right now we only do this when creating a connection so we know index 1 + const [selectedTabIndex, setSelectedTabIndex] = useState(initialFormData ? 1 : 0); const { syncOption } = useSecretSyncOption(destination); @@ -59,7 +66,8 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop initialSyncBehavior: syncOption?.canImportSecrets ? undefined : SecretSyncInitialSyncBehavior.OverwriteDestination - } + }, + ...initialFormData } as Partial, reValidateMode: "onChange" }); @@ -70,7 +78,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop ...formData, connectionId: connection.id, environment: environment.slug, - projectId: currentWorkspace.id + projectId: currentProject.id }); createNotification({ diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx index 94e587709..62542a570 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx @@ -1,14 +1,17 @@ import { Controller, useFormContext } from "react-hook-form"; +import { SingleValue } from "react-select"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Link } from "@tanstack/react-router"; +import { AppConnectionOption } from "@app/components/app-connections"; import { FilterableSelect, FormControl } from "@app/components/v2"; -import { OrgPermissionSubjects, useOrgPermission } from "@app/context"; -import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types"; +import { ProjectPermissionSub, useProject, useProjectPermission } from "@app/context"; +import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { SECRET_SYNC_CONNECTION_MAP } from "@app/helpers/secretSyncs"; +import { usePopUp } from "@app/hooks"; import { useListAvailableAppConnections } from "@app/hooks/api/appConnections"; +import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components"; import { TSecretSyncForm } from "./schemas"; @@ -17,19 +20,26 @@ type Props = { }; export const SecretSyncConnectionField = ({ onChange: callback }: Props) => { - const { permission } = useOrgPermission(); - const { control, watch } = useFormContext(); + const { permission } = useProjectPermission(); + const { control, watch, setValue } = useFormContext(); + + const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const); const destination = watch("destination"); const app = SECRET_SYNC_CONNECTION_MAP[destination]; - const { data: availableConnections, isPending } = useListAvailableAppConnections(app); + const { currentProject } = useProject(); + + const { data: availableConnections, isPending } = useListAvailableAppConnections( + app, + currentProject.id + ); const connectionName = APP_CONNECTION_MAP[app].name; const canCreateConnection = permission.can( - OrgPermissionAppConnectionActions.Create, - OrgPermissionSubjects.AppConnections + ProjectPermissionAppConnectionActions.Create, + ProjectPermissionSub.AppConnections ); const appName = APP_CONNECTION_MAP[SECRET_SYNC_CONNECTION_MAP[destination]].name; @@ -51,36 +61,55 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => { { + if ((newValue as SingleValue<{ id: string; name: string }>)?.id === "_create") { + handlePopUpOpen("addConnection"); + onChange(null); + // store for oauth callback connections + localStorage.setItem("secretSyncFormData", JSON.stringify(watch())); + if (callback) callback(); + return; + } + onChange(newValue); if (callback) callback(); }} isLoading={isPending} - options={availableConnections} + options={[ + ...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []), + ...(availableConnections ?? []) + ]} placeholder="Select connection..." getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} + components={{ Option: AppConnectionOption }} /> )} control={control} name="connection" /> - {availableConnections?.length === 0 && ( + {!isPending && !availableConnections?.length && !canCreateConnection && (

- {canCreateConnection ? ( - <> - You do not have access to any {appName} Connections. Create one from the{" "} - - App Connections - {" "} - page. - - ) : ( - `You do not have access to any ${appName} Connections. Contact an admin to create one.` - )} + You do not have access to any {appName} Connections. Contact an admin to create one.

)} + { + // remove form storage, not oauth connection + localStorage.removeItem("secretSyncFormData"); + handlePopUpToggle("addConnection", isOpen); + }} + projectType={currentProject.type} + projectId={currentProject.id} + app={app} + onComplete={(connection) => { + if (connection) { + setValue("connection", connection); + } + }} + /> ); }; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncSourceFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncSourceFields.tsx index 850ebbc80..127ac503c 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncSourceFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncSourceFields.tsx @@ -4,7 +4,7 @@ import { subject } from "@casl/ability"; import { FilterableSelect, FormControl } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; -import { useProjectPermission, useWorkspace } from "@app/context"; +import { useProject, useProjectPermission } from "@app/context"; import { ProjectPermissionSecretSyncActions, ProjectPermissionSub @@ -16,7 +16,7 @@ export const SecretSyncSourceFields = () => { const { control, watch, setError, clearErrors } = useFormContext(); const { permission } = useProjectPermission(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const selectedEnvironment = watch("environment"); const selectedSecretPath = watch("secretPath"); @@ -48,7 +48,7 @@ export const SecretSyncSourceFields = () => {

( @@ -56,7 +56,7 @@ export const SecretSyncSourceFields = () => { option?.name} getOptionValue={(option) => option?.id} diff --git a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx index a9542c90c..ada741145 100644 --- a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx +++ b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx @@ -13,7 +13,7 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useGetSecretReferenceTree } from "@app/hooks/api"; import { ApiErrorTypes, TApiErrors, TSecretReferenceTraceNode } from "@app/hooks/api/types"; @@ -89,8 +89,8 @@ export const SecretReferenceNode = ({ }; export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Props) => { - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { data, isPending, isError, error } = useGetSecretReferenceTree({ secretPath, diff --git a/frontend/src/components/tags/CreateTagModal/CreateTagModal.tsx b/frontend/src/components/tags/CreateTagModal/CreateTagModal.tsx index b1773b2e3..d48b7d95f 100644 --- a/frontend/src/components/tags/CreateTagModal/CreateTagModal.tsx +++ b/frontend/src/components/tags/CreateTagModal/CreateTagModal.tsx @@ -15,7 +15,7 @@ import { ModalContent, Tooltip } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateWsTag } from "@app/hooks/api"; import { slugSchema } from "@app/lib/schemas"; @@ -115,11 +115,10 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => { } }); - const { currentWorkspace } = useWorkspace(); - const workspaceId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { mutateAsync: createWsTag } = useCreateWsTag(); - const [showHexInput, setShowHexInput] = useState(false); const selectedTagColor = watch("color", secretTagsColors[0].hex); @@ -130,7 +129,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => { const onFormSubmit = async ({ slug, color }: FormData) => { try { await createWsTag({ - workspaceID: workspaceId, + projectId, tagColor: color, tagSlug: slug }); diff --git a/frontend/src/components/v2/ContentLoader/ContentLoader.tsx b/frontend/src/components/v2/ContentLoader/ContentLoader.tsx index f60668aec..c9a3a1bea 100644 --- a/frontend/src/components/v2/ContentLoader/ContentLoader.tsx +++ b/frontend/src/components/v2/ContentLoader/ContentLoader.tsx @@ -11,9 +11,10 @@ type Props = { text?: string | string[]; frequency?: number; className?: string; + lottieClassName?: string; }; -export const ContentLoader = ({ text, frequency = 2000, className }: Props) => { +export const ContentLoader = ({ text, frequency = 2000, className, lottieClassName }: Props) => { const [pos, setPos] = useState(0); const isTextArray = Array.isArray(text); useEffect(() => { @@ -33,7 +34,11 @@ export const ContentLoader = ({ text, frequency = 2000, className }: Props) => { className )} > - + {text && isTextArray && ( ({ isSelected && "text-mineshaft-200", "px-3 py-2 text-xs hover:cursor-pointer" ), - noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md" + noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md", + loadingMessage: () => "text-mineshaft-400 p-2 rounded-md" }} {...props} /> diff --git a/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx b/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx index 7ab8b0f3c..8d0849a04 100644 --- a/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx +++ b/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx @@ -91,7 +91,7 @@ export const FilterableSelect = ({ ), placeholder: () => `${isMulti ? "py-[0.22rem]" : "leading-7"} text-mineshaft-400 text-sm pl-1`, - input: () => "pl-1", + input: () => `pl-1 ${isMulti ? "py-[0.22rem]" : ""}`, valueContainer: () => `px-1 max-h-[8.2rem] ${ isMulti ? "!overflow-y-auto thin-scrollbar py-1" : "py-[0.1rem]" @@ -114,7 +114,8 @@ export const FilterableSelect = ({ isSelected && "text-mineshaft-200", "rounded px-3 py-2 text-xs hover:cursor-pointer" ), - noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md" + noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md", + loadingMessage: () => "text-mineshaft-400 p-2 rounded-md" }} {...props} /> diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index ee2f196b1..ec09a93eb 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -3,7 +3,7 @@ import { faFolder, faKey, faLayerGroup } from "@fortawesome/free-solid-svg-icons import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Popover from "@radix-ui/react-popover"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useDebounce, useToggle } from "@app/hooks"; import { useGetProjectFolders, useGetProjectSecrets } from "@app/hooks/api"; @@ -76,8 +76,8 @@ export const InfisicalSecretInput = forwardRef( }, ref ) => { - const { currentWorkspace } = useWorkspace(); - const workspaceId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const [debouncedValue] = useDebounce(value, 100); @@ -101,7 +101,7 @@ export const InfisicalSecretInput = forwardRef( let predicate = suggestionSourceValue; if (isDeep) { const [envSlug, ...folderPaths] = suggestionSourceValue.split("."); - const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug); + const isValidEnvSlug = currentProject?.environments.find((e) => e.slug === envSlug); suggestionSourceEnv = isValidEnvSlug ? envSlug : undefined; suggestionSourceSecretPath = `/${folderPaths.slice(0, -1)?.join("/")}`; predicate = folderPaths[folderPaths.length - 1]; @@ -125,7 +125,7 @@ export const InfisicalSecretInput = forwardRef( viewSecretValue: false, environment: suggestionSource.environment || "", secretPath: suggestionSource.secretPath || "", - workspaceId, + projectId, options: { enabled: isPopupOpen } @@ -133,7 +133,7 @@ export const InfisicalSecretInput = forwardRef( const { data: folders } = useGetProjectFolders({ environment: suggestionSource.environment || "", path: suggestionSource.secretPath || "", - projectId: workspaceId, + projectId, options: { enabled: isPopupOpen } @@ -148,7 +148,7 @@ export const InfisicalSecretInput = forwardRef( if (!suggestionSource.isDeep) { // At first level only environments and secrets - (currentWorkspace?.environments || []).forEach(({ name, slug }) => { + (currentProject?.environments || []).forEach(({ name, slug }) => { if (name.toLowerCase().startsWith(predicate)) suggestionsArr.push({ label: name, @@ -176,7 +176,7 @@ export const InfisicalSecretInput = forwardRef( }); }); return suggestionsArr; - }, [secrets, folders, currentWorkspace?.environments, isPopupOpen, suggestionSource.value]); + }, [secrets, folders, currentProject?.environments, isPopupOpen, suggestionSource.value]); const handleSuggestionSelect = (selectIndex?: number) => { const selectedSuggestion = diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index 747af73d5..c320bb0af 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -34,7 +34,9 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport? // akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough // when break is added a line break works properly - return formattedContent.concat(
); + return formattedContent.concat( +
+ ); }; type Props = TextareaHTMLAttributes & { diff --git a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx index a1d4fb2a2..a9c08f7ad 100644 --- a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx +++ b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Popover from "@radix-ui/react-popover"; import { twMerge } from "tailwind-merge"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useDebounce } from "@app/hooks"; import { useGetFoldersByEnv } from "@app/hooks/api"; @@ -35,12 +35,12 @@ export const SecretPathInput = ({ const [highlightedIndex, setHighlightedIndex] = useState(-1); const [debouncedInputValue] = useDebounce(inputValue, 200); - const { currentWorkspace } = useWorkspace(); - const workspaceId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { folderNames: folders } = useGetFoldersByEnv({ path: secretPath, - environments: [environment || currentWorkspace?.environments?.[0].slug || ""], - projectId: workspaceId + environments: [environment || currentProject?.environments?.[0].slug || ""], + projectId }); useEffect(() => { diff --git a/frontend/src/components/v2/Tooltip/Tooltip.tsx b/frontend/src/components/v2/Tooltip/Tooltip.tsx index 12d4397d9..acd0f67f8 100644 --- a/frontend/src/components/v2/Tooltip/Tooltip.tsx +++ b/frontend/src/components/v2/Tooltip/Tooltip.tsx @@ -16,6 +16,7 @@ export type TooltipProps = Omit // just render children if tooltip content is empty content ? ( & AppConnectionSubjectFields) -// ) -// ]; + | [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare] + | [ + OrgPermissionAppConnectionActions, + ( + | OrgPermissionSubjects.AppConnections + | (ForcedSubject & AppConnectionSubjectFields) + ) + ]; export type TOrgPermission = MongoAbility; diff --git a/frontend/src/context/ProjectContext/ProjectContext.tsx b/frontend/src/context/ProjectContext/ProjectContext.tsx new file mode 100644 index 000000000..7256741ef --- /dev/null +++ b/frontend/src/context/ProjectContext/ProjectContext.tsx @@ -0,0 +1,22 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { useParams } from "@tanstack/react-router"; + +import { projectKeys } from "@app/hooks/api"; +import { fetchProjectById } from "@app/hooks/api/projects/queries"; + +export const useProject = () => { + const params = useParams({ + strict: false + }); + if (!params.projectId) { + throw new Error("Missing project id"); + } + + const { data: currentProject } = useSuspenseQuery({ + queryKey: projectKeys.getProjectById(params.projectId), + queryFn: () => fetchProjectById(params.projectId as string), + staleTime: Infinity + }); + + return { currentProject, projectId: currentProject.id }; +}; diff --git a/frontend/src/context/ProjectContext/index.tsx b/frontend/src/context/ProjectContext/index.tsx new file mode 100644 index 000000000..f87ed0a3d --- /dev/null +++ b/frontend/src/context/ProjectContext/index.tsx @@ -0,0 +1 @@ +export { useProject } from "./ProjectContext"; diff --git a/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx b/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx index a824f6fa6..871d5c338 100644 --- a/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx +++ b/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx @@ -22,8 +22,8 @@ export const useProjectPermission = () => { const { data: { permission, membership, assumedPrivilegeDetails } } = useSuspenseQuery({ - queryKey: roleQueryKeys.getUserProjectPermissions({ workspaceId: projectId }), - queryFn: () => fetchUserProjectPermissions({ workspaceId: projectId }), + queryKey: roleQueryKeys.getUserProjectPermissions({ projectId }), + queryFn: () => fetchUserProjectPermissions({ projectId }), staleTime: Infinity, select: (data) => { const rule = unpackRules>>(data.permissions); diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index acfab612f..ad6b5bb0a 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -154,6 +154,14 @@ export enum ProjectPermissionAuditLogsActions { Read = "read" } +export enum ProjectPermissionAppConnectionActions { + Read = "read-app-connections", + Create = "create-app-connections", + Edit = "edit-app-connections", + Delete = "delete-app-connections", + Connect = "connect-app-connections" +} + export enum PermissionConditionOperators { $IN = "$in", $ALL = "$all", @@ -173,6 +181,10 @@ export type IdentityManagementSubjectFields = { identityId: string; }; +export type AppConnectionSubjectFields = { + connectionId: string; +}; + export type ConditionalProjectPermissionSubject = | ProjectPermissionSub.SecretSyncs | ProjectPermissionSub.Secrets @@ -184,7 +196,8 @@ export type ConditionalProjectPermissionSubject = | ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretImports | ProjectPermissionSub.SecretRotation - | ProjectPermissionSub.SecretEvents; + | ProjectPermissionSub.SecretEvents + | ProjectPermissionSub.AppConnections; export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = { [PermissionConditionOperators.$EQ]: "equal to", @@ -263,7 +276,8 @@ export enum ProjectPermissionSub { SecretScanningDataSources = "secret-scanning-data-sources", SecretScanningFindings = "secret-scanning-findings", SecretScanningConfigs = "secret-scanning-configs", - SecretEvents = "secret-events" + SecretEvents = "secret-events", + AppConnections = "app-connections" } export type SecretSubjectFields = { @@ -431,6 +445,13 @@ export type ProjectPermissionSet = | ProjectPermissionSub.SecretEvents | (ForcedSubject & SecretEventSubjectFields) ) + ] + | [ + ProjectPermissionAppConnectionActions, + ( + | ProjectPermissionSub.AppConnections + | (ForcedSubject & AppConnectionSubjectFields) + ) ]; export type TProjectPermission = MongoAbility; diff --git a/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx b/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx index de52e5f18..95da6bb01 100644 --- a/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx +++ b/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx @@ -3,7 +3,7 @@ import { useRouteContext } from "@tanstack/react-router"; import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries"; -export const useSubscription = () => { +export const useSubscription = (refreshCache?: boolean) => { const organizationId = useRouteContext({ from: "/_authenticate/_inject-org-details", select: (el) => el.organizationId @@ -11,7 +11,7 @@ export const useSubscription = () => { const { data: subscription } = useSuspenseQuery({ queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId), - queryFn: () => fetchOrgSubscription(organizationId), + queryFn: () => fetchOrgSubscription(organizationId, refreshCache), staleTime: Infinity }); diff --git a/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx b/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx deleted file mode 100644 index 8bef13b31..000000000 --- a/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useSuspenseQuery } from "@tanstack/react-query"; -import { useParams } from "@tanstack/react-router"; - -import { workspaceKeys } from "@app/hooks/api"; -import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries"; - -export const useWorkspace = () => { - const params = useParams({ - strict: false - }); - if (!params.projectId) { - throw new Error("Missing project id"); - } - - const { data: currentWorkspace } = useSuspenseQuery({ - queryKey: workspaceKeys.getWorkspaceById(params.projectId), - queryFn: () => fetchWorkspaceById(params.projectId as string), - staleTime: Infinity - }); - - return { currentWorkspace }; -}; diff --git a/frontend/src/context/WorkspaceContext/index.tsx b/frontend/src/context/WorkspaceContext/index.tsx deleted file mode 100644 index b0c25d4da..000000000 --- a/frontend/src/context/WorkspaceContext/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { useWorkspace } from "./WorkspaceContext"; diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index bfb504664..fff370269 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -9,6 +9,7 @@ export { OrgPermissionSubjects, useOrgPermission } from "./OrgPermissionContext"; +export { useProject } from "./ProjectContext"; export type { TProjectPermission } from "./ProjectPermissionContext"; export { ProjectPermissionActions, @@ -29,4 +30,3 @@ export { export { useServerConfig } from "./ServerConfigContext"; export { useSubscription } from "./SubscriptionContext"; export { useUser } from "./UserContext"; -export { useWorkspace } from "./WorkspaceContext"; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 88d9bb0d1..99103c794 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -8,6 +8,7 @@ import { faServer, faUser } from "@fortawesome/free-solid-svg-icons"; +import { useRouterState } from "@tanstack/react-router"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { @@ -221,3 +222,11 @@ export const AWS_REGIONS = [ { name: "AWS GovCloud (US-East)", slug: "us-gov-east-1" }, { name: "AWS GovCloud (US-West)", slug: "us-gov-west-1" } ]; + +export const useGetAppConnectionOauthReturnUrl = () => { + const { + location: { pathname } + } = useRouterState(); + + return pathname; +}; diff --git a/frontend/src/helpers/auditLogStreams.ts b/frontend/src/helpers/auditLogStreams.ts new file mode 100644 index 000000000..faa132dd2 --- /dev/null +++ b/frontend/src/helpers/auditLogStreams.ts @@ -0,0 +1,36 @@ +import { faCode, IconDefinition } from "@fortawesome/free-solid-svg-icons"; + +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TAuditLogStream } from "@app/hooks/api/types"; +import { DiscriminativePick } from "@app/types"; + +export const AUDIT_LOG_STREAM_PROVIDER_MAP: Record< + LogProvider, + { name: string; image?: string; icon?: IconDefinition; size?: number } +> = { + [LogProvider.Azure]: { name: "Azure", image: "Microsoft Azure.png", size: 60 }, + [LogProvider.Cribl]: { name: "Cribl", image: "Cribl.png", size: 60 }, + [LogProvider.Custom]: { name: "Custom", icon: faCode }, + [LogProvider.Datadog]: { name: "Datadog", image: "Datadog.png" }, + [LogProvider.Splunk]: { name: "Splunk", image: "Splunk.png", size: 65 } +}; + +// Strictly for showing to the client in the front-end +export function getProviderUrl( + logStream: DiscriminativePick +) { + switch (logStream.provider) { + case LogProvider.Custom: + case LogProvider.Datadog: + case LogProvider.Cribl: + return logStream.credentials.url; + case LogProvider.Splunk: + return `https://${logStream.credentials.hostname}:8088/services/collector/event`; + case LogProvider.Azure: + return `${logStream.credentials.dceUrl}/dataCollectionRules/${logStream.credentials.dcrId}/streams/Custom-${logStream.credentials.cltName}_CL`; + default: + throw new Error( + `Unhandled provider in getProviderUrl: ${(logStream as TAuditLogStream).provider}` + ); + } +} diff --git a/frontend/src/helpers/download.ts b/frontend/src/helpers/download.ts index 50adf4fe2..d1659a956 100644 --- a/frontend/src/helpers/download.ts +++ b/frontend/src/helpers/download.ts @@ -4,3 +4,15 @@ export const downloadTxtFile = (filename: string, content: string) => { const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); FileSaver.saveAs(blob, filename); }; + +export const downloadFile = (content: string, filename: string, mimeType: string = "text/csv") => { + const blob = new Blob([content], { type: mimeType }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); +}; diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index 7c9185556..6227b9696 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -1,6 +1,6 @@ import { apiRequest } from "@app/config/request"; -import { createWorkspace } from "@app/hooks/api/workspace/queries"; -import { ProjectType, WorkspaceEnv } from "@app/hooks/api/workspace/types"; +import { createWorkspace } from "@app/hooks/api/projects/queries"; +import { ProjectEnv, ProjectType } from "@app/hooks/api/projects/types"; const secretsToBeAdded = [ { @@ -47,8 +47,8 @@ export const initProjectHelper = async ({ projectName }: { projectName: string } }); try { - const { data } = await apiRequest.post("/api/v3/secrets/batch/raw", { - workspaceId: project.id, + const { data } = await apiRequest.post("/api/v4/secrets/batch", { + projectId: project.id, environment: "dev", secretPath: "/", secrets: secretsToBeAdded @@ -74,7 +74,7 @@ export const getProjectBaseURL = (type: ProjectType) => { // @ts-expect-error akhilmhdh: will remove this later // eslint-disable-next-line @typescript-eslint/no-unused-vars -export const getProjectHomePage = (type: ProjectType, environments: WorkspaceEnv[]) => { +export const getProjectHomePage = (type: ProjectType, environments: ProjectEnv[]) => { switch (type) { case ProjectType.SecretManager: return "/projects/secret-management/$projectId/overview" as const; diff --git a/frontend/src/hooks/api/accessApproval/queries.tsx b/frontend/src/hooks/api/accessApproval/queries.tsx index 44f260f66..2be6c9535 100644 --- a/frontend/src/hooks/api/accessApproval/queries.tsx +++ b/frontend/src/hooks/api/accessApproval/queries.tsx @@ -16,8 +16,8 @@ import { export const accessApprovalKeys = { getAccessApprovalPolicies: (projectSlug: string) => [{ projectSlug }, "access-approval-policies"] as const, - getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) => - [{ workspaceId, environment }, "access-approval-policy"] as const, + getAccessApprovalPolicyOfABoard: (projectId: string, environment: string) => + [{ projectId, environment }, "access-approval-policy"] as const, getAccessApprovalRequests: ( projectSlug: string, diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index 5063f4fff..be698e469 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -1,7 +1,7 @@ import { EnforcementLevel, PolicyType } from "../policies/enums"; +import { ProjectEnv } from "../projects/types"; import { TProjectPermission } from "../roles/types"; import { ApprovalStatus } from "../secretApprovalRequest/types"; -import { WorkspaceEnv } from "../workspace/types"; export type TAccessApprovalPolicy = { id: string; @@ -9,7 +9,7 @@ export type TAccessApprovalPolicy = { approvals: number; secretPath: string; workspace: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; projectId: string; policyType: PolicyType; approversRequired: boolean; diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index b196f19e7..918a0ecd5 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -1,6 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { Organization } from "@app/hooks/api/organization/types"; import { organizationKeys } from "../organization/queries"; import { User } from "../users/types"; @@ -8,9 +9,12 @@ import { adminQueryKeys, adminStandaloneKeys } from "./queries"; import { RootKeyEncryptionStrategy, TCreateAdminUserDTO, + TCreateOrganizationDTO, TInvalidateCacheDTO, + TResendOrgInviteDTO, TServerConfig, - TUpdateServerConfigDTO + TUpdateServerConfigDTO, + TUsageReportResponse } from "./types"; export const useCreateAdminUser = () => { @@ -193,3 +197,58 @@ export const useInvalidateCache = () => { } }); }; + +export const useServerAdminCreateOrganization = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (opt: TCreateOrganizationDTO) => { + const { data } = await apiRequest.post<{ organization: Organization }>( + "/api/v1/admin/organization-management/organizations", + opt + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: adminQueryKeys.getOrganizations() }); + } + }); +}; + +export const useServerAdminResendOrgInvite = () => { + return useMutation({ + mutationFn: async ({ organizationId, membershipId }: TResendOrgInviteDTO) => { + await apiRequest.post( + `/api/v1/admin/organization-management/organizations/${organizationId}/memberships/${membershipId}/resend-invite` + ); + } + }); +}; + +export const useServerAdminAccessOrg = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (orgId: string) => { + const { data } = await apiRequest.post( + `/api/v1/admin/organization-management/organizations/${orgId}/access` + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations }); + queryClient.invalidateQueries({ queryKey: adminQueryKeys.getOrganizations() }); + } + }); +}; + +export const useGenerateUsageReport = () => { + return useMutation({ + mutationFn: async () => { + const { data } = await apiRequest.post( + "/api/v1/admin/usage-report/generate" + ); + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index 871c7288c..4b10ba3ce 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -1,4 +1,11 @@ -import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query"; +import { + DefaultError, + InfiniteData, + UndefinedInitialDataInfiniteOptions, + useInfiniteQuery, + useQuery, + UseQueryOptions +} from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { Identity } from "@app/hooks/api/identities/types"; @@ -25,8 +32,8 @@ export const adminStandaloneKeys = { export const adminQueryKeys = { serverConfig: () => ["server-config"] as const, getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const, - getOrganizations: (filters: AdminGetOrganizationsFilters) => - [adminStandaloneKeys.getOrganizations, { filters }] as const, + getOrganizations: (filters?: AdminGetOrganizationsFilters) => + [adminStandaloneKeys.getOrganizations, ...(filters ? [{ filters }] : [])] as const, getIdentities: (filters: AdminGetIdentitiesFilters) => [adminStandaloneKeys.getIdentities, { filters }] as const, getAdminSlackConfig: () => ["admin-slack-config"] as const, @@ -83,7 +90,18 @@ export const useGetServerConfig = ({ enabled: options?.enabled ?? true }); -export const useAdminGetUsers = (filters: AdminGetUsersFilters) => { +export const useAdminGetUsers = ( + filters: AdminGetUsersFilters, + options?: Partial< + UndefinedInitialDataInfiniteOptions< + User[], + DefaultError, + InfiniteData, + ReturnType, + number + > + > +) => { return useInfiniteQuery({ initialPageParam: 0, queryKey: adminQueryKeys.getUsers(filters), @@ -101,7 +119,8 @@ export const useAdminGetUsers = (filters: AdminGetUsersFilters) => { return data.users; }, getNextPageParam: (lastPage, pages) => - lastPage.length !== 0 ? pages.length * filters.limit : undefined + lastPage.length !== 0 ? pages.length * filters.limit : undefined, + ...options }); }; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index aca8b5bb3..f8336fcac 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -1,3 +1,5 @@ +import { OrgMembershipStatus } from "@app/hooks/api/organization/types"; + import { Organization } from "../types"; export enum LoginMethod { @@ -20,6 +22,7 @@ export type OrganizationWithProjects = Organization & { lastName: string | null; }; membershipId: string; + status: OrgMembershipStatus; role: string; roleId: string | null; }[]; @@ -53,6 +56,7 @@ export type TServerConfig = { fipsEnabled: boolean; envOverrides?: Record; paramsFolderSecretDetectionEnabled: boolean; + isOfflineUsageReportsEnabled: boolean; }; export type TUpdateServerConfigDTO = { @@ -142,3 +146,19 @@ export interface TGetEnvOverrides { fields: { key: string; value: string; hasEnvEntry: boolean; description?: string }[]; }; } + +export type TUsageReportResponse = { + filename: string; + csvContent: string; + signature: string; +}; + +export type TCreateOrganizationDTO = { + name: string; + inviteAdminEmails: string[]; +}; + +export type TResendOrgInviteDTO = { + organizationId: string; + membershipId: string; +}; diff --git a/frontend/src/hooks/api/appConnections/mutations.tsx b/frontend/src/hooks/api/appConnections/mutations.tsx index bb8831342..cee433462 100644 --- a/frontend/src/hooks/api/appConnections/mutations.tsx +++ b/frontend/src/hooks/api/appConnections/mutations.tsx @@ -20,7 +20,10 @@ export const useCreateAppConnection = () => { return data.appConnection; }, - onSuccess: () => queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() }) + onSuccess: ({ projectId, app }) => { + queryClient.invalidateQueries({ queryKey: appConnectionKeys.list(projectId) }); + queryClient.invalidateQueries({ queryKey: appConnectionKeys.listAvailable(app, projectId) }); + } }); }; @@ -35,9 +38,10 @@ export const useUpdateAppConnection = () => { return data.appConnection; }, - onSuccess: (_, { connectionId, app }) => { - queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() }); - queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) }); + onSuccess: ({ projectId, app }) => { + queryClient.invalidateQueries({ queryKey: appConnectionKeys.list(projectId) }); + queryClient.invalidateQueries({ queryKey: appConnectionKeys.listAvailable(app, projectId) }); + // queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) }); } }); }; @@ -50,9 +54,10 @@ export const useDeleteAppConnection = () => { return data; }, - onSuccess: (_, { connectionId, app }) => { - queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() }); - queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) }); + onSuccess: ({ projectId, app }) => { + queryClient.invalidateQueries({ queryKey: appConnectionKeys.list(projectId) }); + queryClient.invalidateQueries({ queryKey: appConnectionKeys.listAvailable(app, projectId) }); + // queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) }); } }); }; diff --git a/frontend/src/hooks/api/appConnections/queries.tsx b/frontend/src/hooks/api/appConnections/queries.tsx index dbccade88..a27ffdc10 100644 --- a/frontend/src/hooks/api/appConnections/queries.tsx +++ b/frontend/src/hooks/api/appConnections/queries.tsx @@ -5,29 +5,36 @@ import { apiRequest } from "@app/config/request"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { TAppConnection, - TAppConnectionMap, TAppConnectionOptions, TAvailableAppConnection, TAvailableAppConnectionsResponse, - TGetAppConnection, TListAppConnections } from "@app/hooks/api/appConnections/types"; import { TAppConnectionOption, TAppConnectionOptionMap } from "@app/hooks/api/appConnections/types/app-options"; +import { ProjectType } from "@app/hooks/api/projects/types"; export const appConnectionKeys = { all: ["app-connection"] as const, - options: () => [...appConnectionKeys.all, "options"] as const, - list: () => [...appConnectionKeys.all, "list"] as const, - listAvailable: (app: AppConnection) => [...appConnectionKeys.all, app, "list-available"] as const, - listByApp: (app: AppConnection) => [...appConnectionKeys.list(), app], - byId: (app: AppConnection, connectionId: string) => - [...appConnectionKeys.all, app, "by-id", connectionId] as const + options: (projectType?: ProjectType) => + [...appConnectionKeys.all, "options", ...(projectType ? [projectType] : [])] as const, + list: (projectId?: string | null) => + [...appConnectionKeys.all, "list", ...(projectId ? [projectId] : [])] as const, + listAvailable: (app: AppConnection, projectId?: string | null) => + [...appConnectionKeys.all, app, "list-available", ...(projectId ? [projectId] : [])] as const + // scott: may need these in the future but not using now + // getUsage: (app: AppConnection, connectionId: string) => + // [...appConnectionKeys.all, "usage", app, connectionId] as const + // listByApp: (app: AppConnection) => [...appConnectionKeys.list(), app], + // scott: we will need this once we have individual app connection page + // byId: (app: AppConnection, connectionId: string) => + // [...appConnectionKeys.all, app, "by-id", connectionId] as const }; export const useAppConnectionOptions = ( + projectType?: ProjectType, options?: Omit< UseQueryOptions< TAppConnectionOption[], @@ -39,10 +46,11 @@ export const useAppConnectionOptions = ( > ) => { return useQuery({ - queryKey: appConnectionKeys.options(), + queryKey: appConnectionKeys.options(projectType), queryFn: async () => { const { data } = await apiRequest.get( - "/api/v1/app-connections/options" + "/api/v1/app-connections/options", + { params: { projectType } } ); return data.appConnectionOptions; @@ -64,6 +72,7 @@ export const useGetAppConnectionOption = (app: T) => { }; export const useListAppConnections = ( + projectId?: string, options?: Omit< UseQueryOptions< TAppConnection[], @@ -75,10 +84,12 @@ export const useListAppConnections = ( > ) => { return useQuery({ - queryKey: appConnectionKeys.list(), + queryKey: appConnectionKeys.list(projectId), queryFn: async () => { - const { data } = - await apiRequest.get>("/api/v1/app-connections"); + const { data } = await apiRequest.get>( + "/api/v1/app-connections", + { params: { projectId } } + ); return data.appConnections; }, @@ -88,6 +99,7 @@ export const useListAppConnections = ( export const useListAvailableAppConnections = ( app: AppConnection, + projectId: string, options?: Omit< UseQueryOptions< TAvailableAppConnection[], @@ -99,10 +111,11 @@ export const useListAvailableAppConnections = ( > ) => { return useQuery({ - queryKey: appConnectionKeys.listAvailable(app), + queryKey: appConnectionKeys.listAvailable(app, projectId), queryFn: async () => { const { data } = await apiRequest.get( - `/api/v1/app-connections/${app}/available` + `/api/v1/app-connections/${app}/available`, + { params: { projectId } } ); return data.appConnections; @@ -111,53 +124,82 @@ export const useListAvailableAppConnections = ( }); }; -export const useListAppConnectionsByApp = ( - app: T, - options?: Omit< - UseQueryOptions< - TAppConnectionMap[T][], - unknown, - TAppConnectionMap[T][], - ReturnType - >, - "queryKey" | "queryFn" - > -) => { - return useQuery({ - queryKey: appConnectionKeys.listByApp(app), - queryFn: async () => { - const { data } = await apiRequest.get>( - `/api/v1/app-connections/${app}` - ); +// scott: may need these in the future but not using now +// export const useGetAppConnectionUsageById = ( +// app: AppConnection, +// connectionId: string, +// options?: Omit< +// UseQueryOptions< +// AppConnectionUsage, +// unknown, +// AppConnectionUsage, +// ReturnType +// >, +// "queryKey" | "queryFn" +// > +// ) => { +// return useQuery({ +// queryKey: appConnectionKeys.getUsage(app, connectionId), +// queryFn: async () => { +// const { data } = await apiRequest.get( +// `/api/v1/app-connections/${app}/${connectionId}/usage` +// ); +// +// return data; +// }, +// ...options +// }); +// }; - return data.appConnections; - }, - ...options - }); -}; +// scott: may need these in the future but not using now +// export const useListAppConnectionsByApp = ( +// app: T, +// options?: Omit< +// UseQueryOptions< +// TAppConnectionMap[T][], +// unknown, +// TAppConnectionMap[T][], +// ReturnType +// >, +// "queryKey" | "queryFn" +// > +// ) => { +// return useQuery({ +// queryKey: appConnectionKeys.listByApp(app), +// queryFn: async () => { +// const { data } = await apiRequest.get>( +// `/api/v1/app-connections/${app}` +// ); +// +// return data.appConnections; +// }, +// ...options +// }); +// }; -export const useGetAppConnectionById = ( - app: T, - connectionId: string, - options?: Omit< - UseQueryOptions< - TAppConnectionMap[T], - unknown, - TAppConnectionMap[T], - ReturnType - >, - "queryKey" | "queryFn" - > -) => { - return useQuery({ - queryKey: appConnectionKeys.byId(app, connectionId), - queryFn: async () => { - const { data } = await apiRequest.get>( - `/api/v1/app-connections/${app}/${connectionId}` - ); - - return data.appConnection; - }, - ...options - }); -}; +// scott: we will need this once we have individual app connection page +// export const useGetAppConnectionById = ( +// app: T, +// connectionId: string, +// options?: Omit< +// UseQueryOptions< +// TAppConnectionMap[T], +// unknown, +// TAppConnectionMap[T], +// ReturnType +// >, +// "queryKey" | "queryFn" +// > +// ) => { +// return useQuery({ +// queryKey: appConnectionKeys.byId(app, connectionId), +// queryFn: async () => { +// const { data } = await apiRequest.get>( +// `/api/v1/app-connections/${app}/${connectionId}` +// ); +// +// return data.appConnection; +// }, +// ...options +// }); +// }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index e63979a8e..8c1d86ca3 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -116,10 +116,11 @@ export type TAppConnection = | TNetlifyConnection | TOktaConnection; -export type TAvailableAppConnection = Pick; +export type TAvailableAppConnection = Pick; export type TListAppConnections = { appConnections: T[] }; -export type TGetAppConnection = { appConnection: T }; +// scott: we will need this once we have individual app connection page +// export type TGetAppConnection = { appConnection: T }; export type TAppConnectionOptions = { appConnectionOptions: TAppConnectionOption[] }; export type TAppConnectionResponse = { appConnection: TAppConnection }; export type TAvailableAppConnectionsResponse = { appConnections: TAvailableAppConnection[] }; @@ -133,6 +134,7 @@ export type TCreateAppConnectionDTO = Pick< | "description" | "isPlatformManagedCredentials" | "gatewayId" + | "projectId" >; export type TUpdateAppConnectionDTO = Partial< @@ -150,43 +152,60 @@ export type TDeleteAppConnectionDTO = { connectionId: string; }; -export type TAppConnectionMap = { - [AppConnection.AWS]: TAwsConnection; - [AppConnection.GitHub]: TGitHubConnection; - [AppConnection.GitHubRadar]: TGitHubRadarConnection; - [AppConnection.GCP]: TGcpConnection; - [AppConnection.AzureKeyVault]: TAzureKeyVaultConnection; - [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection; - [AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection; - [AppConnection.AzureDevOps]: TAzureDevOpsConnection; - [AppConnection.AzureADCS]: TAzureADCSConnection; - [AppConnection.Databricks]: TDatabricksConnection; - [AppConnection.Humanitec]: THumanitecConnection; - [AppConnection.TerraformCloud]: TTerraformCloudConnection; - [AppConnection.Vercel]: TVercelConnection; - [AppConnection.Postgres]: TPostgresConnection; - [AppConnection.MsSql]: TMsSqlConnection; - [AppConnection.MySql]: TMySqlConnection; - [AppConnection.OracleDB]: TOracleDBConnection; - [AppConnection.Camunda]: TCamundaConnection; - [AppConnection.Windmill]: TWindmillConnection; - [AppConnection.Auth0]: TAuth0Connection; - [AppConnection.HCVault]: THCVaultConnection; - [AppConnection.LDAP]: TLdapConnection; - [AppConnection.TeamCity]: TTeamCityConnection; - [AppConnection.OCI]: TOCIConnection; - [AppConnection.OnePass]: TOnePassConnection; - [AppConnection.Heroku]: THerokuConnection; - [AppConnection.Render]: TRenderConnection; - [AppConnection.Flyio]: TFlyioConnection; - [AppConnection.GitLab]: TGitLabConnection; - [AppConnection.Cloudflare]: TCloudflareConnection; - [AppConnection.Bitbucket]: TBitbucketConnection; - [AppConnection.Zabbix]: TZabbixConnection; - [AppConnection.Railway]: TRailwayConnection; - [AppConnection.Checkly]: TChecklyConnection; - [AppConnection.Supabase]: TSupabaseConnection; - [AppConnection.DigitalOcean]: TDigitalOceanConnection; - [AppConnection.Netlify]: TNetlifyConnection; - [AppConnection.Okta]: TOktaConnection; -}; +// scott: we will need this once we have individual app connection page +// export type TAppConnectionMap = { +// [AppConnection.AWS]: TAwsConnection; +// [AppConnection.GitHub]: TGitHubConnection; +// [AppConnection.GitHubRadar]: TGitHubRadarConnection; +// [AppConnection.GCP]: TGcpConnection; +// [AppConnection.AzureKeyVault]: TAzureKeyVaultConnection; +// [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection; +// [AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection; +// [AppConnection.AzureDevOps]: TAzureDevOpsConnection; +// [AppConnection.AzureADCS]: TAzureADCSConnection; +// [AppConnection.Databricks]: TDatabricksConnection; +// [AppConnection.Humanitec]: THumanitecConnection; +// [AppConnection.TerraformCloud]: TTerraformCloudConnection; +// [AppConnection.Vercel]: TVercelConnection; +// [AppConnection.Postgres]: TPostgresConnection; +// [AppConnection.MsSql]: TMsSqlConnection; +// [AppConnection.MySql]: TMySqlConnection; +// [AppConnection.OracleDB]: TOracleDBConnection; +// [AppConnection.Camunda]: TCamundaConnection; +// [AppConnection.Windmill]: TWindmillConnection; +// [AppConnection.Auth0]: TAuth0Connection; +// [AppConnection.HCVault]: THCVaultConnection; +// [AppConnection.LDAP]: TLdapConnection; +// [AppConnection.TeamCity]: TTeamCityConnection; +// [AppConnection.OCI]: TOCIConnection; +// [AppConnection.OnePass]: TOnePassConnection; +// [AppConnection.Heroku]: THerokuConnection; +// [AppConnection.Render]: TRenderConnection; +// [AppConnection.Flyio]: TFlyioConnection; +// [AppConnection.GitLab]: TGitLabConnection; +// [AppConnection.Cloudflare]: TCloudflareConnection; +// [AppConnection.Bitbucket]: TBitbucketConnection; +// [AppConnection.Zabbix]: TZabbixConnection; +// [AppConnection.Railway]: TRailwayConnection; +// [AppConnection.Checkly]: TChecklyConnection; +// [AppConnection.Supabase]: TSupabaseConnection; +// [AppConnection.DigitalOcean]: TDigitalOceanConnection; +// [AppConnection.Netlify]: TNetlifyConnection; +// [AppConnection.Okta]: TOktaConnection; +// }; + +// scott: we will need this once we have individual app connection page +// export type AppConnectionUsage = { +// projects: Array<{ +// id: string; +// name: string; +// slug: string; +// type: ProjectType; +// resources: { +// secretSyncs: Array<{ id: string; name: string }>; +// externalCas: Array<{ id: string; name: string }>; +// secretRotations: Array<{ id: string; name: string }>; +// dataSources: Array<{ id: string; name: string }>; +// }; +// }>; +// }; diff --git a/frontend/src/hooks/api/appConnections/types/root-connection.ts b/frontend/src/hooks/api/appConnections/types/root-connection.ts index 5b8f5cd02..96198265e 100644 --- a/frontend/src/hooks/api/appConnections/types/root-connection.ts +++ b/frontend/src/hooks/api/appConnections/types/root-connection.ts @@ -1,3 +1,5 @@ +import { ProjectType } from "@app/hooks/api/projects/types"; + export type TRootAppConnection = { id: string; name: string; @@ -8,4 +10,11 @@ export type TRootAppConnection = { updatedAt: string; isPlatformManagedCredentials?: boolean; gatewayId?: string | null; + projectId?: string | null; + project?: { + name: string; + type: ProjectType; + slug: string; + id: string; + } | null; }; diff --git a/frontend/src/hooks/api/assumePrivileges/mutations.tsx b/frontend/src/hooks/api/assumePrivileges/mutations.tsx index 50e4e5b6c..687882fe8 100644 --- a/frontend/src/hooks/api/assumePrivileges/mutations.tsx +++ b/frontend/src/hooks/api/assumePrivileges/mutations.tsx @@ -8,7 +8,7 @@ export const useAssumeProjectPrivileges = () => useMutation({ mutationFn: async ({ projectId, actorId, actorType }: TProjectAssumePrivilegesDTO) => { const { data } = await apiRequest.post<{ message: string }>( - `/api/v1/workspace/${projectId}/assume-privileges`, + `/api/v1/projects/${projectId}/assume-privileges`, { actorId, actorType } ); @@ -20,7 +20,7 @@ export const useRemoveAssumeProjectPrivilege = () => useMutation({ mutationFn: async ({ projectId }: { projectId: string }) => { const { data } = await apiRequest.delete<{ message: string }>( - `/api/v1/workspace/${projectId}/assume-privileges` + `/api/v1/projects/${projectId}/assume-privileges` ); return data; diff --git a/frontend/src/hooks/api/auditLogStreams/enums.ts b/frontend/src/hooks/api/auditLogStreams/enums.ts new file mode 100644 index 000000000..ebef18574 --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/enums.ts @@ -0,0 +1,7 @@ +export enum LogProvider { + Azure = "azure", + Cribl = "cribl", + Custom = "custom", + Datadog = "datadog", + Splunk = "splunk" +} diff --git a/frontend/src/hooks/api/auditLogStreams/index.tsx b/frontend/src/hooks/api/auditLogStreams/index.tsx index 72b1fba1a..0c2adeab0 100644 --- a/frontend/src/hooks/api/auditLogStreams/index.tsx +++ b/frontend/src/hooks/api/auditLogStreams/index.tsx @@ -1,6 +1,2 @@ -export { - useCreateAuditLogStream, - useDeleteAuditLogStream, - useUpdateAuditLogStream -} from "./mutations"; -export { useGetAuditLogStreamDetails, useGetAuditLogStreams } from "./queries"; +export * from "./mutations"; +export * from "./queries"; diff --git a/frontend/src/hooks/api/auditLogStreams/mutations.tsx b/frontend/src/hooks/api/auditLogStreams/mutations.tsx index 1ed93f95b..24253ca63 100644 --- a/frontend/src/hooks/api/auditLogStreams/mutations.tsx +++ b/frontend/src/hooks/api/auditLogStreams/mutations.tsx @@ -12,50 +12,54 @@ import { export const useCreateAuditLogStream = () => { const queryClient = useQueryClient(); - - return useMutation<{ auditLogStream: TAuditLogStream }, object, TCreateAuditLogStreamDTO>({ - mutationFn: async (dto) => { + return useMutation({ + mutationFn: async ({ provider, ...params }: TCreateAuditLogStreamDTO) => { const { data } = await apiRequest.post<{ auditLogStream: TAuditLogStream }>( - "/api/v1/audit-log-streams", - dto + `/api/v1/audit-log-streams/${provider}`, + params ); - return data; + + return data.auditLogStream; }, - onSuccess: (_, { orgId }) => { - queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list(orgId) }); - } + onSuccess: () => queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list() }) }); }; export const useUpdateAuditLogStream = () => { const queryClient = useQueryClient(); - - return useMutation<{ auditLogStream: TAuditLogStream }, object, TUpdateAuditLogStreamDTO>({ - mutationFn: async (dto) => { + return useMutation({ + mutationFn: async ({ auditLogStreamId, provider, ...params }: TUpdateAuditLogStreamDTO) => { const { data } = await apiRequest.patch<{ auditLogStream: TAuditLogStream }>( - `/api/v1/audit-log-streams/${dto.id}`, - dto + `/api/v1/audit-log-streams/${provider}/${auditLogStreamId}`, + params ); - return data; + + return data.auditLogStream; }, - onSuccess: (_, { orgId }) => { - queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list(orgId) }); + onSuccess: (_, { auditLogStreamId, provider }) => { + queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list() }); + queryClient.invalidateQueries({ + queryKey: auditLogStreamKeys.getById(provider, auditLogStreamId) + }); } }); }; export const useDeleteAuditLogStream = () => { const queryClient = useQueryClient(); - - return useMutation<{ auditLogStream: TAuditLogStream }, object, TDeleteAuditLogStreamDTO>({ - mutationFn: async (dto) => { + return useMutation({ + mutationFn: async ({ auditLogStreamId, provider }: TDeleteAuditLogStreamDTO) => { const { data } = await apiRequest.delete<{ auditLogStream: TAuditLogStream }>( - `/api/v1/audit-log-streams/${dto.id}` + `/api/v1/audit-log-streams/${provider}/${auditLogStreamId}` ); - return data; + + return data.auditLogStream; }, - onSuccess: (_, { orgId }) => { - queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list(orgId) }); + onSuccess: (_, { auditLogStreamId, provider }) => { + queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list() }); + queryClient.invalidateQueries({ + queryKey: auditLogStreamKeys.getById(provider, auditLogStreamId) + }); } }); }; diff --git a/frontend/src/hooks/api/auditLogStreams/queries.tsx b/frontend/src/hooks/api/auditLogStreams/queries.tsx index ff7d8b499..97d7d6267 100644 --- a/frontend/src/hooks/api/auditLogStreams/queries.tsx +++ b/frontend/src/hooks/api/auditLogStreams/queries.tsx @@ -1,40 +1,89 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TAuditLogStream } from "./types"; +import { TAuditLogStreamProviderOption } from "./types/provider-options"; +import { LogProvider } from "./enums"; +import { TAuditLogStream, TAuditLogStreamProviderMap } from "./types"; export const auditLogStreamKeys = { - list: (orgId: string) => ["audit-log-stream", { orgId }], - getById: (id: string) => ["audit-log-stream-details", { id }] + all: ["audit-log-stream"] as const, + options: () => [...auditLogStreamKeys.all, "options"] as const, + list: () => [...auditLogStreamKeys.all, "list"] as const, + getById: (provider: string, id: string) => + [...auditLogStreamKeys.all, provider, "get-by-id", id] as const }; -const fetchAuditLogStreams = async () => { - const { data } = await apiRequest.get<{ auditLogStreams: TAuditLogStream[] }>( - "/api/v1/audit-log-streams" - ); +export const useGetAuditLogStreamOptions = ( + options?: Omit< + UseQueryOptions< + TAuditLogStreamProviderOption[], + unknown, + TAuditLogStreamProviderOption[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: auditLogStreamKeys.options(), + queryFn: async () => { + const { data } = await apiRequest.get<{ providerOptions: TAuditLogStreamProviderOption[] }>( + "/api/v1/audit-log-streams/options" + ); - return data.auditLogStreams; -}; - -export const useGetAuditLogStreams = (orgId: string) => - useQuery({ - queryKey: auditLogStreamKeys.list(orgId), - queryFn: () => fetchAuditLogStreams(), - enabled: Boolean(orgId) + return data.providerOptions; + }, + ...options }); - -const fetchAuditLogStreamDetails = async (id: string) => { - const { data } = await apiRequest.get<{ auditLogStream: TAuditLogStream }>( - `/api/v1/audit-log-streams/${id}` - ); - - return data.auditLogStream; }; -export const useGetAuditLogStreamDetails = (id: string) => - useQuery({ - queryKey: auditLogStreamKeys.getById(id), - queryFn: () => fetchAuditLogStreamDetails(id), - enabled: Boolean(id) +export const useListAuditLogStreams = ( + options?: Omit< + UseQueryOptions< + TAuditLogStream[], + unknown, + TAuditLogStream[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: auditLogStreamKeys.list(), + queryFn: async () => { + const { data } = await apiRequest.get<{ auditLogStreams: TAuditLogStream[] }>( + "/api/v1/audit-log-streams" + ); + + return data.auditLogStreams; + }, + ...options }); +}; + +export const useGetAuditLogStreamById = ( + provider: T, + logStreamId: string, + options?: Omit< + UseQueryOptions< + TAuditLogStreamProviderMap[T], + unknown, + TAuditLogStreamProviderMap[T], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: auditLogStreamKeys.getById(provider, logStreamId), + queryFn: async () => { + const { data } = await apiRequest.get<{ auditLogStream: TAuditLogStreamProviderMap[T] }>( + `/api/v1/audit-log-streams/${provider}/${logStreamId}` + ); + + return data.auditLogStream; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/auditLogStreams/types.ts b/frontend/src/hooks/api/auditLogStreams/types.ts deleted file mode 100644 index 8e21a3209..000000000 --- a/frontend/src/hooks/api/auditLogStreams/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -export type LogStreamHeaders = { - key: string; - value: string; -}; - -export type TAuditLogStream = { - id: string; - url: string; - headers?: LogStreamHeaders[]; -}; - -export type TCreateAuditLogStreamDTO = { - url: string; - headers?: LogStreamHeaders[]; - orgId: string; -}; - -export type TUpdateAuditLogStreamDTO = { - id: string; - url?: string; - headers?: LogStreamHeaders[]; - orgId: string; -}; - -export type TDeleteAuditLogStreamDTO = { - id: string; - orgId: string; -}; diff --git a/frontend/src/hooks/api/auditLogStreams/types/index.ts b/frontend/src/hooks/api/auditLogStreams/types/index.ts new file mode 100644 index 000000000..f780510c2 --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/index.ts @@ -0,0 +1,31 @@ +import { LogProvider } from "../enums"; +import { TAzureProviderLogStream } from "./providers/azure-provider"; +import { TCriblProviderLogStream } from "./providers/cribl-provider"; +import { TCustomProviderLogStream } from "./providers/custom-provider"; +import { TDatadogProviderLogStream } from "./providers/datadog-provider"; +import { TSplunkProviderLogStream } from "./providers/splunk-provider"; + +export type TAuditLogStream = + | TCustomProviderLogStream + | TDatadogProviderLogStream + | TSplunkProviderLogStream + | TAzureProviderLogStream + | TCriblProviderLogStream; + +export type TAuditLogStreamProviderMap = { + [LogProvider.Azure]: TAzureProviderLogStream; + [LogProvider.Cribl]: TCriblProviderLogStream; + [LogProvider.Custom]: TCustomProviderLogStream; + [LogProvider.Datadog]: TDatadogProviderLogStream; + [LogProvider.Splunk]: TSplunkProviderLogStream; +}; + +export type TCreateAuditLogStreamDTO = Pick; +export type TUpdateAuditLogStreamDTO = Pick & { + provider: LogProvider; + auditLogStreamId: string; +}; +export type TDeleteAuditLogStreamDTO = { + provider: LogProvider; + auditLogStreamId: string; +}; diff --git a/frontend/src/hooks/api/auditLogStreams/types/provider-options.ts b/frontend/src/hooks/api/auditLogStreams/types/provider-options.ts new file mode 100644 index 000000000..e9b06dfef --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/provider-options.ts @@ -0,0 +1,11 @@ +import { LogProvider } from "../enums"; + +export type TAuditLogStreamProviderOptionBase = { + name: string; +}; + +export type TAuditLogStreamProviderOption = { + [P in keyof typeof LogProvider]: TAuditLogStreamProviderOptionBase & { + provider: (typeof LogProvider)[P]; + }; +}[keyof typeof LogProvider]; diff --git a/frontend/src/hooks/api/auditLogStreams/types/providers/azure-provider.ts b/frontend/src/hooks/api/auditLogStreams/types/providers/azure-provider.ts new file mode 100644 index 000000000..3086fd7cc --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/providers/azure-provider.ts @@ -0,0 +1,14 @@ +import { LogProvider } from "../../enums"; +import { TRootProviderLogStream } from "./root-provider"; + +export type TAzureProviderLogStream = TRootProviderLogStream & { + provider: LogProvider.Azure; + credentials: { + tenantId: string; + clientId: string; + clientSecret: string; + dceUrl: string; + dcrId: string; + cltName: string; + }; +}; diff --git a/frontend/src/hooks/api/auditLogStreams/types/providers/cribl-provider.ts b/frontend/src/hooks/api/auditLogStreams/types/providers/cribl-provider.ts new file mode 100644 index 000000000..aeda0390a --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/providers/cribl-provider.ts @@ -0,0 +1,10 @@ +import { LogProvider } from "../../enums"; +import { TRootProviderLogStream } from "./root-provider"; + +export type TCriblProviderLogStream = TRootProviderLogStream & { + provider: LogProvider.Cribl; + credentials: { + url: string; + token: string; + }; +}; diff --git a/frontend/src/hooks/api/auditLogStreams/types/providers/custom-provider.ts b/frontend/src/hooks/api/auditLogStreams/types/providers/custom-provider.ts new file mode 100644 index 000000000..83ff9e010 --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/providers/custom-provider.ts @@ -0,0 +1,10 @@ +import { LogProvider } from "../../enums"; +import { TRootProviderLogStream } from "./root-provider"; + +export type TCustomProviderLogStream = TRootProviderLogStream & { + provider: LogProvider.Custom; + credentials: { + url: string; + headers: { key: string; value: string }[]; + }; +}; diff --git a/frontend/src/hooks/api/auditLogStreams/types/providers/datadog-provider.ts b/frontend/src/hooks/api/auditLogStreams/types/providers/datadog-provider.ts new file mode 100644 index 000000000..85198bb1a --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/providers/datadog-provider.ts @@ -0,0 +1,10 @@ +import { LogProvider } from "../../enums"; +import { TRootProviderLogStream } from "./root-provider"; + +export type TDatadogProviderLogStream = TRootProviderLogStream & { + provider: LogProvider.Datadog; + credentials: { + url: string; + token: string; + }; +}; diff --git a/frontend/src/hooks/api/auditLogStreams/types/providers/root-provider.ts b/frontend/src/hooks/api/auditLogStreams/types/providers/root-provider.ts new file mode 100644 index 000000000..5ea3137e5 --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/providers/root-provider.ts @@ -0,0 +1,6 @@ +export type TRootProviderLogStream = { + id: string; + orgId: string; + createdAt: string; + updatedAt: string; +}; diff --git a/frontend/src/hooks/api/auditLogStreams/types/providers/splunk-provider.ts b/frontend/src/hooks/api/auditLogStreams/types/providers/splunk-provider.ts new file mode 100644 index 000000000..bf5c676ef --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/providers/splunk-provider.ts @@ -0,0 +1,10 @@ +import { LogProvider } from "../../enums"; +import { TRootProviderLogStream } from "./root-provider"; + +export type TSplunkProviderLogStream = TRootProviderLogStream & { + provider: LogProvider.Splunk; + credentials: { + hostname: string; + token: string; + }; +}; diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 186a8e539..503264fb2 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -20,7 +20,7 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.CREATE_SECRET]: "Create secret", [EventType.UPDATE_SECRET]: "Update secret", [EventType.DELETE_SECRET]: "Delete secret", - [EventType.GET_WORKSPACE_KEY]: "Read project key", + [EventType.GET_PROJECT_KEY]: "Read project key", [EventType.AUTHORIZE_INTEGRATION]: "Authorize integration", [EventType.UPDATE_INTEGRATION_AUTH]: "Update integration auth", [EventType.UNAUTHORIZE_INTEGRATION]: "Unauthorize integration", @@ -45,8 +45,8 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.CREATE_ENVIRONMENT]: "Create environment", [EventType.UPDATE_ENVIRONMENT]: "Update environment", [EventType.DELETE_ENVIRONMENT]: "Delete environment", - [EventType.ADD_WORKSPACE_MEMBER]: "Add member", - [EventType.REMOVE_WORKSPACE_MEMBER]: "Remove member", + [EventType.ADD_PROJECT_MEMBER]: "Add member", + [EventType.REMOVE_PROJECT_MEMBER]: "Remove member", [EventType.CREATE_FOLDER]: "Create folder", [EventType.UPDATE_FOLDER]: "Update folder", [EventType.DELETE_FOLDER]: "Delete folder", @@ -58,8 +58,8 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.CREATE_SECRET_IMPORT]: "Create secret import", [EventType.UPDATE_SECRET_IMPORT]: "Update secret import", [EventType.DELETE_SECRET_IMPORT]: "Delete secret import", - [EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS]: "Update denied permissions", - [EventType.UPDATE_USER_WORKSPACE_ROLE]: "Update user role", + [EventType.UPDATE_USER_PROJECT_DENIED_PERMISSIONS]: "Update denied permissions", + [EventType.UPDATE_USER_PROJECT_ROLE]: "Update user role", [EventType.CREATE_CA]: "Create CA", [EventType.GET_CA]: "Get CA", [EventType.UPDATE_CA]: "Update CA", @@ -132,6 +132,8 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.CREATE_APP_CONNECTION]: "Create App Connection", [EventType.UPDATE_APP_CONNECTION]: "Update App Connection", [EventType.DELETE_APP_CONNECTION]: "Delete App Connection", + [EventType.GET_APP_CONNECTION_USAGE]: "Get App Connection Usage", + [EventType.MIGRATE_APP_CONNECTION]: "Migrate App Connection", [EventType.GET_SECRET_SYNCS]: "List secret syncs", [EventType.GET_SECRET_SYNC]: "Get Secret Sync", [EventType.CREATE_SECRET_SYNC]: "Create Secret Sync", @@ -192,6 +194,7 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.UPDATE_IDENTITY_LDAP_AUTH]: "Updated LDAP Auth for identity", [EventType.GET_IDENTITY_LDAP_AUTH]: "Retrieved LDAP Auth for identity", [EventType.REVOKE_IDENTITY_LDAP_AUTH]: "Revoked LDAP Auth for identity", + [EventType.CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS]: "Clear LDAP Auth lockouts", [EventType.SECRET_SCANNING_DATA_SOURCE_LIST]: "List Secret Scanning Data Sources", [EventType.SECRET_SCANNING_DATA_SOURCE_CREATE]: "Create Secret Scanning Data Source", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 4fe8948dd..bfa983188 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -26,7 +26,7 @@ export enum EventType { CREATE_SECRET = "create-secret", UPDATE_SECRET = "update-secret", DELETE_SECRET = "delete-secret", - GET_WORKSPACE_KEY = "get-workspace-key", + GET_PROJECT_KEY = "get-project-key", AUTHORIZE_INTEGRATION = "authorize-integration", UPDATE_INTEGRATION_AUTH = "update-integration-auth", UNAUTHORIZE_INTEGRATION = "unauthorize-integration", @@ -54,12 +54,13 @@ export enum EventType { UPDATE_IDENTITY_LDAP_AUTH = "update-identity-ldap-auth", GET_IDENTITY_LDAP_AUTH = "get-identity-ldap-auth", REVOKE_IDENTITY_LDAP_AUTH = "revoke-identity-ldap-auth", + CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS = "clear-identity-ldap-auth-lockouts", CREATE_ENVIRONMENT = "create-environment", UPDATE_ENVIRONMENT = "update-environment", DELETE_ENVIRONMENT = "delete-environment", - ADD_WORKSPACE_MEMBER = "add-workspace-member", - REMOVE_WORKSPACE_MEMBER = "remove-workspace-member", + ADD_PROJECT_MEMBER = "add-project-member", + REMOVE_PROJECT_MEMBER = "remove-project-member", CREATE_FOLDER = "create-folder", UPDATE_FOLDER = "update-folder", DELETE_FOLDER = "delete-folder", @@ -71,8 +72,8 @@ export enum EventType { CREATE_SECRET_IMPORT = "create-secret-import", UPDATE_SECRET_IMPORT = "update-secret-import", DELETE_SECRET_IMPORT = "delete-secret-import", - UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role", - UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions", + UPDATE_USER_PROJECT_ROLE = "update-user-project-role", + UPDATE_USER_PROJECT_DENIED_PERMISSIONS = "update-user-project-denied-permissions", CREATE_CA = "create-certificate-authority", GET_CA = "get-certificate-authority", UPDATE_CA = "update-certificate-authority", @@ -140,6 +141,8 @@ export enum EventType { CREATE_APP_CONNECTION = "create-app-connection", UPDATE_APP_CONNECTION = "update-app-connection", DELETE_APP_CONNECTION = "delete-app-connection", + GET_APP_CONNECTION_USAGE = "get-app-connection-usage", + MIGRATE_APP_CONNECTION = "migrate-app-connection", GET_SECRET_SYNCS = "get-secret-syncs", GET_SECRET_SYNC = "get-secret-sync", CREATE_SECRET_SYNC = "create-secret-sync", diff --git a/frontend/src/hooks/api/auditLogs/queries.tsx b/frontend/src/hooks/api/auditLogs/queries.tsx index 8cb438c92..5e25d8104 100644 --- a/frontend/src/hooks/api/auditLogs/queries.tsx +++ b/frontend/src/hooks/api/auditLogs/queries.tsx @@ -7,10 +7,10 @@ import { TReactQueryOptions } from "@app/types/reactQuery"; import { Actor, AuditLog, TGetAuditLogsFilter } from "./types"; export const auditLogKeys = { - getAuditLogs: (workspaceId: string | null, filters: TGetAuditLogsFilter) => - [{ workspaceId, filters }, "audit-logs"] as const, - getAuditLogActorFilterOpts: (workspaceId: string) => - [{ workspaceId }, "audit-log-actor-filters"] as const + getAuditLogs: (projectId: string | null, filters: TGetAuditLogsFilter) => + [{ projectId, filters }, "audit-logs"] as const, + getAuditLogActorFilterOpts: (projectId: string) => + [{ projectId }, "audit-log-actor-filters"] as const }; export const useGetAuditLogs = ( @@ -56,12 +56,12 @@ export const useGetAuditLogs = ( }); }; -export const useGetAuditLogActorFilterOpts = (workspaceId: string) => { +export const useGetAuditLogActorFilterOpts = (projectId: string) => { return useQuery({ - queryKey: auditLogKeys.getAuditLogActorFilterOpts(workspaceId), + queryKey: auditLogKeys.getAuditLogActorFilterOpts(projectId), queryFn: async () => { const { data } = await apiRequest.get<{ actors: Actor[] }>( - `/api/v1/workspace/${workspaceId}/audit-logs/filters/actors` + `/api/v1/projects/${projectId}/audit-logs/filters/actors` ); return data.actors; } diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 1ca819e60..1bb321fa2 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -129,7 +129,7 @@ interface DeleteSecretEvent { } interface GetWorkspaceKeyEvent { - type: EventType.GET_WORKSPACE_KEY; + type: EventType.GET_PROJECT_KEY; metadata: { keyId: string; }; @@ -361,7 +361,7 @@ interface DeleteEnvironmentEvent { } interface AddWorkspaceMemberEvent { - type: EventType.ADD_WORKSPACE_MEMBER; + type: EventType.ADD_PROJECT_MEMBER; metadata: { userId: string; email: string; @@ -369,7 +369,7 @@ interface AddWorkspaceMemberEvent { } interface RemoveWorkspaceMemberEvent { - type: EventType.REMOVE_WORKSPACE_MEMBER; + type: EventType.REMOVE_PROJECT_MEMBER; metadata: { userId: string; email: string; @@ -500,7 +500,7 @@ interface DeleteSecretImportEvent { } interface UpdateUserRole { - type: EventType.UPDATE_USER_WORKSPACE_ROLE; + type: EventType.UPDATE_USER_PROJECT_ROLE; metadata: { userId: string; email: string; @@ -510,7 +510,7 @@ interface UpdateUserRole { } interface UpdateUserDeniedPermissions { - type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS; + type: EventType.UPDATE_USER_PROJECT_DENIED_PERMISSIONS; metadata: { userId: string; email: string; @@ -874,6 +874,13 @@ interface IntegrationSyncedEvent { }; } +interface ClearIdentityLdapAuthLockoutsEvent { + type: EventType.CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS; + metadata: { + identityId: string; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -958,7 +965,8 @@ export type Event = | GetCertificateTemplateEstConfig | UpdateProjectWorkflowIntegrationConfig | GetProjectWorkflowIntegrationConfig - | IntegrationSyncedEvent; + | IntegrationSyncedEvent + | ClearIdentityLdapAuthLockoutsEvent; export type AuditLog = { id: string; diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index cb7e3f64c..d41a4d115 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -5,8 +5,8 @@ import { apiRequest } from "@app/config/request"; import { SessionStorageKeys } from "@app/const"; import { organizationKeys } from "../organization/queries"; +import { projectKeys } from "../projects"; import { setAuthToken } from "../reactQuery"; -import { workspaceKeys } from "../workspace"; import { CompleteAccountDTO, CompleteAccountSignupDTO, @@ -101,7 +101,7 @@ export const useSelectOrganization = () => { }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: [organizationKeys.getUserOrganizations, workspaceKeys.getAllUserWorkspace] + queryKey: [organizationKeys.getUserOrganizations, projectKeys.getAllUserProjects] }); } }); diff --git a/frontend/src/hooks/api/ca/mutations.tsx b/frontend/src/hooks/api/ca/mutations.tsx index f6204d4fc..a14a0244b 100644 --- a/frontend/src/hooks/api/ca/mutations.tsx +++ b/frontend/src/hooks/api/ca/mutations.tsx @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { workspaceKeys } from "../workspace"; +import { projectKeys } from "../projects"; import { CaType } from "./enums"; import { caKeys } from "./queries"; import { @@ -118,8 +118,8 @@ export const useImportCaCertificate = (projectId: string) => { ); return data; }, - onSuccess: (_, { caId, projectSlug }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceCas({ projectSlug }) }); + onSuccess: (_, { caId }) => { + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectCas({ projectId }) }); queryClient.invalidateQueries({ queryKey: caKeys.getCaCerts(caId) }); queryClient.invalidateQueries({ queryKey: caKeys.getCaCert(caId) }); queryClient.invalidateQueries({ @@ -142,7 +142,7 @@ export const useCreateCertificate = () => { }, onSuccess: (_, { projectSlug }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug) + queryKey: projectKeys.forProjectCertificates(projectSlug) }); } }); @@ -158,8 +158,8 @@ export const useRenewCa = () => { ); return data; }, - onSuccess: (_, { caId, projectSlug }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceCas({ projectSlug }) }); + onSuccess: ({ projectId }, { caId }) => { + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectCas({ projectId }) }); queryClient.invalidateQueries({ queryKey: caKeys.getCaById(caId) }); queryClient.invalidateQueries({ queryKey: caKeys.getCaCert(caId) }); queryClient.invalidateQueries({ queryKey: caKeys.getCaCerts(caId) }); diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index 336688ca4..0443dd5e9 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -182,4 +182,5 @@ export type TRenewCaResponse = { certificate: string; certificateChain: string; serialNumber: string; + projectId: string; }; diff --git a/frontend/src/hooks/api/certificateTemplates/mutations.tsx b/frontend/src/hooks/api/certificateTemplates/mutations.tsx index d1069e7e6..24a7d0e5f 100644 --- a/frontend/src/hooks/api/certificateTemplates/mutations.tsx +++ b/frontend/src/hooks/api/certificateTemplates/mutations.tsx @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { caKeys } from "../ca/queries"; -import { workspaceKeys } from "../workspace"; +import { projectKeys } from "../projects"; import { certTemplateKeys } from "./queries"; import { TCertificateTemplate, @@ -29,7 +29,7 @@ export const useCreateCertTemplate = () => { }, onSuccess: ({ caId }, { projectId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceCertificateTemplates(projectId) + queryKey: projectKeys.getProjectCertificateTemplates(projectId) }); queryClient.invalidateQueries({ queryKey: caKeys.getCaCertTemplates(caId) }); } @@ -49,7 +49,7 @@ export const useUpdateCertTemplate = () => { }, onSuccess: ({ caId }, { projectId, id }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceCertificateTemplates(projectId) + queryKey: projectKeys.getProjectCertificateTemplates(projectId) }); queryClient.invalidateQueries({ queryKey: certTemplateKeys.getCertTemplateById(id) }); queryClient.invalidateQueries({ queryKey: caKeys.getCaCertTemplates(caId) }); @@ -68,7 +68,7 @@ export const useDeleteCertTemplate = () => { }, onSuccess: ({ caId }, { projectId, id }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceCertificateTemplates(projectId) + queryKey: projectKeys.getProjectCertificateTemplates(projectId) }); queryClient.invalidateQueries({ queryKey: certTemplateKeys.getCertTemplateById(id) }); queryClient.invalidateQueries({ queryKey: caKeys.getCaCertTemplates(caId) }); diff --git a/frontend/src/hooks/api/certificates/mutations.tsx b/frontend/src/hooks/api/certificates/mutations.tsx index 12f8e834b..77a3dab72 100644 --- a/frontend/src/hooks/api/certificates/mutations.tsx +++ b/frontend/src/hooks/api/certificates/mutations.tsx @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { pkiSubscriberKeys } from "../pkiSubscriber/queries"; -import { workspaceKeys } from "../workspace"; +import { projectKeys } from "../projects"; import { TCertificate, TDeleteCertDTO, @@ -25,7 +25,7 @@ export const useDeleteCert = () => { }, onSuccess: (_, { projectSlug }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug) + queryKey: projectKeys.forProjectCertificates(projectSlug) }); } }); @@ -47,7 +47,7 @@ export const useRevokeCert = () => { }, onSuccess: (_, { projectSlug }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug) + queryKey: projectKeys.forProjectCertificates(projectSlug) }); queryClient.invalidateQueries({ queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates() @@ -68,7 +68,7 @@ export const useImportCertificate = () => { }, onSuccess: (_, { projectSlug }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug) + queryKey: projectKeys.forProjectCertificates(projectSlug) }); } }); diff --git a/frontend/src/hooks/api/folderCommits/queries.tsx b/frontend/src/hooks/api/folderCommits/queries.tsx index 1bca7ce9a..4d37a4f7b 100644 --- a/frontend/src/hooks/api/folderCommits/queries.tsx +++ b/frontend/src/hooks/api/folderCommits/queries.tsx @@ -7,27 +7,27 @@ import { Commit, CommitHistoryItem, CommitWithChanges, RollbackPreview } from ". export const commitKeys = { count: ({ - workspaceId, + projectId, environment, directory }: { - workspaceId: string; + projectId: string; environment: string; directory?: string; - }) => [{ workspaceId, environment, directory }, "folder-commits-count"] as const, + }) => [{ projectId, environment, directory }, "folder-commits-count"] as const, history: ({ - workspaceId, + projectId, environment, directory }: { - workspaceId: string; + projectId: string; environment: string; directory?: string; - }) => [{ workspaceId, environment, directory }, "folder-commits"] as const, + }) => [{ projectId, environment, directory }, "folder-commits"] as const, - details: ({ workspaceId, commitId }: { workspaceId: string; commitId: string }) => - [{ workspaceId, commitId }, "commit-details"] as const, + details: ({ projectId, commitId }: { projectId: string; commitId: string }) => + [{ projectId, commitId }, "commit-details"] as const, rollbackPreview: ({ folderId, @@ -45,11 +45,11 @@ export const commitKeys = { }; const fetchFolderCommitsCount = async ({ - workspaceId, + projectId, environment, directory }: { - workspaceId: string; + projectId: string; environment: string; directory?: string; }) => { @@ -59,7 +59,7 @@ const fetchFolderCommitsCount = async ({ params: { environment, path: directory, - projectId: workspaceId + projectId } } ); @@ -67,7 +67,7 @@ const fetchFolderCommitsCount = async ({ }; const fetchFolderCommitHistory = async ( - workspaceId: string, + projectId: string, environment: string, directory: string, offset: number = 0, @@ -87,7 +87,7 @@ const fetchFolderCommitHistory = async ( params: { environment, path: directory, - projectId: workspaceId, + projectId, offset, limit, search, @@ -97,12 +97,12 @@ const fetchFolderCommitHistory = async ( return res.data; }; -export const fetchCommitDetails = async (workspaceId: string, commitId: string) => { +export const fetchCommitDetails = async (projectId: string, commitId: string) => { const { data } = await apiRequest.get( `/api/v1/pit/commits/${commitId}/changes`, { params: { - projectId: workspaceId + projectId } } ); @@ -113,7 +113,7 @@ export const fetchRollbackPreview = async ( folderId: string, commitId: string, envSlug: string, - workspaceId: string, + projectId: string, deepRollback: boolean, secretPath: string ): Promise => { @@ -125,7 +125,7 @@ export const fetchRollbackPreview = async ( environment: envSlug, deepRollback, secretPath, - projectId: workspaceId + projectId } } ); @@ -135,7 +135,7 @@ export const fetchRollbackPreview = async ( const fetchRollback = async ( folderId: string, commitId: string, - workspaceId: string, + projectId: string, deepRollback: boolean, message?: string, envSlug?: string @@ -147,17 +147,17 @@ const fetchRollback = async ( deepRollback, message, environment: envSlug, - projectId: workspaceId + projectId } ); return data; }; -const fetchRevert = async (commitId: string, workspaceId: string) => { +const fetchRevert = async (commitId: string, projectId: string) => { const { data } = await apiRequest.post<{ success: boolean; message: string }>( `/api/v1/pit/commits/${commitId}/revert`, { - projectId: workspaceId + projectId } ); return data; @@ -180,9 +180,9 @@ export const useCommitRevert = ({ onSuccess: () => { queryClient.invalidateQueries({ queryKey: [ - commitKeys.details({ workspaceId: projectId, commitId }), - commitKeys.history({ workspaceId: projectId, environment, directory }), - commitKeys.count({ workspaceId: projectId, environment, directory }) + commitKeys.details({ projectId, commitId }), + commitKeys.history({ projectId, environment, directory }), + commitKeys.count({ projectId, environment, directory }) ] }); } @@ -190,7 +190,7 @@ export const useCommitRevert = ({ }; export const useCommitRollback = ({ - workspaceId, + projectId, commitId, folderId, deepRollback, @@ -198,7 +198,7 @@ export const useCommitRollback = ({ directory, envSlug }: { - workspaceId: string; + projectId: string; commitId: string; folderId: string; deepRollback: boolean; @@ -209,13 +209,13 @@ export const useCommitRollback = ({ const queryClient = useQueryClient(); return useMutation({ mutationFn: (message: string) => - fetchRollback(folderId, commitId, workspaceId, deepRollback, message, envSlug), + fetchRollback(folderId, commitId, projectId, deepRollback, message, envSlug), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [ - commitKeys.details({ workspaceId, commitId }), - commitKeys.history({ workspaceId, environment, directory }), - commitKeys.count({ workspaceId, environment, directory }) + commitKeys.details({ projectId, commitId }), + commitKeys.history({ projectId, environment, directory }), + commitKeys.count({ projectId, environment, directory }) ] }); } @@ -223,31 +223,31 @@ export const useCommitRollback = ({ }; export const useGetFolderCommitsCount = ({ - workspaceId, + projectId, environment, directory, isPaused }: { - workspaceId: string; + projectId: string; environment: string; directory: string; isPaused?: boolean; }) => useQuery({ - enabled: Boolean(workspaceId && environment) && !isPaused, - queryKey: commitKeys.count({ workspaceId, environment, directory }), - queryFn: () => fetchFolderCommitsCount({ workspaceId, environment, directory }) + enabled: Boolean(projectId && environment) && !isPaused, + queryKey: commitKeys.count({ projectId, environment, directory }), + queryFn: () => fetchFolderCommitsCount({ projectId, environment, directory }) }); export const useGetFolderCommitHistory = ({ - workspaceId, + projectId, environment, directory, limit = 20, search, sort = "desc" }: { - workspaceId: string; + projectId: string; environment: string; directory: string; limit?: number; @@ -256,10 +256,10 @@ export const useGetFolderCommitHistory = ({ }) => { return useInfiniteQuery({ initialPageParam: 0, - queryKey: [commitKeys.history({ workspaceId, environment, directory }), limit, search, sort], + queryKey: [commitKeys.history({ projectId, environment, directory }), limit, search, sort], queryFn: ({ pageParam }) => - fetchFolderCommitHistory(workspaceId, environment, directory, pageParam, limit, search, sort), - enabled: Boolean(workspaceId && environment), + fetchFolderCommitHistory(projectId, environment, directory, pageParam, limit, search, sort), + enabled: Boolean(projectId && environment), select: (data) => { return (data?.pages ?? []) ?.map((page) => page.commits) @@ -280,11 +280,11 @@ export const useGetFolderCommitHistory = ({ }); }; -export const useGetCommitDetails = (workspaceId: string, commitId: string) => { +export const useGetCommitDetails = (projectId: string, commitId: string) => { return useQuery({ - queryKey: commitKeys.details({ workspaceId, commitId }), - queryFn: () => fetchCommitDetails(workspaceId, commitId), - enabled: Boolean(workspaceId) && Boolean(commitId) + queryKey: commitKeys.details({ projectId, commitId }), + queryFn: () => fetchCommitDetails(projectId, commitId), + enabled: Boolean(projectId) && Boolean(commitId) }); }; diff --git a/frontend/src/hooks/api/gateways-v2/index.tsx b/frontend/src/hooks/api/gateways-v2/index.tsx new file mode 100644 index 000000000..f8dd99d03 --- /dev/null +++ b/frontend/src/hooks/api/gateways-v2/index.tsx @@ -0,0 +1 @@ +export * from "./mutations"; diff --git a/frontend/src/hooks/api/gateways-v2/mutations.tsx b/frontend/src/hooks/api/gateways-v2/mutations.tsx new file mode 100644 index 000000000..c3bf8bd1b --- /dev/null +++ b/frontend/src/hooks/api/gateways-v2/mutations.tsx @@ -0,0 +1,17 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { gatewaysQueryKeys } from "../gateways/queries"; + +export const useDeleteGatewayV2ById = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => { + return apiRequest.delete(`/api/v2/gateways/${id}`); + }, + onSuccess: () => { + queryClient.invalidateQueries(gatewaysQueryKeys.list()); + } + }); +}; diff --git a/frontend/src/hooks/api/gateways-v2/types.ts b/frontend/src/hooks/api/gateways-v2/types.ts new file mode 100644 index 000000000..69bc21702 --- /dev/null +++ b/frontend/src/hooks/api/gateways-v2/types.ts @@ -0,0 +1,12 @@ +export type TGatewayV2 = { + id: string; + identityId: string; + name: string; + createdAt: string; + updatedAt: string; + heartbeat: string; + identity: { + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/gateways/queries.tsx b/frontend/src/hooks/api/gateways/queries.tsx index bb05b17a4..ef4dafb75 100644 --- a/frontend/src/hooks/api/gateways/queries.tsx +++ b/frontend/src/hooks/api/gateways/queries.tsx @@ -2,6 +2,7 @@ import { queryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { TGatewayV2 } from "../gateways-v2/types"; import { TGateway } from "./types"; export const gatewaysQueryKeys = { @@ -11,8 +12,21 @@ export const gatewaysQueryKeys = { queryOptions({ queryKey: gatewaysQueryKeys.listKey(), queryFn: async () => { - const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways"); - return data.gateways; + const [{ data }, { data: dataV2 }] = await Promise.all([ + apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways"), + apiRequest.get("/api/v2/gateways") + ]); + + return [ + ...data.gateways.map((g) => ({ + ...g, + isV1: true + })), + ...dataV2.map((g) => ({ + ...g, + isV1: false + })) + ]; } }) }; diff --git a/frontend/src/hooks/api/groups/queries.tsx b/frontend/src/hooks/api/groups/queries.tsx index 6fbe0ec13..ca524066f 100644 --- a/frontend/src/hooks/api/groups/queries.tsx +++ b/frontend/src/hooks/api/groups/queries.tsx @@ -138,7 +138,7 @@ export const useListProjectGroupUsers = ({ }); const { data } = await apiRequest.get<{ users: TGroupUser[]; totalCount: number }>( - `/api/v2/workspace/${projectId}/groups/${id}/users`, + `/api/v1/projects/${projectId}/groups/${id}/users`, { params } diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 5189f187c..4ada1fb9b 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -18,6 +18,7 @@ import { AddIdentityTlsCertAuthDTO, AddIdentityTokenAuthDTO, AddIdentityUniversalAuthDTO, + ClearIdentityLdapAuthLockoutsDTO, ClearIdentityUniversalAuthLockoutsDTO, ClientSecretData, CreateIdentityDTO, @@ -1432,7 +1433,11 @@ export const useAddIdentityLdapAuth = () => { accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, - accessTokenTrustedIps + accessTokenTrustedIps, + lockoutEnabled, + lockoutThreshold, + lockoutDurationSeconds, + lockoutCounterResetSeconds }) => { const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>( `/api/v1/auth/ldap-auth/identities/${identityId}`, @@ -1448,7 +1453,11 @@ export const useAddIdentityLdapAuth = () => { accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, - accessTokenTrustedIps + accessTokenTrustedIps, + lockoutEnabled, + lockoutThreshold, + lockoutDurationSeconds, + lockoutCounterResetSeconds } ); return data.identityLdapAuth; @@ -1481,7 +1490,11 @@ export const useUpdateIdentityLdapAuth = () => { accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, - accessTokenTrustedIps + accessTokenTrustedIps, + lockoutEnabled, + lockoutThreshold, + lockoutDurationSeconds, + lockoutCounterResetSeconds }) => { const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>( `/api/v1/auth/ldap-auth/identities/${identityId}`, @@ -1497,7 +1510,11 @@ export const useUpdateIdentityLdapAuth = () => { accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, - accessTokenTrustedIps + accessTokenTrustedIps, + lockoutEnabled, + lockoutThreshold, + lockoutDurationSeconds, + lockoutCounterResetSeconds } ); return data.identityLdapAuth; @@ -1532,3 +1549,22 @@ export const useDeleteIdentityLdapAuth = () => { } }); }; + +export const useClearIdentityLdapAuthLockouts = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { + data: { deleted } + } = await apiRequest.post<{ deleted: number }>( + `/api/v1/auth/ldap-auth/identities/${identityId}/clear-lockouts` + ); + return deleted; + }, + onSuccess: (_, { identityId }) => { + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityLdapAuth(identityId) + }); + } + }); +}; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 36f9eae4e..99e377143 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -1,7 +1,7 @@ import { OrderByDirection } from "../generic/types"; import { OrgIdentityOrderBy } from "../organization/types"; +import { Project, ProjectUserMembershipTemporaryMode } from "../projects/types"; import { TOrgRole } from "../roles/types"; -import { ProjectUserMembershipTemporaryMode, Workspace } from "../workspace/types"; import { IdentityAuthMethod, IdentityJwtConfigurationType } from "./enums"; export type IdentityTrustedIp = { @@ -54,7 +54,7 @@ export type IdentityMembershipOrg = { export type IdentityMembership = { id: string; identity: Identity; - project: Pick; + project: Pick; roles: Array< { id: string; @@ -603,6 +603,11 @@ export type AddIdentityLdapAuthDTO = { accessTokenTrustedIps: { ipAddress: string; }[]; + + lockoutEnabled: boolean; + lockoutThreshold: number; + lockoutDurationSeconds: number; + lockoutCounterResetSeconds: number; }; export type UpdateIdentityLdapAuthDTO = { @@ -625,6 +630,11 @@ export type UpdateIdentityLdapAuthDTO = { accessTokenTrustedIps?: { ipAddress: string; }[]; + + lockoutEnabled?: boolean; + lockoutThreshold?: number; + lockoutDurationSeconds?: number; + lockoutCounterResetSeconds?: number; }; export type DeleteIdentityLdapAuthDTO = { @@ -650,6 +660,15 @@ export type IdentityLdapAuth = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: IdentityTrustedIp[]; + + lockoutEnabled: boolean; + lockoutThreshold: number; + lockoutDurationSeconds: number; + lockoutCounterResetSeconds: number; +}; + +export type ClearIdentityLdapAuthLockoutsDTO = { + identityId: string; }; export type AddIdentityTokenAuthDTO = { diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 78dcd3b79..d65c7e72c 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -20,7 +20,6 @@ export * from "./identityProjectAdditionalPrivilege"; export * from "./incidentContacts"; export * from "./integrationAuth"; export * from "./integrations"; -export * from "./keys"; export * from "./kms"; export * from "./ldapConfig"; export * from "./oidcConfig"; @@ -29,6 +28,7 @@ export * from "./organization"; export * from "./pkiAlerts"; export * from "./pkiCollections"; export * from "./pkiSubscriber"; +export * from "./projects"; export * from "./projectUserAdditionalPrivilege"; export * from "./rateLimit"; export * from "./roles"; @@ -54,4 +54,3 @@ export * from "./trustedIps"; export * from "./users"; export * from "./webhooks"; export * from "./workflowIntegrations"; -export * from "./workspace"; diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 044ec8d2a..f6c936480 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { TReactQueryOptions } from "@app/types/reactQuery"; -import { workspaceKeys } from "../workspace"; +import { projectKeys } from "../projects"; import { App, BitbucketEnvironment, @@ -970,7 +970,7 @@ export const useAuthorizeIntegration = () => { }, onSuccess: (res) => { queryClient.invalidateQueries({ - queryKey: { queryKey: workspaceKeys.getWorkspaceAuthorization(res.workspace) } + queryKey: { queryKey: projectKeys.getProjectAuthorization(res.workspace) } }); } }); @@ -1016,7 +1016,7 @@ export const useSaveIntegrationAccessToken = () => { }, onSuccess: (res) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceAuthorization(res.workspace) + queryKey: projectKeys.getProjectAuthorization(res.workspace) }); } }); @@ -1035,10 +1035,10 @@ export const useDeleteIntegrationAuths = () => { ), onSuccess: (_, { workspaceId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceAuthorization(workspaceId) + queryKey: projectKeys.getProjectAuthorization(workspaceId) }); queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceIntegrations(workspaceId) + queryKey: projectKeys.getProjectIntegrations(workspaceId) }); } }); @@ -1052,10 +1052,10 @@ export const useDeleteIntegrationAuth = () => { mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration-auth/${id}`), onSuccess: (_, { workspaceId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceAuthorization(workspaceId) + queryKey: projectKeys.getProjectAuthorization(workspaceId) }); queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceIntegrations(workspaceId) + queryKey: projectKeys.getProjectIntegrations(workspaceId) }); } }); diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index a439f8caa..69519a5a4 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstac import { createNotification } from "@app/components/notifications"; import { apiRequest } from "@app/config/request"; -import { workspaceKeys } from "../workspace"; +import { projectKeys } from "../projects"; import { IntegrationMetadataSyncMode, TCloudIntegration, @@ -121,7 +121,7 @@ export const useCreateIntegration = () => { }, onSuccess: (res) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceIntegrations(res.workspace) + queryKey: projectKeys.getProjectIntegrations(res.workspace) }); } }); @@ -141,10 +141,10 @@ export const useDeleteIntegration = () => { ), onSuccess: (_, { workspaceId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceIntegrations(workspaceId) + queryKey: projectKeys.getProjectIntegrations(workspaceId) }); queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceAuthorization(workspaceId) + queryKey: projectKeys.getProjectAuthorization(workspaceId) }); } }); diff --git a/frontend/src/hooks/api/keys/index.tsx b/frontend/src/hooks/api/keys/index.tsx deleted file mode 100644 index cd4f0aea8..000000000 --- a/frontend/src/hooks/api/keys/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { useGetUserWsKey, useUploadWsKey } from "./queries"; diff --git a/frontend/src/hooks/api/keys/queries.tsx b/frontend/src/hooks/api/keys/queries.tsx deleted file mode 100644 index f902f85d3..000000000 --- a/frontend/src/hooks/api/keys/queries.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; - -import { apiRequest } from "@app/config/request"; - -import { UploadWsKeyDTO, UserWsKeyPair } from "./types"; - -const encKeyKeys = { - getUserWorkspaceKey: (workspaceID: string) => ["workspace-key-pair", { workspaceID }] as const -}; - -export const fetchUserWsKey = async (projectId: string) => { - const { data } = await apiRequest.get( - `/api/v2/workspace/${projectId}/encrypted-key` - ); - - return data; -}; - -export const useGetUserWsKey = (workspaceID: string) => - useQuery({ - queryKey: encKeyKeys.getUserWorkspaceKey(workspaceID), - queryFn: () => fetchUserWsKey(workspaceID), - enabled: Boolean(workspaceID) - }); - -// mutations -export const uploadWsKey = async ({ workspaceId, userId, encryptedKey, nonce }: UploadWsKeyDTO) => { - return apiRequest.post(`/api/v1/workspace/${workspaceId}/key`, { - key: { userId, encryptedKey, nonce } - }); -}; - -export const useUploadWsKey = () => - useMutation({ - mutationFn: async ({ encryptedKey, nonce, userId, workspaceId }) => { - return uploadWsKey({ - workspaceId, - userId, - encryptedKey, - nonce - }); - } - }); diff --git a/frontend/src/hooks/api/keys/types.ts b/frontend/src/hooks/api/keys/types.ts deleted file mode 100644 index fc455deaf..000000000 --- a/frontend/src/hooks/api/keys/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -export type UserWsKeyPair = { - id: string; - encryptedKey: string; - nonce: string; - sender: Sender; - receiver: string; - workspace: string; - createdAt: string; - updatedAt: string; - __v: number; -}; - -export type Sender = { - id: string; - email: string; - createdAt: string; - updatedAt: string; - __v: number; - firstName: string; - lastName: string; - publicKey: string; -}; - -export type UploadWsKeyDTO = { - userId: string; - encryptedKey: string; - nonce: string; - workspaceId: string; -}; diff --git a/frontend/src/hooks/api/kms/mutations.tsx b/frontend/src/hooks/api/kms/mutations.tsx index 2bf27bf6a..4fb0a5af5 100644 --- a/frontend/src/hooks/api/kms/mutations.tsx +++ b/frontend/src/hooks/api/kms/mutations.tsx @@ -75,7 +75,7 @@ export const useUpdateProjectKms = (projectId: string) => { mutationFn: async ( updatedData: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string } ) => { - const { data } = await apiRequest.patch(`/api/v1/workspace/${projectId}/kms`, { + const { data } = await apiRequest.patch(`/api/v1/projects/${projectId}/kms`, { kms: updatedData }); @@ -91,7 +91,7 @@ export const useLoadProjectKmsBackup = (projectId: string) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (backup: string) => { - const { data } = await apiRequest.post(`/api/v1/workspace/${projectId}/kms/backup`, { + const { data } = await apiRequest.post(`/api/v1/projects/${projectId}/kms/backup`, { backup }); diff --git a/frontend/src/hooks/api/kms/queries.tsx b/frontend/src/hooks/api/kms/queries.tsx index 9efa9cc51..97d25376c 100644 --- a/frontend/src/hooks/api/kms/queries.tsx +++ b/frontend/src/hooks/api/kms/queries.tsx @@ -49,7 +49,7 @@ export const useGetActiveProjectKms = (projectId: string) => { name: string; isExternal: string; }; - }>(`/api/v1/workspace/${projectId}/kms`); + }>(`/api/v1/projects/${projectId}/kms`); return secretManagerKmsKey; } }); @@ -58,7 +58,7 @@ export const useGetActiveProjectKms = (projectId: string) => { export const fetchProjectKmsBackup = async (projectId: string) => { const { data } = await apiRequest.get<{ secretManager: string; - }>(`/api/v1/workspace/${projectId}/kms/backup`); + }>(`/api/v1/projects/${projectId}/kms/backup`); return data; }; diff --git a/frontend/src/hooks/api/migration/index.ts b/frontend/src/hooks/api/migration/index.ts index f8dd99d03..0c2adeab0 100644 --- a/frontend/src/hooks/api/migration/index.ts +++ b/frontend/src/hooks/api/migration/index.ts @@ -1 +1,2 @@ export * from "./mutations"; +export * from "./queries"; diff --git a/frontend/src/hooks/api/migration/mutations.tsx b/frontend/src/hooks/api/migration/mutations.tsx index 182797954..b2694b458 100644 --- a/frontend/src/hooks/api/migration/mutations.tsx +++ b/frontend/src/hooks/api/migration/mutations.tsx @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { workspaceKeys } from "../workspace"; +import { projectKeys } from "../projects"; export const useImportEnvKey = () => { const queryClient = useQueryClient(); @@ -34,7 +34,7 @@ export const useImportEnvKey = () => { }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getAllUserWorkspace() + queryKey: projectKeys.getAllUserProjects() }); } }); diff --git a/frontend/src/hooks/api/migration/queries.tsx b/frontend/src/hooks/api/migration/queries.tsx new file mode 100644 index 000000000..e96533b09 --- /dev/null +++ b/frontend/src/hooks/api/migration/queries.tsx @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { ExternalMigrationProviders } from "./types"; + +const externalMigrationQueryKeys = { + customMigrationAvailable: (provider: ExternalMigrationProviders) => [ + "custom-migration-available", + provider + ] +}; + +export const useHasCustomMigrationAvailable = (provider: ExternalMigrationProviders) => { + return useQuery({ + queryKey: externalMigrationQueryKeys.customMigrationAvailable(provider), + queryFn: () => + apiRequest.get<{ enabled: boolean }>( + `/api/v3/external-migration/custom-migration-enabled/${provider}` + ) + }); +}; diff --git a/frontend/src/hooks/api/migration/types.ts b/frontend/src/hooks/api/migration/types.ts new file mode 100644 index 000000000..945f18d8e --- /dev/null +++ b/frontend/src/hooks/api/migration/types.ts @@ -0,0 +1,4 @@ +export enum ExternalMigrationProviders { + Vault = "vault", + EnvKey = "env-key" +} diff --git a/frontend/src/hooks/api/notifications/mutations.tsx b/frontend/src/hooks/api/notifications/mutations.tsx new file mode 100644 index 000000000..34ab7aa26 --- /dev/null +++ b/frontend/src/hooks/api/notifications/mutations.tsx @@ -0,0 +1,70 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { useOrganization } from "@app/context"; + +import { notificationKeys } from "./queries"; +import { TUserNotification } from "./types"; + +export const useMarkAllNotificationsAsRead = () => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg.id || ""; + + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => { + await apiRequest.post("/api/v1/notifications/user/mark-as-read"); + }, + onSuccess: () => { + queryClient.setQueryData(notificationKeys.list(orgId), (oldData) => { + if (!oldData) return oldData; + return oldData.map((notification) => ({ + ...notification, + isRead: true + })); + }); + } + }); +}; + +export const useUpdateNotification = () => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg.id || ""; + + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ notificationId, isRead }: { notificationId: string; isRead: boolean }) => { + const { data } = await apiRequest.patch<{ notification: TUserNotification }>( + `/api/v1/notifications/user/${notificationId}`, + { isRead } + ); + return data.notification; + }, + onSuccess: (updatedNotification) => { + queryClient.setQueryData(notificationKeys.list(orgId), (oldData) => { + if (!oldData) return oldData; + return oldData.map((notification) => + notification.id === updatedNotification.id ? updatedNotification : notification + ); + }); + } + }); +}; + +export const useDeleteNotification = () => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg.id || ""; + + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (notificationId: string) => { + await apiRequest.delete(`/api/v1/notifications/user/${notificationId}`); + }, + onSuccess: (_, notificationId) => { + queryClient.setQueryData(notificationKeys.list(orgId), (oldData) => { + if (!oldData) return oldData; + return oldData.filter((notification) => notification.id !== notificationId); + }); + } + }); +}; diff --git a/frontend/src/hooks/api/notifications/queries.tsx b/frontend/src/hooks/api/notifications/queries.tsx new file mode 100644 index 000000000..f7a31afe6 --- /dev/null +++ b/frontend/src/hooks/api/notifications/queries.tsx @@ -0,0 +1,29 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { useOrganization } from "@app/context"; + +import { TUserNotification } from "./types"; + +export const notificationKeys = { + all: ["notifications"] as const, + list: (orgId: string) => [...notificationKeys.all, "list", { orgId }] as const +}; + +export const useGetMyNotifications = () => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg.id || ""; + + return useQuery({ + queryKey: notificationKeys.list(orgId), + queryFn: async () => { + const { + data: { notifications } + } = await apiRequest.get<{ notifications: TUserNotification[] }>( + "/api/v1/notifications/user" + ); + return notifications; + }, + refetchInterval: 30 * 1000 // Poll every 30 seconds + }); +}; diff --git a/frontend/src/hooks/api/notifications/types.ts b/frontend/src/hooks/api/notifications/types.ts new file mode 100644 index 000000000..97552046c --- /dev/null +++ b/frontend/src/hooks/api/notifications/types.ts @@ -0,0 +1,10 @@ +export interface TUserNotification { + id: string; + userId: string; + type: string; + title: string; + body?: string | null; + link?: string | null; + isRead: boolean; + createdAt: string; +} diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index 71f2a8b90..e9c36fada 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -159,3 +159,8 @@ export enum OrgIdentityOrderBy { Name = "name", Role = "role" } + +export enum OrgMembershipStatus { + Invited = "invited", + Accepted = "accepted" +} diff --git a/frontend/src/hooks/api/pkiAlerts/mutations.tsx b/frontend/src/hooks/api/pkiAlerts/mutations.tsx index 34397c141..df48a6aab 100644 --- a/frontend/src/hooks/api/pkiAlerts/mutations.tsx +++ b/frontend/src/hooks/api/pkiAlerts/mutations.tsx @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { workspaceKeys } from "../workspace"; +import { projectKeys } from "../projects"; import { pkiAlertKeys } from "./queries"; import { TCreatePkiAlertDTO, TDeletePkiAlertDTO, TPkiAlert, TUpdatePkiAlertDTO } from "./types"; @@ -14,7 +14,7 @@ export const useCreatePkiAlert = () => { return alert; }, onSuccess: (_, { projectId }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspacePkiAlerts(projectId) }); + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectPkiAlerts(projectId) }); } }); }; @@ -30,7 +30,7 @@ export const useUpdatePkiAlert = () => { return alert; }, onSuccess: (_, { projectId, alertId }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspacePkiAlerts(projectId) }); + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectPkiAlerts(projectId) }); queryClient.invalidateQueries({ queryKey: pkiAlertKeys.getPkiAlertById(alertId) }); } }); @@ -44,7 +44,7 @@ export const useDeletePkiAlert = () => { return alert; }, onSuccess: (_, { projectId, alertId }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspacePkiAlerts(projectId) }); + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectPkiAlerts(projectId) }); queryClient.invalidateQueries({ queryKey: pkiAlertKeys.getPkiAlertById(alertId) }); } }); diff --git a/frontend/src/hooks/api/pkiCollections/mutations.tsx b/frontend/src/hooks/api/pkiCollections/mutations.tsx index af1f0af18..a06afcd09 100644 --- a/frontend/src/hooks/api/pkiCollections/mutations.tsx +++ b/frontend/src/hooks/api/pkiCollections/mutations.tsx @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { workspaceKeys } from "../workspace"; +import { projectKeys } from "../projects"; import { pkiCollectionKeys } from "./queries"; import { TAddItemToPkiCollectionDTO, @@ -26,7 +26,7 @@ export const useCreatePkiCollection = () => { }, onSuccess: (_, { projectId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspacePkiCollections(projectId) + queryKey: projectKeys.getProjectPkiCollections(projectId) }); } }); @@ -44,7 +44,7 @@ export const useUpdatePkiCollection = () => { }, onSuccess: (_, { projectId, collectionId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspacePkiCollections(projectId) + queryKey: projectKeys.getProjectPkiCollections(projectId) }); queryClient.invalidateQueries({ queryKey: pkiCollectionKeys.getPkiCollectionById(collectionId) @@ -64,7 +64,7 @@ export const useDeletePkiCollection = () => { }, onSuccess: (_, { projectId, collectionId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspacePkiCollections(projectId) + queryKey: projectKeys.getProjectPkiCollections(projectId) }); queryClient.invalidateQueries({ queryKey: pkiCollectionKeys.getPkiCollectionById(collectionId) diff --git a/frontend/src/hooks/api/pkiSubscriber/mutations.tsx b/frontend/src/hooks/api/pkiSubscriber/mutations.tsx index b30924f97..57d086780 100644 --- a/frontend/src/hooks/api/pkiSubscriber/mutations.tsx +++ b/frontend/src/hooks/api/pkiSubscriber/mutations.tsx @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { TCreateCertificateResponse } from "../ca/types"; -import { workspaceKeys } from "../workspace/query-keys"; +import { projectKeys } from "../projects/query-keys"; import { pkiSubscriberKeys } from "./queries"; import { TCreatePkiSubscriberDTO, @@ -22,7 +22,7 @@ export const useCreatePkiSubscriber = () => { }, onSuccess: ({ projectId, name }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspacePkiSubscribers(projectId) + queryKey: projectKeys.getProjectPkiSubscribers(projectId) }); queryClient.invalidateQueries({ queryKey: pkiSubscriberKeys.getPkiSubscriber({ @@ -46,7 +46,7 @@ export const useUpdatePkiSubscriber = () => { }, onSuccess: ({ projectId, name }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspacePkiSubscribers(projectId) + queryKey: projectKeys.getProjectPkiSubscribers(projectId) }); queryClient.invalidateQueries({ queryKey: pkiSubscriberKeys.getPkiSubscriber({ @@ -74,7 +74,7 @@ export const useDeletePkiSubscriber = () => { }, onSuccess: ({ name, projectId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspacePkiSubscribers(projectId) + queryKey: projectKeys.getProjectPkiSubscribers(projectId) }); queryClient.invalidateQueries({ queryKey: pkiSubscriberKeys.getPkiSubscriber({ diff --git a/frontend/src/hooks/api/projectTemplates/types.ts b/frontend/src/hooks/api/projectTemplates/types.ts index f5ff22dff..d58e80f53 100644 --- a/frontend/src/hooks/api/projectTemplates/types.ts +++ b/frontend/src/hooks/api/projectTemplates/types.ts @@ -1,6 +1,6 @@ import { TProjectRole } from "@app/hooks/api/roles/types"; -import { ProjectType } from "../workspace/types"; +import { ProjectType } from "../projects/types"; export type TProjectTemplate = { id: string; diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/projects/index.tsx similarity index 89% rename from frontend/src/hooks/api/workspace/index.tsx rename to frontend/src/hooks/api/projects/index.tsx index df2d55dc3..ed8eba815 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/projects/index.tsx @@ -17,15 +17,14 @@ export { useDeleteWsEnvironment, useGetProjectSshConfig, useGetUpgradeProjectStatus, + useGetUserProjects, useGetUserWorkspaceMemberships, - useGetUserWorkspaces, useGetWorkspaceAuthorizations, useGetWorkspaceById, useGetWorkspaceIdentityMembershipDetails, useGetWorkspaceIdentityMemberships, useGetWorkspaceIndexStatus, useGetWorkspaceIntegrations, - useGetWorkspaceSecrets, useGetWorkspaceUserDetails, useGetWorkspaceUsers, useGetWorkspaceWorkflowIntegrationConfig, @@ -41,13 +40,11 @@ export { useListWorkspaceSshCertificateTemplates, useListWorkspaceSshHostGroups, useListWorkspaceSshHosts, - useNameWorkspaceSecrets, useSearchProjects, - useToggleAutoCapitalization, useUpdateIdentityWorkspaceRole, useUpdateProject, useUpdateUserWorkspaceRole, useUpdateWsEnvironment, useUpgradeProject } from "./queries"; -export { workspaceKeys } from "./query-keys"; +export { projectKeys } from "./query-keys"; diff --git a/frontend/src/hooks/api/workspace/mutations.tsx b/frontend/src/hooks/api/projects/mutations.tsx similarity index 68% rename from frontend/src/hooks/api/workspace/mutations.tsx rename to frontend/src/hooks/api/projects/mutations.tsx index 5cd7e3bbc..e2a80ff53 100644 --- a/frontend/src/hooks/api/workspace/mutations.tsx +++ b/frontend/src/hooks/api/projects/mutations.tsx @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { userKeys } from "../users/query-keys"; -import { workspaceKeys } from "./query-keys"; +import { projectKeys } from "./query-keys"; import { TProjectSshConfig, TUpdateProjectSshConfigDTO, @@ -24,7 +24,7 @@ export const useAddGroupToWorkspace = () => { }) => { const { data: { groupMembership } - } = await apiRequest.post(`/api/v2/workspace/${projectId}/groups/${groupId}`, { + } = await apiRequest.post(`/api/v1/projects/${projectId}/groups/${groupId}`, { role }); @@ -32,7 +32,7 @@ export const useAddGroupToWorkspace = () => { }, onSuccess: (_, { projectId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId) + queryKey: projectKeys.getProjectGroupMemberships(projectId) }); } }); @@ -44,7 +44,7 @@ export const useUpdateGroupWorkspaceRole = () => { mutationFn: async ({ groupId, projectId, roles }: TUpdateWorkspaceGroupRoleDTO) => { const { data: { groupMembership } - } = await apiRequest.patch(`/api/v2/workspace/${projectId}/groups/${groupId}`, { + } = await apiRequest.patch(`/api/v1/projects/${projectId}/groups/${groupId}`, { roles }); @@ -52,10 +52,10 @@ export const useUpdateGroupWorkspaceRole = () => { }, onSuccess: (_, { projectId, groupId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId) + queryKey: projectKeys.getProjectGroupMemberships(projectId) }); queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceGroupMembershipDetails(projectId, groupId) + queryKey: projectKeys.getProjectGroupMembershipDetails(projectId, groupId) }); } }); @@ -74,12 +74,12 @@ export const useDeleteGroupFromWorkspace = () => { }) => { const { data: { groupMembership } - } = await apiRequest.delete(`/api/v2/workspace/${projectId}/groups/${groupId}`); + } = await apiRequest.delete(`/api/v1/projects/${projectId}/groups/${groupId}`); return groupMembership; }, onSuccess: (_, { projectId, username }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId) + queryKey: projectKeys.getProjectGroupMemberships(projectId) }); if (username) { @@ -91,25 +91,25 @@ export const useDeleteGroupFromWorkspace = () => { export const useLeaveProject = () => { const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ workspaceId }) => { - return apiRequest.delete(`/api/v1/workspace/${workspaceId}/leave`); + return useMutation({ + mutationFn: ({ projectId }) => { + return apiRequest.delete(`/api/v1/projects/${projectId}/leave`); }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); + queryClient.invalidateQueries({ queryKey: projectKeys.getAllUserProjects() }); } }); }; export const useMigrateProjectToV3 = () => { const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ workspaceId }) => { - return apiRequest.post(`/api/v1/workspace/${workspaceId}/migrate-v3`); + return useMutation({ + mutationFn: ({ projectId }) => { + return apiRequest.post(`/api/v1/projects/${projectId}/migrate-v3`); }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getAllUserWorkspace() + queryKey: projectKeys.getAllUserProjects() }); } }); @@ -118,7 +118,7 @@ export const useMigrateProjectToV3 = () => { export const useRequestProjectAccess = () => { return useMutation({ mutationFn: ({ projectId, comment }) => { - return apiRequest.post(`/api/v1/workspace/${projectId}/project-access`, { + return apiRequest.post(`/api/v1/projects/${projectId}/project-access`, { comment }); } @@ -129,14 +129,14 @@ export const useUpdateProjectSshConfig = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ projectId, defaultUserSshCaId, defaultHostSshCaId }) => { - return apiRequest.patch(`/api/v1/workspace/${projectId}/ssh-config`, { + return apiRequest.patch(`/api/v1/projects/${projectId}/ssh-config`, { defaultUserSshCaId, defaultHostSshCaId }); }, onSuccess: (_, { projectId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getProjectSshConfig(projectId) + queryKey: projectKeys.getProjectSshConfig(projectId) }); } }); diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/projects/queries.tsx similarity index 55% rename from frontend/src/hooks/api/workspace/queries.tsx rename to frontend/src/hooks/api/projects/queries.tsx index 3c7a6d30c..0445d5984 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/projects/queries.tsx @@ -15,7 +15,6 @@ import { TIntegration } from "../integrations/types"; import { TPkiAlert } from "../pkiAlerts/types"; import { TPkiCollection } from "../pkiCollections/types"; import { TPkiSubscriber } from "../pkiSubscriber/types"; -import { EncryptedSecret } from "../secrets/types"; import { TSshCertificate, TSshCertificateAuthority } from "../sshCa/types"; import { TSshCertificateTemplate } from "../sshCertificateTemplates/types"; import { TSshHost } from "../sshHost/types"; @@ -26,42 +25,36 @@ import { ProjectWorkflowIntegrationConfig, WorkflowIntegrationPlatform } from "../workflowIntegrations/types"; -import { workspaceKeys } from "./query-keys"; +import { projectKeys } from "./query-keys"; import { CreateEnvironmentDTO, CreateWorkspaceDTO, DeleteEnvironmentDTO, DeleteWorkspaceDTO, - NameWorkspaceSecretsDTO, + Project, + ProjectEnv, ProjectIdentityOrderBy, ProjectType, TGetUpgradeProjectStatusDTO, TListProjectIdentitiesDTO, - ToggleAutoCapitalizationDTO, - ToggleDeleteProjectProtectionDTO, TProjectSshConfig, TSearchProjectsDTO, TUpdateWorkspaceIdentityRoleDTO, TUpdateWorkspaceUserRoleDTO, UpdateAuditLogsRetentionDTO, UpdateEnvironmentDTO, - UpdatePitVersionLimitDTO, - UpdateProjectDTO, - Workspace, - WorkspaceEnv + UpdateProjectDTO } from "./types"; -export const fetchWorkspaceById = async (workspaceId: string) => { - const { data } = await apiRequest.get<{ workspace: Workspace }>( - `/api/v1/workspace/${workspaceId}` - ); +export const fetchProjectById = async (projectId: string) => { + const { data } = await apiRequest.get<{ project: Project }>(`/api/v1/projects/${projectId}`); - return data.workspace; + return data.project; }; -const fetchWorkspaceIndexStatus = async (workspaceId: string) => { +const fetchWorkspaceIndexStatus = async (projectId: string) => { const { data } = await apiRequest.get( - `/api/v3/workspaces/${workspaceId}/secrets/blind-index-status` + `/api/v3/projects/${projectId}/secrets/blind-index-status` ); return data; @@ -69,34 +62,24 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => { const fetchProjectUpgradeStatus = async (projectId: string) => { const { data } = await apiRequest.get<{ status: string }>( - `/api/v2/workspace/${projectId}/upgrade/status` + `/api/v1/projects/${projectId}/upgrade/status` ); return data; }; -export const fetchWorkspaceSecrets = async (workspaceId: string) => { - const { - data: { secrets } - } = await apiRequest.get<{ secrets: EncryptedSecret[] }>( - `/api/v3/workspaces/${workspaceId}/secrets` - ); - - return secrets; -}; - export const useUpgradeProject = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ projectId, privateKey }) => { - return apiRequest.post(`/api/v2/workspace/${projectId}/upgrade`, { + return apiRequest.post(`/api/v1/projects/${projectId}/upgrade`, { userPrivateKey: privateKey }); }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getAllUserWorkspace() + queryKey: projectKeys.getAllUserProjects() }); } }); @@ -108,7 +91,7 @@ export const useGetUpgradeProjectStatus = ({ refetchInterval }: TGetUpgradeProjectStatusDTO) => { return useQuery({ - queryKey: workspaceKeys.getProjectUpgradeStatus(projectId), + queryKey: projectKeys.getProjectUpgradeStatus(projectId), queryFn: () => fetchProjectUpgradeStatus(projectId), enabled, refetchInterval @@ -116,44 +99,36 @@ export const useGetUpgradeProjectStatus = ({ }; const fetchUserWorkspaces = async (includeRoles?: boolean, type?: ProjectType | "all") => { - const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace", { + const { data } = await apiRequest.get<{ projects: Project[] }>("/api/v1/projects", { params: { includeRoles, type } }); - return data.workspaces; + return data.projects; }; -export const useGetWorkspaceIndexStatus = (workspaceId: string) => { +export const useGetWorkspaceIndexStatus = (projectId: string) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceIndexStatus(workspaceId), - queryFn: () => fetchWorkspaceIndexStatus(workspaceId), - enabled: true - }); -}; - -export const useGetWorkspaceSecrets = (workspaceId: string) => { - return useQuery({ - queryKey: workspaceKeys.getWorkspaceSecrets(workspaceId), - queryFn: () => fetchWorkspaceSecrets(workspaceId), + queryKey: projectKeys.getProjectIndexStatus(projectId), + queryFn: () => fetchWorkspaceIndexStatus(projectId), enabled: true }); }; export const useGetWorkspaceById = ( - workspaceId: string, + projectId: string, dto?: { refetchInterval?: number | false } ) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceById(workspaceId), - queryFn: () => fetchWorkspaceById(workspaceId), - enabled: Boolean(workspaceId), + queryKey: projectKeys.getProjectById(projectId), + queryFn: () => fetchProjectById(projectId), + enabled: Boolean(projectId), refetchInterval: dto?.refetchInterval }); }; -export const useGetUserWorkspaces = ({ +export const useGetUserProjects = ({ includeRoles, options = {} }: { @@ -161,19 +136,19 @@ export const useGetUserWorkspaces = ({ options?: { enabled?: boolean }; } = {}) => useQuery({ - queryKey: workspaceKeys.getAllUserWorkspace(), + queryKey: projectKeys.getAllUserProjects(), queryFn: () => fetchUserWorkspaces(includeRoles), ...options }); export const useSearchProjects = ({ options, ...dto }: TSearchProjectsDTO) => useQuery({ - queryKey: workspaceKeys.searchWorkspace(dto), + queryKey: projectKeys.searchProject(dto), queryFn: async () => { const { data } = await apiRequest.post<{ - projects: (Workspace & { isMember: boolean })[]; + projects: (Project & { isMember: boolean })[]; totalCount: number; - }>("/api/v1/workspace/search", dto); + }>("/api/v1/projects/search", dto); return data; }, @@ -181,81 +156,65 @@ export const useSearchProjects = ({ options, ...dto }: TSearchProjectsDTO) => }); const fetchUserWorkspaceMemberships = async (orgId: string) => { - const { data } = await apiRequest.get>( - `/api/v1/organization/${orgId}/workspace-memberships` + const { data } = await apiRequest.get>( + `/api/v1/organization/${orgId}/project-memberships` ); return data; }; -// to get all userids in an org with the workspace they are part of +// to get all userids in an org with the project they are part of export const useGetUserWorkspaceMemberships = (orgId: string) => useQuery({ - queryKey: workspaceKeys.getWorkspaceMemberships(orgId), + queryKey: projectKeys.getProjectMemberships(orgId), queryFn: () => fetchUserWorkspaceMemberships(orgId), enabled: Boolean(orgId) }); -export const useNameWorkspaceSecrets = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: async ({ workspaceId, secretsToUpdate }) => - apiRequest.post(`/api/v3/workspaces/${workspaceId}/secrets/names`, { - secretsToUpdate - }), - onSuccess: (_, variables) => { - queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceIndexStatus(variables.workspaceId) - }); - } - }); -}; - -const fetchWorkspaceAuthorization = async (workspaceId: string) => { +const fetchWorkspaceAuthorization = async (projectId: string) => { const { data } = await apiRequest.get<{ authorizations: IntegrationAuth[] }>( - `/api/v1/workspace/${workspaceId}/authorizations` + `/api/v1/projects/${projectId}/authorizations` ); return data.authorizations; }; export const useGetWorkspaceAuthorizations = ( - workspaceId: string, + projectId: string, select?: (data: IntegrationAuth[]) => TData ) => useQuery({ - queryKey: workspaceKeys.getWorkspaceAuthorization(workspaceId), - queryFn: () => fetchWorkspaceAuthorization(workspaceId), - enabled: Boolean(workspaceId), + queryKey: projectKeys.getProjectAuthorization(projectId), + queryFn: () => fetchWorkspaceAuthorization(projectId), + enabled: Boolean(projectId), select }); -export const fetchWorkspaceIntegrations = async (workspaceId: string) => { +export const fetchWorkspaceIntegrations = async (projectId: string) => { const { data } = await apiRequest.get<{ integrations: TIntegration[] }>( - `/api/v1/workspace/${workspaceId}/integrations` + `/api/v1/projects/${projectId}/integrations` ); return data.integrations; }; -export const useGetWorkspaceIntegrations = (workspaceId: string) => +export const useGetWorkspaceIntegrations = (projectId: string) => useQuery({ - queryKey: workspaceKeys.getWorkspaceIntegrations(workspaceId), - queryFn: () => fetchWorkspaceIntegrations(workspaceId), - enabled: Boolean(workspaceId), + queryKey: projectKeys.getProjectIntegrations(projectId), + queryFn: () => fetchWorkspaceIntegrations(projectId), + enabled: Boolean(projectId), refetchInterval: 4000 }); export const createWorkspace = ( dto: CreateWorkspaceDTO -): Promise<{ data: { project: Workspace } }> => { - return apiRequest.post("/api/v2/workspace", dto); +): Promise<{ data: { project: Project } }> => { + return apiRequest.post("/api/v1/projects", dto); }; export const useCreateWorkspace = () => { const queryClient = useQueryClient(); - return useMutation<{ data: { project: Workspace } }, object, CreateWorkspaceDTO>({ + return useMutation<{ data: { project: Project } }, object, CreateWorkspaceDTO>({ mutationFn: async ({ projectName, projectDescription, kmsKeyId, template, type }) => createWorkspace({ projectName, @@ -266,7 +225,7 @@ export const useCreateWorkspace = () => { }), onSuccess: () => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getAllUserWorkspace() + queryKey: projectKeys.getAllUserProjects() }); } }); @@ -275,85 +234,37 @@ export const useCreateWorkspace = () => { export const useUpdateProject = () => { const queryClient = useQueryClient(); - return useMutation({ + return useMutation({ mutationFn: async ({ - projectID, + projectId: projectID, newProjectName, + hasDeleteProtection, newProjectDescription, newSlug, secretSharing, showSnapshotsLegacy, - secretDetectionIgnoreValues + secretDetectionIgnoreValues, + autoCapitalization, + pitVersionLimit }) => { - const { data } = await apiRequest.patch<{ workspace: Workspace }>( - `/api/v1/workspace/${projectID}`, + const { data } = await apiRequest.patch<{ project: Project }>( + `/api/v1/projects/${projectID}`, { name: newProjectName, description: newProjectDescription, slug: newSlug, secretSharing, showSnapshotsLegacy, - secretDetectionIgnoreValues + secretDetectionIgnoreValues, + autoCapitalization, + pitVersionLimit, + hasDeleteProtection } ); - return data.workspace; + return data.project; }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); - } - }); -}; - -export const useToggleAutoCapitalization = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: async ({ workspaceID, state }) => { - const { data } = await apiRequest.post<{ workspace: Workspace }>( - `/api/v1/workspace/${workspaceID}/auto-capitalization`, - { - autoCapitalization: state - } - ); - return data.workspace; - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); - } - }); -}; - -export const useToggleDeleteProjectProtection = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: async ({ workspaceID, state }) => { - const { data } = await apiRequest.post<{ workspace: Workspace }>( - `/api/v1/workspace/${workspaceID}/delete-protection`, - { - hasDeleteProtection: state - } - ); - return data.workspace; - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); - } - }); -}; - -export const useUpdateWorkspaceVersionLimit = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: async ({ projectSlug, pitVersionLimit }) => { - const { data } = await apiRequest.put(`/api/v1/workspace/${projectSlug}/version-limit`, { - pitVersionLimit - }); - return data.workspace; - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); + queryClient.invalidateQueries({ queryKey: projectKeys.getAllUserProjects() }); } }); }; @@ -361,18 +272,18 @@ export const useUpdateWorkspaceVersionLimit = () => { export const useUpdateWorkspaceAuditLogsRetention = () => { const queryClient = useQueryClient(); - return useMutation({ + return useMutation({ mutationFn: async ({ projectSlug, auditLogsRetentionDays }) => { const { data } = await apiRequest.put( - `/api/v1/workspace/${projectSlug}/audit-logs-retention`, + `/api/v1/projects/${projectSlug}/audit-logs-retention`, { auditLogsRetentionDays } ); - return data.workspace; + return data.project; }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); + queryClient.invalidateQueries({ queryKey: projectKeys.getAllUserProjects() }); } }); }; @@ -380,13 +291,13 @@ export const useUpdateWorkspaceAuditLogsRetention = () => { export const useDeleteWorkspace = () => { const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ workspaceID }) => { - const { data } = await apiRequest.delete(`/api/v1/workspace/${workspaceID}`); - return data.workspace; + return useMutation({ + mutationFn: async ({ projectID }) => { + const { data } = await apiRequest.delete(`/api/v1/projects/${projectID}`); + return data.project; }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); + queryClient.invalidateQueries({ queryKey: projectKeys.getAllUserProjects() }); queryClient.invalidateQueries({ queryKey: ["org-admin-projects"] }); @@ -397,10 +308,10 @@ export const useDeleteWorkspace = () => { export const useCreateWsEnvironment = () => { const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ workspaceId, name, slug }) => { - const { data } = await apiRequest.post<{ environment: WorkspaceEnv }>( - `/api/v1/workspace/${workspaceId}/environments`, + return useMutation({ + mutationFn: async ({ projectId, name, slug }) => { + const { data } = await apiRequest.post<{ environment: ProjectEnv }>( + `/api/v1/projects/${projectId}/environments`, { name, slug @@ -410,7 +321,7 @@ export const useCreateWsEnvironment = () => { }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getAllUserWorkspace() + queryKey: projectKeys.getAllUserProjects() }); } }); @@ -420,8 +331,8 @@ export const useUpdateWsEnvironment = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ workspaceId, id, name, slug, position }) => { - return apiRequest.patch(`/api/v1/workspace/${workspaceId}/environments/${id}`, { + mutationFn: ({ projectId, id, name, slug, position }) => { + return apiRequest.patch(`/api/v1/projects/${projectId}/environments/${id}`, { name, slug, position @@ -429,7 +340,7 @@ export const useUpdateWsEnvironment = () => { }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getAllUserWorkspace() + queryKey: projectKeys.getAllUserProjects() }); } }); @@ -439,57 +350,54 @@ export const useDeleteWsEnvironment = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ id, workspaceId }) => { - return apiRequest.delete(`/api/v1/workspace/${workspaceId}/environments/${id}`); + mutationFn: ({ id, projectId }) => { + return apiRequest.delete(`/api/v1/projects/${projectId}/environments/${id}`); }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getAllUserWorkspace() + queryKey: projectKeys.getAllUserProjects() }); } }); }; export const useGetWorkspaceUsers = ( - workspaceId: string, + projectId: string, includeGroupMembers?: boolean, roles?: string[] ) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceUsers(workspaceId, includeGroupMembers, roles), + queryKey: projectKeys.getProjectUsers(projectId, includeGroupMembers, roles), queryFn: async () => { const { data: { users } - } = await apiRequest.get<{ users: TWorkspaceUser[] }>( - `/api/v1/workspace/${workspaceId}/users`, - { - params: { - includeGroupMembers, - roles: - roles && roles.length > 0 - ? roles.map((role) => encodeURIComponent(role)).join(",") - : undefined - } + } = await apiRequest.get<{ users: TWorkspaceUser[] }>(`/api/v1/projects/${projectId}/users`, { + params: { + includeGroupMembers, + roles: + roles && roles.length > 0 + ? roles.map((role) => encodeURIComponent(role)).join(",") + : undefined } - ); + }); return users; }, enabled: true }); }; -export const useGetWorkspaceUserDetails = (workspaceId: string, membershipId: string) => { +export const useGetWorkspaceUserDetails = (projectId: string, membershipId: string) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceUserDetails(workspaceId, membershipId), + queryKey: projectKeys.getProjectUserDetails(projectId, membershipId), queryFn: async () => { const { data: { membership } } = await apiRequest.get<{ membership: TWorkspaceUser }>( - `/api/v1/workspace/${workspaceId}/memberships/${membershipId}` + `/api/v1/projects/${projectId}/memberships/${membershipId}` ); return membership; }, - enabled: Boolean(workspaceId) && Boolean(membershipId) + enabled: Boolean(projectId) && Boolean(membershipId) }); }; @@ -499,21 +407,21 @@ export const useDeleteUserFromWorkspace = () => { return useMutation({ mutationFn: async ({ usernames, - workspaceId + projectId }: { - workspaceId: string; + projectId: string; usernames: string[]; orgId: string; }) => { const { data: { deletedMembership } - } = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/memberships`, { + } = await apiRequest.delete(`/api/v1/projects/${projectId}/memberships`, { data: { usernames } }); return deletedMembership; }, - onSuccess: (_, { orgId, workspaceId }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceUsers(workspaceId) }); + onSuccess: (_, { orgId, projectId }) => { + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectUsers(projectId) }); queryClient.invalidateQueries({ queryKey: userKeys.allOrgMembershipProjectMemberships(orgId) }); @@ -524,21 +432,21 @@ export const useDeleteUserFromWorkspace = () => { export const useUpdateUserWorkspaceRole = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ membershipId, roles, workspaceId }: TUpdateWorkspaceUserRoleDTO) => { + mutationFn: async ({ membershipId, roles, projectId }: TUpdateWorkspaceUserRoleDTO) => { const { data: { membership } } = await apiRequest.patch<{ membership: { projectId: string } }>( - `/api/v1/workspace/${workspaceId}/memberships/${membershipId}`, + `/api/v1/projects/${projectId}/memberships/${membershipId}`, { roles } ); return membership; }, - onSuccess: (_, { workspaceId, membershipId }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceUsers(workspaceId) }); + onSuccess: (_, { projectId, membershipId }) => { + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectUsers(projectId) }); queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceUserDetails(workspaceId, membershipId) + queryKey: projectKeys.getProjectUserDetails(projectId, membershipId) }); } }); @@ -549,17 +457,17 @@ export const useAddIdentityToWorkspace = () => { return useMutation({ mutationFn: async ({ identityId, - workspaceId, + projectId, role }: { identityId: string; - workspaceId: string; + projectId: string; role?: string; }) => { const { data: { identityMembership } } = await apiRequest.post( - `/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`, + `/api/v1/projects/${projectId}/identity-memberships/${identityId}`, { role } @@ -567,9 +475,9 @@ export const useAddIdentityToWorkspace = () => { return identityMembership; }, - onSuccess: (_, { identityId, workspaceId }) => { + onSuccess: (_, { identityId, projectId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceIdentityMemberships(workspaceId) + queryKey: projectKeys.getProjectIdentityMemberships(projectId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityProjectMemberships(identityId) @@ -581,11 +489,11 @@ export const useAddIdentityToWorkspace = () => { export const useUpdateIdentityWorkspaceRole = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ identityId, workspaceId, roles }: TUpdateWorkspaceIdentityRoleDTO) => { + mutationFn: async ({ identityId, projectId, roles }: TUpdateWorkspaceIdentityRoleDTO) => { const { data: { identityMembership } } = await apiRequest.patch( - `/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`, + `/api/v1/projects/${projectId}/identity-memberships/${identityId}`, { roles } @@ -593,15 +501,15 @@ export const useUpdateIdentityWorkspaceRole = () => { return identityMembership; }, - onSuccess: (_, { identityId, workspaceId }) => { + onSuccess: (_, { identityId, projectId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceIdentityMemberships(workspaceId) + queryKey: projectKeys.getProjectIdentityMemberships(projectId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityProjectMemberships(identityId) }); queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceIdentityMembershipDetails(workspaceId, identityId) + queryKey: projectKeys.getProjectIdentityMembershipDetails(projectId, identityId) }); } }); @@ -610,23 +518,17 @@ export const useUpdateIdentityWorkspaceRole = () => { export const useDeleteIdentityFromWorkspace = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ - identityId, - workspaceId - }: { - identityId: string; - workspaceId: string; - }) => { + mutationFn: async ({ identityId, projectId }: { identityId: string; projectId: string }) => { const { data: { identityMembership } } = await apiRequest.delete( - `/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}` + `/api/v1/projects/${projectId}/identity-memberships/${identityId}` ); return identityMembership; }, - onSuccess: (_, { identityId, workspaceId }) => { + onSuccess: (_, { identityId, projectId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceIdentityMemberships(workspaceId) + queryKey: projectKeys.getProjectIdentityMemberships(projectId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityProjectMemberships(identityId) @@ -637,7 +539,7 @@ export const useDeleteIdentityFromWorkspace = () => { export const useGetWorkspaceIdentityMemberships = ( { - workspaceId, + projectId, offset = 0, limit = 100, orderBy = ProjectIdentityOrderBy.Name, @@ -649,14 +551,14 @@ export const useGetWorkspaceIdentityMemberships = ( TProjectIdentitiesList, unknown, TProjectIdentitiesList, - ReturnType + ReturnType >, "queryKey" | "queryFn" > ) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceIdentityMembershipsWithParams({ - workspaceId, + queryKey: projectKeys.getProjectIdentityMembershipsWithParams({ + projectId, offset, limit, orderBy, @@ -673,7 +575,7 @@ export const useGetWorkspaceIdentityMemberships = ( }); const { data } = await apiRequest.get( - `/api/v2/workspace/${workspaceId}/identity-memberships`, + `/api/v1/projects/${projectId}/identity-memberships`, { params } ); return data; @@ -686,12 +588,12 @@ export const useGetWorkspaceIdentityMemberships = ( export const useGetWorkspaceIdentityMembershipDetails = (projectId: string, identityId: string) => { return useQuery({ enabled: Boolean(projectId && identityId), - queryKey: workspaceKeys.getWorkspaceIdentityMembershipDetails(projectId, identityId), + queryKey: projectKeys.getProjectIdentityMembershipDetails(projectId, identityId), queryFn: async () => { const { data: { identityMembership } } = await apiRequest.get<{ identityMembership: IdentityMembership }>( - `/api/v2/workspace/${projectId}/identity-memberships/${identityId}` + `/api/v1/projects/${projectId}/identity-memberships/${identityId}` ); return identityMembership; } @@ -701,12 +603,12 @@ export const useGetWorkspaceIdentityMembershipDetails = (projectId: string, iden export const useGetWorkspaceGroupMembershipDetails = (projectId: string, groupId: string) => { return useQuery({ enabled: Boolean(projectId && groupId), - queryKey: workspaceKeys.getWorkspaceGroupMembershipDetails(projectId, groupId), + queryKey: projectKeys.getProjectGroupMembershipDetails(projectId, groupId), queryFn: async () => { const { data: { groupMembership } } = await apiRequest.get<{ groupMembership: TGroupMembership }>( - `/api/v2/workspace/${projectId}/groups/${groupId}` + `/api/v1/projects/${projectId}/groups/${groupId}` ); return groupMembership; } @@ -715,12 +617,12 @@ export const useGetWorkspaceGroupMembershipDetails = (projectId: string, groupId export const useListWorkspaceGroups = (projectId: string) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId), + queryKey: projectKeys.getProjectGroupMemberships(projectId), queryFn: async () => { const { data: { groupMemberships } } = await apiRequest.get<{ groupMemberships: TGroupMembership[] }>( - `/api/v2/workspace/${projectId}/groups` + `/api/v1/projects/${projectId}/groups` ); return groupMemberships; }, @@ -729,15 +631,15 @@ export const useListWorkspaceGroups = (projectId: string) => { }; export const useListWorkspaceCas = ({ - projectSlug, + projectId, status }: { - projectSlug: string; + projectId: string; status?: CaStatus; }) => { return useQuery({ - queryKey: workspaceKeys.specificWorkspaceCas({ - projectSlug, + queryKey: projectKeys.specificProjectCas({ + projectId, status }), queryFn: async () => { @@ -748,29 +650,29 @@ export const useListWorkspaceCas = ({ const { data: { cas } } = await apiRequest.get<{ cas: TCertificateAuthority[] }>( - `/api/v2/workspace/${projectSlug}/cas`, + `/api/v1/projects/${projectId}/cas`, { params } ); return cas; }, - enabled: Boolean(projectSlug) + enabled: Boolean(projectId) }); }; export const useListWorkspaceCertificates = ({ - projectSlug, + projectId, offset, limit }: { - projectSlug: string; + projectId: string; offset: number; limit: number; }) => { return useQuery({ - queryKey: workspaceKeys.specificWorkspaceCertificates({ - slug: projectSlug, + queryKey: projectKeys.specificProjectCertificates({ + projectId, offset, limit }), @@ -783,7 +685,7 @@ export const useListWorkspaceCertificates = ({ const { data: { certificates, totalCount } } = await apiRequest.get<{ certificates: TCertificate[]; totalCount: number }>( - `/api/v2/workspace/${projectSlug}/certificates`, + `/api/v1/projects/${projectId}/certificates`, { params } @@ -791,55 +693,53 @@ export const useListWorkspaceCertificates = ({ return { certificates, totalCount }; }, - enabled: Boolean(projectSlug) + enabled: Boolean(projectId) }); }; -export const useListWorkspacePkiAlerts = ({ workspaceId }: { workspaceId: string }) => { +export const useListWorkspacePkiAlerts = ({ projectId }: { projectId: string }) => { return useQuery({ - queryKey: workspaceKeys.getWorkspacePkiAlerts(workspaceId), + queryKey: projectKeys.getProjectPkiAlerts(projectId), queryFn: async () => { const { data: { alerts } - } = await apiRequest.get<{ alerts: TPkiAlert[] }>( - `/api/v2/workspace/${workspaceId}/pki-alerts` - ); + } = await apiRequest.get<{ alerts: TPkiAlert[] }>(`/api/v1/projects/${projectId}/pki-alerts`); return { alerts }; }, - enabled: Boolean(workspaceId) + enabled: Boolean(projectId) }); }; -export const useListWorkspacePkiCollections = ({ workspaceId }: { workspaceId: string }) => { +export const useListWorkspacePkiCollections = ({ projectId }: { projectId: string }) => { return useQuery({ - queryKey: workspaceKeys.getWorkspacePkiCollections(workspaceId), + queryKey: projectKeys.getProjectPkiCollections(projectId), queryFn: async () => { const { data: { collections } } = await apiRequest.get<{ collections: TPkiCollection[] }>( - `/api/v2/workspace/${workspaceId}/pki-collections` + `/api/v1/projects/${projectId}/pki-collections` ); return { collections }; }, - enabled: Boolean(workspaceId) + enabled: Boolean(projectId) }); }; -export const useListWorkspaceCertificateTemplates = ({ workspaceId }: { workspaceId: string }) => { +export const useListWorkspaceCertificateTemplates = ({ projectId }: { projectId: string }) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceCertificateTemplates(workspaceId), + queryKey: projectKeys.getProjectCertificateTemplates(projectId), queryFn: async () => { const { data: { certificateTemplates } } = await apiRequest.get<{ certificateTemplates: TCertificateTemplate[] }>( - `/api/v2/workspace/${workspaceId}/certificate-templates` + `/api/v1/projects/${projectId}/certificate-templates` ); return { certificateTemplates }; }, - enabled: Boolean(workspaceId) + enabled: Boolean(projectId) }); }; @@ -853,7 +753,7 @@ export const useListWorkspaceSshCertificates = ({ projectId: string; }) => { return useQuery({ - queryKey: workspaceKeys.specificWorkspaceSshCertificates({ + queryKey: projectKeys.specificProjectSshCertificates({ offset, limit, projectId @@ -867,7 +767,7 @@ export const useListWorkspaceSshCertificates = ({ const { data } = await apiRequest.get<{ certificates: TSshCertificate[]; totalCount: number; - }>(`/api/v2/workspace/${projectId}/ssh-certificates`, { + }>(`/api/v1/projects/${projectId}/ssh-certificates`, { params }); return data; @@ -878,12 +778,12 @@ export const useListWorkspaceSshCertificates = ({ export const useListWorkspaceSshCas = (projectId: string) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceSshCas(projectId), + queryKey: projectKeys.getProjectSshCas(projectId), queryFn: async () => { const { data: { cas } } = await apiRequest.get<{ cas: Omit[] }>( - `/api/v2/workspace/${projectId}/ssh-cas` + `/api/v1/projects/${projectId}/ssh-cas` ); return cas; }, @@ -893,11 +793,11 @@ export const useListWorkspaceSshCas = (projectId: string) => { export const useListWorkspaceSshHosts = (projectId: string) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceSshHosts(projectId), + queryKey: projectKeys.getProjectSshHosts(projectId), queryFn: async () => { const { data: { hosts } - } = await apiRequest.get<{ hosts: TSshHost[] }>(`/api/v2/workspace/${projectId}/ssh-hosts`); + } = await apiRequest.get<{ hosts: TSshHost[] }>(`/api/v1/projects/${projectId}/ssh-hosts`); return hosts; }, enabled: Boolean(projectId) @@ -906,12 +806,12 @@ export const useListWorkspaceSshHosts = (projectId: string) => { export const useListWorkspacePkiSubscribers = (projectId: string) => { return useQuery({ - queryKey: workspaceKeys.getWorkspacePkiSubscribers(projectId), + queryKey: projectKeys.getProjectPkiSubscribers(projectId), queryFn: async () => { const { data: { subscribers } } = await apiRequest.get<{ subscribers: TPkiSubscriber[] }>( - `/api/v2/workspace/${projectId}/pki-subscribers` + `/api/v1/projects/${projectId}/pki-subscribers` ); return subscribers; }, @@ -921,12 +821,12 @@ export const useListWorkspacePkiSubscribers = (projectId: string) => { export const useListWorkspaceSshHostGroups = (projectId: string) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId), + queryKey: projectKeys.getProjectSshHostGroups(projectId), queryFn: async () => { const { data: { groups } } = await apiRequest.get<{ groups: (TSshHostGroup & { hostCount: number })[] }>( - `/api/v2/workspace/${projectId}/ssh-host-groups` + `/api/v1/projects/${projectId}/ssh-host-groups` ); return groups; }, @@ -936,10 +836,10 @@ export const useListWorkspaceSshHostGroups = (projectId: string) => { export const useListWorkspaceSshCertificateTemplates = (projectId: string) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceSshCertificateTemplates(projectId), + queryKey: projectKeys.getProjectSshCertificateTemplates(projectId), queryFn: async () => { const { data } = await apiRequest.get<{ certificateTemplates: TSshCertificateTemplate[] }>( - `/api/v2/workspace/${projectId}/ssh-certificate-templates` + `/api/v1/projects/${projectId}/ssh-certificate-templates` ); return data; }, @@ -948,18 +848,18 @@ export const useListWorkspaceSshCertificateTemplates = (projectId: string) => { }; export const useGetWorkspaceWorkflowIntegrationConfig = ({ - workspaceId, + projectId, integration }: { - workspaceId: string; + projectId: string; integration: WorkflowIntegrationPlatform; }) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(workspaceId, integration), + queryKey: projectKeys.getProjectWorkflowIntegrationConfig(projectId, integration), queryFn: async () => { const { data } = await apiRequest .get( - `/api/v1/workspace/${workspaceId}/workflow-integration-config/${integration}` + `/api/v1/projects/${projectId}/workflow-integration-config/${integration}` ) .catch((err) => { if (err.response.status === 404) { @@ -971,16 +871,16 @@ export const useGetWorkspaceWorkflowIntegrationConfig = ({ return data; }, - enabled: Boolean(workspaceId) + enabled: Boolean(projectId) }); }; export const useGetProjectSshConfig = (projectId: string) => { return useQuery({ - queryKey: workspaceKeys.getProjectSshConfig(projectId), + queryKey: projectKeys.getProjectSshConfig(projectId), queryFn: async () => { const { data } = await apiRequest.get( - `/api/v1/workspace/${projectId}/ssh-config` + `/api/v1/projects/${projectId}/ssh-config` ); return data; diff --git a/frontend/src/hooks/api/projects/query-keys.tsx b/frontend/src/hooks/api/projects/query-keys.tsx new file mode 100644 index 000000000..51e3db71c --- /dev/null +++ b/frontend/src/hooks/api/projects/query-keys.tsx @@ -0,0 +1,77 @@ +import type { CaStatus } from "../ca"; +import { WorkflowIntegrationPlatform } from "../workflowIntegrations/types"; +import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "./types"; + +export const projectKeys = { + getProjectById: (projectId: string) => ["projects", { projectId }] as const, + getProjectSecrets: (projectId: string) => [{ projectId }, "project-secrets"] as const, + getProjectIndexStatus: (projectId: string) => [{ projectId }, "project-index-status"] as const, + getProjectUpgradeStatus: (projectId: string) => [{ projectId }, "project-upgrade-status"], + getProjectMemberships: (orgId: string) => [{ orgId }, "project-memberships"], + getProjectAuthorization: (projectId: string) => [{ projectId }, "project-authorizations"], + getProjectIntegrations: (projectId: string) => [{ projectId }, "project-integrations"], + getAllUserProjects: () => ["projects"] as const, + getProjectAuditLogs: (projectId: string) => [{ projectId }, "project-audit-logs"] as const, + getProjectUsers: ( + projectId: string, + includeGroupMembers: boolean = false, + roles: string[] = [] + ) => [{ projectId, includeGroupMembers, roles }, "project-users"] as const, + getProjectUserDetails: (projectId: string, membershipId: string) => + [{ projectId, membershipId }, "project-user-details"] as const, + getProjectIdentityMemberships: (projectId: string) => + [{ projectId }, "project-identity-memberships"] as const, + getProjectIdentityMembershipDetails: (projectId: string, identityId: string) => + [{ projectId, identityId }, "project-identity-membership-details"] as const, + // allows invalidation using above key without knowing params + getProjectIdentityMembershipsWithParams: ({ projectId, ...params }: TListProjectIdentitiesDTO) => + [...projectKeys.getProjectIdentityMemberships(projectId), params] as const, + searchProject: (dto: TSearchProjectsDTO) => ["search-projects", dto] as const, + getProjectGroupMemberships: (projectId: string) => [{ projectId }, "project-groups"] as const, + getProjectGroupMembershipDetails: (projectId: string, groupId: string) => + [{ projectId, groupId }, "project-group-membership-details"] as const, + getProjectCas: ({ projectId }: { projectId: string }) => [{ projectId }, "project-cas"] as const, + specificProjectCas: ({ projectId, status }: { projectId: string; status?: CaStatus }) => + [...projectKeys.getProjectCas({ projectId }), { status }] as const, + allProjectCertificates: () => ["project-certificates"] as const, + forProjectCertificates: (projectId: string) => + [...projectKeys.allProjectCertificates(), projectId] as const, + specificProjectCertificates: ({ + projectId, + offset, + limit + }: { + projectId: string; + offset: number; + limit: number; + }) => [...projectKeys.forProjectCertificates(projectId), { offset, limit }] as const, + getProjectPkiAlerts: (projectId: string) => [{ projectId }, "project-pki-alerts"] as const, + getProjectPkiSubscribers: (projectId: string) => + [{ projectId }, "project-pki-subscribers"] as const, + getProjectPkiCollections: (projectId: string) => + [{ projectId }, "project-pki-collections"] as const, + getProjectCertificateTemplates: (projectId: string) => + [{ projectId }, "project-certificate-templates"] as const, + getProjectWorkflowIntegrationConfig: ( + projectId: string, + integration: WorkflowIntegrationPlatform + ) => [{ projectId, integration }, "project-workflow-integration-config"] as const, + getProjectSshCas: (projectId: string) => [{ projectId }, "project-ssh-cas"] as const, + allProjectSshCertificates: (projectId: string) => + [{ projectId }, "project-ssh-certificates"] as const, + getProjectSshHosts: (projectId: string) => [{ projectId }, "project-ssh-hosts"] as const, + getProjectSshHostGroups: (projectId: string) => + [{ projectId }, "project-ssh-host-groups"] as const, + specificProjectSshCertificates: ({ + offset, + limit, + projectId + }: { + offset: number; + limit: number; + projectId: string; + }) => [...projectKeys.allProjectSshCertificates(projectId), { offset, limit }] as const, + getProjectSshCertificateTemplates: (projectId: string) => + [{ projectId }, "project-ssh-certificate-templates"] as const, + getProjectSshConfig: (projectId: string) => [{ projectId }, "project-ssh-config"] as const +}; diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/projects/types.ts similarity index 84% rename from frontend/src/hooks/api/workspace/types.ts rename to frontend/src/hooks/api/projects/types.ts index 33eb0909f..59b242d05 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/projects/types.ts @@ -20,7 +20,7 @@ export enum ProjectUserMembershipTemporaryMode { Relative = "relative" } -export type Workspace = { +export type Project = { __v: number; id: string; name: string; @@ -31,7 +31,7 @@ export type Workspace = { upgradeStatus: string | null; updatedAt: string; autoCapitalization: boolean; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; pitVersionLimit: number; auditLogsRetentionDays: number; slug: string; @@ -43,16 +43,16 @@ export type Workspace = { secretDetectionIgnoreValues: string[]; }; -export type WorkspaceEnv = { +export type ProjectEnv = { id: string; name: string; slug: string; }; -export type WorkspaceTag = { id: string; name: string; slug: string }; +export type ProjectTag = { id: string; name: string; slug: string }; export type NameWorkspaceSecretsDTO = { - workspaceId: string; + projectId: string; secretsToUpdate: { secretName: string; secretId: string; @@ -76,30 +76,33 @@ export type CreateWorkspaceDTO = { }; export type UpdateProjectDTO = { - projectID: string; + projectId: string; newProjectName?: string; newProjectDescription?: string; newSlug?: string; secretSharing?: boolean; showSnapshotsLegacy?: boolean; secretDetectionIgnoreValues?: string[]; + pitVersionLimit?: number; + autoCapitalization?: boolean; + hasDeleteProtection?: boolean; }; export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number }; export type UpdateAuditLogsRetentionDTO = { projectSlug: string; auditLogsRetentionDays: number }; -export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean }; -export type ToggleDeleteProjectProtectionDTO = { workspaceID: string; state: boolean }; +export type ToggleAutoCapitalizationDTO = { projectID: string; state: boolean }; +export type ToggleDeleteProjectProtectionDTO = { projectID: string; state: boolean }; -export type DeleteWorkspaceDTO = { workspaceID: string }; +export type DeleteWorkspaceDTO = { projectID: string }; export type CreateEnvironmentDTO = { - workspaceId: string; + projectId: string; name: string; slug: string; }; export type ReorderEnvironmentsDTO = { - workspaceId: string; + projectId: string; environmentSlug: string; environmentName: string; otherEnvironmentSlug: string; @@ -107,18 +110,18 @@ export type ReorderEnvironmentsDTO = { }; export type UpdateEnvironmentDTO = { - workspaceId: string; + projectId: string; id: string; name?: string; slug?: string; position?: number; }; -export type DeleteEnvironmentDTO = { workspaceId: string; id: string }; +export type DeleteEnvironmentDTO = { projectId: string; id: string }; export type TUpdateWorkspaceUserRoleDTO = { membershipId: string; - workspaceId: string; + projectId: string; roles: ( | { role: string; @@ -136,7 +139,7 @@ export type TUpdateWorkspaceUserRoleDTO = { export type TUpdateWorkspaceIdentityRoleDTO = { identityId: string; - workspaceId: string; + projectId: string; roles: ( | { role: string; @@ -171,7 +174,7 @@ export type TUpdateWorkspaceGroupRoleDTO = { }; export type TListProjectIdentitiesDTO = { - workspaceId: string; + projectId: string; offset?: number; limit?: number; orderBy?: ProjectIdentityOrderBy; @@ -186,6 +189,7 @@ export type TSearchProjectsDTO = { name?: string; limit?: number; offset?: number; + projectIds?: string[]; type?: ProjectType; options?: { enabled?: boolean }; orderBy?: ProjectIdentityOrderBy; diff --git a/frontend/src/hooks/api/roles/mutation.tsx b/frontend/src/hooks/api/roles/mutation.tsx index 6df9aa937..e9caa68d5 100644 --- a/frontend/src/hooks/api/roles/mutation.tsx +++ b/frontend/src/hooks/api/roles/mutation.tsx @@ -22,7 +22,7 @@ export const useCreateProjectRole = () => { mutationFn: async ({ projectId, ...dto }: TCreateProjectRoleDTO) => { const { data: { role } - } = await apiRequest.post(`/api/v2/workspace/${projectId}/roles`, dto); + } = await apiRequest.post(`/api/v1/projects/${projectId}/roles`, dto); return role; }, onSuccess: (_, { projectId }) => { @@ -38,7 +38,7 @@ export const useUpdateProjectRole = () => { mutationFn: async ({ id, projectId, ...dto }: TUpdateProjectRoleDTO) => { const { data: { role } - } = await apiRequest.patch(`/api/v2/workspace/${projectId}/roles/${id}`, dto); + } = await apiRequest.patch(`/api/v1/projects/${projectId}/roles/${id}`, dto); return role; }, onSuccess: (_, { projectId, slug }) => { @@ -58,7 +58,7 @@ export const useDeleteProjectRole = () => { mutationFn: async ({ projectId, id }: TDeleteProjectRoleDTO) => { const { data: { role } - } = await apiRequest.delete(`/api/v2/workspace/${projectId}/roles/${id}`); + } = await apiRequest.delete(`/api/v1/projects/${projectId}/roles/${id}`); return role; }, onSuccess: (_, { projectId }) => { diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx index a406b6d45..fd7b28db9 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -46,13 +46,13 @@ export const roleQueryKeys = { getOrgRole: (orgId: string, roleId: string) => [{ orgId, roleId }, "org-role"] as const, getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) => ["user-permissions", { orgId }] as const, - getUserProjectPermissions: ({ workspaceId }: TGetUserProjectPermissionDTO) => - ["user-project-permissions", { workspaceId }] as const + getUserProjectPermissions: ({ projectId }: TGetUserProjectPermissionDTO) => + ["user-project-permissions", { projectId }] as const }; export const getProjectRoles = async (projectId: string) => { const { data } = await apiRequest.get<{ roles: Array> }>( - `/api/v2/workspace/${projectId}/roles` + `/api/v1/projects/${projectId}/roles` ); return data.roles; }; @@ -69,7 +69,7 @@ export const useGetProjectRoleBySlug = (projectId: string, roleSlug: string) => queryKey: roleQueryKeys.getProjectRoleBySlug(projectId, roleSlug), queryFn: async () => { const { data } = await apiRequest.get<{ role: TProjectRole }>( - `/api/v2/workspace/${projectId}/roles/slug/${roleSlug}` + `/api/v1/projects/${projectId}/roles/slug/${roleSlug}` ); return data.role; }, @@ -131,9 +131,7 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) = } }); -export const fetchUserProjectPermissions = async ({ - workspaceId -}: TGetUserProjectPermissionDTO) => { +export const fetchUserProjectPermissions = async ({ projectId }: TGetUserProjectPermissionDTO) => { const { data } = await apiRequest.get<{ data: { permissions: PackRule>>[]; @@ -145,16 +143,16 @@ export const fetchUserProjectPermissions = async ({ actorName: string; }; }; - }>(`/api/v1/workspace/${workspaceId}/permissions`, {}); + }>(`/api/v1/projects/${projectId}/permissions`, {}); return data.data; }; -export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPermissionDTO) => +export const useGetUserProjectPermissions = ({ projectId }: TGetUserProjectPermissionDTO) => useQuery({ - queryKey: roleQueryKeys.getUserProjectPermissions({ workspaceId }), - queryFn: () => fetchUserProjectPermissions({ workspaceId }), - enabled: Boolean(workspaceId), + queryKey: roleQueryKeys.getUserProjectPermissions({ projectId }), + queryFn: () => fetchUserProjectPermissions({ projectId }), + enabled: Boolean(projectId), select: (data) => { const rule = unpackRules>>(data.permissions); const negatedRules = groupBy( diff --git a/frontend/src/hooks/api/roles/types.ts b/frontend/src/hooks/api/roles/types.ts index 12286bf9b..e94f13305 100644 --- a/frontend/src/hooks/api/roles/types.ts +++ b/frontend/src/hooks/api/roles/types.ts @@ -9,7 +9,7 @@ export enum ProjectMembershipRole { } export type TGetProjectRolesDTO = { - workspaceId?: string; + projectId?: string; }; export type TProjectRole = { @@ -52,7 +52,7 @@ export type TGetUserOrgPermissionsDTO = { }; export type TGetUserProjectPermissionDTO = { - workspaceId: string; + projectId: string; }; export type TCreateOrgRoleDTO = { diff --git a/frontend/src/hooks/api/secretApproval/mutation.tsx b/frontend/src/hooks/api/secretApproval/mutation.tsx index 8370d0367..c8fe51d19 100644 --- a/frontend/src/hooks/api/secretApproval/mutation.tsx +++ b/frontend/src/hooks/api/secretApproval/mutation.tsx @@ -11,7 +11,7 @@ export const useCreateSecretApprovalPolicy = () => { return useMutation({ mutationFn: async ({ environments, - workspaceId, + projectId, approvals, approvers, bypassers, @@ -20,9 +20,9 @@ export const useCreateSecretApprovalPolicy = () => { enforcementLevel, allowedSelfApprovals }) => { - const { data } = await apiRequest.post("/api/v1/secret-approvals", { + const { data } = await apiRequest.post("/api/v2/secret-approvals", { environments, - workspaceId, + projectId, approvals, approvers, bypassers, @@ -33,9 +33,9 @@ export const useCreateSecretApprovalPolicy = () => { }); return data; }, - onSuccess: (_, { workspaceId }) => { + onSuccess: (_, { projectId }) => { queryClient.invalidateQueries({ - queryKey: secretApprovalKeys.getApprovalPolicies(workspaceId) + queryKey: secretApprovalKeys.getApprovalPolicies(projectId) }); } }); @@ -56,7 +56,7 @@ export const useUpdateSecretApprovalPolicy = () => { allowedSelfApprovals, environments }) => { - const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { + const { data } = await apiRequest.patch(`/api/v2/secret-approvals/${id}`, { approvals, approvers, bypassers, @@ -68,9 +68,9 @@ export const useUpdateSecretApprovalPolicy = () => { }); return data; }, - onSuccess: (_, { workspaceId }) => { + onSuccess: (_, { projectId }) => { queryClient.invalidateQueries({ - queryKey: secretApprovalKeys.getApprovalPolicies(workspaceId) + queryKey: secretApprovalKeys.getApprovalPolicies(projectId) }); } }); @@ -81,12 +81,12 @@ export const useDeleteSecretApprovalPolicy = () => { return useMutation({ mutationFn: async ({ id }) => { - const { data } = await apiRequest.delete(`/api/v1/secret-approvals/${id}`); + const { data } = await apiRequest.delete(`/api/v2/secret-approvals/${id}`); return data; }, - onSuccess: (_, { workspaceId }) => { + onSuccess: (_, { projectId }) => { queryClient.invalidateQueries({ - queryKey: secretApprovalKeys.getApprovalPolicies(workspaceId) + queryKey: secretApprovalKeys.getApprovalPolicies(projectId) }); } }); diff --git a/frontend/src/hooks/api/secretApproval/queries.tsx b/frontend/src/hooks/api/secretApproval/queries.tsx index 52c3743fb..e81af93b8 100644 --- a/frontend/src/hooks/api/secretApproval/queries.tsx +++ b/frontend/src/hooks/api/secretApproval/queries.tsx @@ -10,54 +10,53 @@ import { } from "./types"; export const secretApprovalKeys = { - getApprovalPolicies: (workspaceId: string) => - [{ workspaceId }, "secret-approval-policies"] as const, - getApprovalPolicyOfABoard: (workspaceId: string, environment: string, secretPath: string) => [ - { workspaceId, environment, secretPath }, + getApprovalPolicies: (projectId: string) => [{ projectId }, "secret-approval-policies"] as const, + getApprovalPolicyOfABoard: (projectId: string, environment: string, secretPath: string) => [ + { projectId, environment, secretPath }, "Secret-approval-policy" ] }; -const fetchApprovalPolicies = async (workspaceId: string) => { +const fetchApprovalPolicies = async (projectId: string) => { const { data } = await apiRequest.get<{ approvals: TSecretApprovalPolicy[] }>( - "/api/v1/secret-approvals", - { params: { workspaceId } } + "/api/v2/secret-approvals", + { params: { projectId } } ); return data.approvals; }; export const useGetSecretApprovalPolicies = ({ - workspaceId, + projectId, options = {} }: TGetSecretApprovalPoliciesDTO & TReactQueryOptions) => useQuery({ - queryKey: secretApprovalKeys.getApprovalPolicies(workspaceId), - queryFn: () => fetchApprovalPolicies(workspaceId), + queryKey: secretApprovalKeys.getApprovalPolicies(projectId), + queryFn: () => fetchApprovalPolicies(projectId), ...options, - enabled: Boolean(workspaceId) && (options?.enabled ?? true) + enabled: Boolean(projectId) && (options?.enabled ?? true) }); const fetchApprovalPolicyOfABoard = async ( - workspaceId: string, + projectId: string, environment: string, secretPath: string ) => { const { data } = await apiRequest.get<{ policy: TSecretApprovalPolicy }>( - "/api/v1/secret-approvals/board", - { params: { workspaceId, environment, secretPath } } + "/api/v2/secret-approvals/board", + { params: { projectId, environment, secretPath } } ); return data.policy || ""; }; export const useGetSecretApprovalPolicyOfABoard = ({ - workspaceId, + projectId, secretPath = "/", environment, options = {} }: TGetSecretApprovalPolicyOfBoardDTO & TReactQueryOptions) => useQuery({ - queryKey: secretApprovalKeys.getApprovalPolicyOfABoard(workspaceId, environment, secretPath), - queryFn: () => fetchApprovalPolicyOfABoard(workspaceId, environment, secretPath), + queryKey: secretApprovalKeys.getApprovalPolicyOfABoard(projectId, environment, secretPath), + queryFn: () => fetchApprovalPolicyOfABoard(projectId, environment, secretPath), ...options, - enabled: Boolean(workspaceId && secretPath && environment) && (options?.enabled ?? true) + enabled: Boolean(projectId && secretPath && environment) && (options?.enabled ?? true) }); diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index 1785ffa8a..ac912c401 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -1,11 +1,11 @@ import { EnforcementLevel } from "../policies/enums"; -import { WorkspaceEnv } from "../workspace/types"; +import { ProjectEnv } from "../projects/types"; export type TSecretApprovalPolicy = { id: string; - workspace: string; + project: string; name: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; secretPath?: string; approvals: number; approvers: Approver[]; @@ -36,17 +36,17 @@ export type Bypasser = { }; export type TGetSecretApprovalPoliciesDTO = { - workspaceId: string; + projectId: string; }; export type TGetSecretApprovalPolicyOfBoardDTO = { - workspaceId: string; + projectId: string; environment: string; secretPath: string; }; export type TCreateSecretPolicyDTO = { - workspaceId: string; + projectId: string; name?: string; environments: string[]; secretPath: string; @@ -67,12 +67,12 @@ export type TUpdateSecretPolicyDTO = { allowedSelfApprovals?: boolean; enforcementLevel?: EnforcementLevel; // for invalidating list - workspaceId: string; + projectId: string; environments?: string[]; }; export type TDeleteSecretPolicyDTO = { id: string; // for invalidating list - workspaceId: string; + projectId: string; }; diff --git a/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx b/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx index 78e1b37a1..6a03b431c 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx +++ b/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx @@ -36,9 +36,9 @@ export const useUpdateSecretApprovalRequestStatus = () => { }); return data; }, - onSuccess: (_, { id, workspaceId }) => { + onSuccess: (_, { id, projectId }) => { queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.detail({ id }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); } }); }; @@ -53,10 +53,10 @@ export const usePerformSecretApprovalRequestMerge = () => { }); return data; }, - onSuccess: (_, { id, workspaceId }) => { + onSuccess: (_, { id, projectId }) => { queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.detail({ id }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.list({ workspaceId }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.list({ projectId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); } }); }; diff --git a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx index a1736f70f..63cdd455e 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx +++ b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx @@ -14,7 +14,7 @@ import { export const secretApprovalRequestKeys = { list: ({ - workspaceId, + projectId, environment, status, committer, @@ -23,20 +23,20 @@ export const secretApprovalRequestKeys = { search }: TGetSecretApprovalRequestList) => [ - { workspaceId, environment, status, committer, offset, limit, search }, + { projectId, environment, status, committer, offset, limit, search }, "secret-approval-requests" ] as const, detail: ({ id }: Omit) => [{ id }, "secret-approval-request-detail"] as const, - count: ({ workspaceId, policyId }: TGetSecretApprovalRequestCount) => [ - { workspaceId }, + count: ({ projectId, policyId }: TGetSecretApprovalRequestCount) => [ + { projectId }, "secret-approval-request-count", ...(policyId ? [policyId] : []) ] }; const fetchSecretApprovalRequestList = async ({ - workspaceId, + projectId, environment, committer, status = "open", @@ -49,7 +49,7 @@ const fetchSecretApprovalRequestList = async ({ totalCount: number; }>("/api/v1/secret-approval-requests", { params: { - workspaceId, + projectId, environment, committer, status, @@ -63,7 +63,7 @@ const fetchSecretApprovalRequestList = async ({ }; export const useGetSecretApprovalRequests = ({ - workspaceId, + projectId, environment, options = {}, status, @@ -74,7 +74,7 @@ export const useGetSecretApprovalRequests = ({ }: TGetSecretApprovalRequestList & TReactQueryOptions) => useQuery({ queryKey: secretApprovalRequestKeys.list({ - workspaceId, + projectId, environment, committer, status, @@ -84,7 +84,7 @@ export const useGetSecretApprovalRequests = ({ }), queryFn: () => fetchSecretApprovalRequestList({ - workspaceId, + projectId, environment, status, committer, @@ -92,7 +92,7 @@ export const useGetSecretApprovalRequests = ({ offset, search }), - enabled: Boolean(workspaceId) && (options?.enabled ?? true), + enabled: Boolean(projectId) && (options?.enabled ?? true), placeholderData: (previousData) => previousData }); @@ -127,19 +127,19 @@ export const useGetSecretApprovalRequestDetails = ({ }); const fetchSecretApprovalRequestCount = async ({ - workspaceId, + projectId, policyId }: TGetSecretApprovalRequestCount) => { const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequestCount }>( "/api/v1/secret-approval-requests/count", - { params: { workspaceId, policyId } } + { params: { projectId, policyId } } ); return data.approvals; }; export const useGetSecretApprovalRequestCount = ({ - workspaceId, + projectId, policyId, options = {} }: TGetSecretApprovalRequestCount & { @@ -154,8 +154,8 @@ export const useGetSecretApprovalRequestCount = ({ >; }) => useQuery({ - queryKey: secretApprovalRequestKeys.count({ workspaceId, policyId }), + queryKey: secretApprovalRequestKeys.count({ projectId, policyId }), refetchInterval: 15000, - queryFn: () => fetchSecretApprovalRequestCount({ workspaceId, policyId }), - enabled: Boolean(workspaceId) && (options?.enabled ?? true) + queryFn: () => fetchSecretApprovalRequestCount({ projectId, policyId }), + enabled: Boolean(projectId) && (options?.enabled ?? true) }); diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts index 0eef3fa3d..5a381e871 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/types.ts +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -55,7 +55,7 @@ export type TSecretApprovalRequest = { username: string; isOrgMembershipActive: boolean; }[]; - workspace: string; + project: string; environment: string; folderId: string; secretPath: string; @@ -110,7 +110,7 @@ export type TSecretApprovalRequestCount = { }; export type TGetSecretApprovalRequestList = { - workspaceId: string; + projectId: string; environment?: string; status?: "open" | "close"; committer?: string; @@ -120,7 +120,7 @@ export type TGetSecretApprovalRequestList = { }; export type TGetSecretApprovalRequestCount = { - workspaceId: string; + projectId: string; policyId?: string; }; @@ -137,11 +137,11 @@ export type TUpdateSecretApprovalReviewStatusDTO = { export type TUpdateSecretApprovalRequestStatusDTO = { status: "open" | "close"; id: string; - workspaceId: string; + projectId: string; }; export type TPerformSecretApprovalRequestMerge = { id: string; - workspaceId: string; + projectId: string; bypassReason?: string; }; diff --git a/frontend/src/hooks/api/secretFolders/queries.tsx b/frontend/src/hooks/api/secretFolders/queries.tsx index 9e12f329d..3e752eed3 100644 --- a/frontend/src/hooks/api/secretFolders/queries.tsx +++ b/frontend/src/hooks/api/secretFolders/queries.tsx @@ -30,10 +30,10 @@ export const folderQueryKeys = { ["secret-folders", "environment", projectId] as const }; -const fetchProjectFolders = async (workspaceId: string, environment: string, path = "/") => { - const { data } = await apiRequest.get<{ folders: TSecretFolder[] }>("/api/v1/folders", { +const fetchProjectFolders = async (projectId: string, environment: string, path = "/") => { + const { data } = await apiRequest.get<{ folders: TSecretFolder[] }>("/api/v2/folders", { params: { - workspaceId, + projectId, environment, path } @@ -57,7 +57,7 @@ export const useListProjectEnvironmentsFolders = ( queryKey: folderQueryKeys.getProjectEnvironmentsFolders(projectId), queryFn: async () => { const { data } = await apiRequest.get( - `/api/v1/workspace/${projectId}/environment-folder-tree` + `/api/v1/projects/${projectId}/environment-folder-tree` ); return data; }, @@ -145,9 +145,9 @@ export const useCreateFolder = () => { return useMutation({ mutationFn: async (dto) => { - const { data } = await apiRequest.post("/api/v1/folders", { + const { data } = await apiRequest.post("/api/v2/folders", { ...dto, - workspaceId: dto.projectId + projectId: dto.projectId }); return data; }, @@ -162,13 +162,13 @@ export const useCreateFolder = () => { queryKey: folderQueryKeys.getSecretFolders({ projectId, environment, path }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ workspaceId: projectId, environment, directory: path }) + queryKey: secretSnapshotKeys.list({ projectId, environment, directory: path }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path }) + queryKey: secretSnapshotKeys.count({ projectId, environment, directory: path }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ workspaceId: projectId, environment, directory: path }) + queryKey: commitKeys.count({ projectId, environment, directory: path }) }); } }); @@ -179,10 +179,10 @@ export const useUpdateFolder = () => { return useMutation({ mutationFn: async ({ path = "/", folderId, name, environment, projectId, description }) => { - const { data } = await apiRequest.patch(`/api/v1/folders/${folderId}`, { + const { data } = await apiRequest.patch(`/api/v2/folders/${folderId}`, { name, environment, - workspaceId: projectId, + projectId, path, description }); @@ -199,16 +199,16 @@ export const useUpdateFolder = () => { queryKey: folderQueryKeys.getSecretFolders({ projectId, environment, path }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ workspaceId: projectId, environment, directory: path }) + queryKey: secretSnapshotKeys.list({ projectId, environment, directory: path }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path }) + queryKey: secretSnapshotKeys.count({ projectId, environment, directory: path }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ workspaceId: projectId, environment, directory: path }) + queryKey: commitKeys.count({ projectId, environment, directory: path }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId: projectId, environment, directory: path }) + queryKey: commitKeys.history({ projectId, environment, directory: path }) }); } }); @@ -219,10 +219,10 @@ export const useDeleteFolder = () => { return useMutation({ mutationFn: async ({ path = "/", folderId, environment, projectId }) => { - const { data } = await apiRequest.delete(`/api/v1/folders/${folderId}`, { + const { data } = await apiRequest.delete(`/api/v2/folders/${folderId}`, { data: { environment, - workspaceId: projectId, + projectId, path } }); @@ -239,16 +239,16 @@ export const useDeleteFolder = () => { queryKey: folderQueryKeys.getSecretFolders({ projectId, environment, path }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ workspaceId: projectId, environment, directory: path }) + queryKey: secretSnapshotKeys.list({ projectId, environment, directory: path }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path }) + queryKey: secretSnapshotKeys.count({ projectId, environment, directory: path }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ workspaceId: projectId, environment, directory: path }) + queryKey: commitKeys.count({ projectId, environment, directory: path }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId: projectId, environment, directory: path }) + queryKey: commitKeys.history({ projectId, environment, directory: path }) }); } }); @@ -258,9 +258,9 @@ export const useUpdateFolderBatch = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ projectSlug, folders }) => { - const { data } = await apiRequest.patch("/api/v1/folders/batch", { - projectSlug, + mutationFn: async ({ projectId, folders }) => { + const { data } = await apiRequest.patch("/api/v2/folders/batch", { + projectId, folders }); @@ -283,28 +283,28 @@ export const useUpdateFolderBatch = () => { }); queryClient.invalidateQueries({ queryKey: secretSnapshotKeys.list({ - workspaceId: projectId, + projectId, environment: folder.environment, directory: folder.path }) }); queryClient.invalidateQueries({ queryKey: secretSnapshotKeys.count({ - workspaceId: projectId, + projectId, environment: folder.environment, directory: folder.path }) }); queryClient.invalidateQueries({ queryKey: commitKeys.count({ - workspaceId: projectId, + projectId, environment: folder.environment, directory: folder.path }) }); queryClient.invalidateQueries({ queryKey: commitKeys.history({ - workspaceId: projectId, + projectId, environment: folder.environment, directory: folder.path }) diff --git a/frontend/src/hooks/api/secretFolders/types.ts b/frontend/src/hooks/api/secretFolders/types.ts index dc9bce70f..e9ee2ae5d 100644 --- a/frontend/src/hooks/api/secretFolders/types.ts +++ b/frontend/src/hooks/api/secretFolders/types.ts @@ -1,4 +1,4 @@ -import { WorkspaceEnv } from "@app/hooks/api/workspace/types"; +import { ProjectEnv } from "@app/hooks/api/projects/types"; export enum ReservedFolders { SecretReplication = "__reserve_replication_" @@ -22,7 +22,7 @@ export type TSecretFolder = { export type TSecretFolderWithPath = TSecretFolder & { path: string }; export type TProjectEnvironmentsFolders = { - [key: string]: WorkspaceEnv & { folders: TSecretFolderWithPath[] }; + [key: string]: ProjectEnv & { folders: TSecretFolderWithPath[] }; }; export type TGetProjectFoldersDTO = { @@ -63,7 +63,6 @@ export type TDeleteFolderDTO = { export type TUpdateFolderBatchDTO = { projectId: string; - projectSlug: string; folders: { name: string; environment: string; diff --git a/frontend/src/hooks/api/secretImports/mutation.tsx b/frontend/src/hooks/api/secretImports/mutation.tsx index 72254bae2..c9484ccfb 100644 --- a/frontend/src/hooks/api/secretImports/mutation.tsx +++ b/frontend/src/hooks/api/secretImports/mutation.tsx @@ -16,10 +16,10 @@ export const useCreateSecretImport = () => { return useMutation({ mutationFn: async ({ import: secretImport, environment, isReplication, projectId, path }) => { - const { data } = await apiRequest.post("/api/v1/secret-imports", { + const { data } = await apiRequest.post("/api/v2/secret-imports", { import: secretImport, environment, - workspaceId: projectId, + projectId, path, isReplication }); @@ -44,11 +44,11 @@ export const useUpdateSecretImport = () => { return useMutation({ mutationFn: async ({ environment, import: secretImports, projectId, path, id }) => { - const { data } = await apiRequest.patch(`/api/v1/secret-imports/${id}`, { + const { data } = await apiRequest.patch(`/api/v2/secret-imports/${id}`, { import: secretImports, environment, path, - workspaceId: projectId + projectId }); return data; }, @@ -69,10 +69,10 @@ export const useUpdateSecretImport = () => { export const useResyncSecretReplication = () => { return useMutation({ mutationFn: async ({ environment, projectId, path, id }) => { - const { data } = await apiRequest.post(`/api/v1/secret-imports/${id}/replication-resync`, { + const { data } = await apiRequest.post(`/api/v2/secret-imports/${id}/replication-resync`, { environment, path, - workspaceId: projectId + projectId }); return data; } @@ -84,9 +84,9 @@ export const useDeleteSecretImport = () => { return useMutation({ mutationFn: async ({ id, projectId, path, environment }) => { - const { data } = await apiRequest.delete(`/api/v1/secret-imports/${id}`, { + const { data } = await apiRequest.delete(`/api/v2/secret-imports/${id}`, { data: { - workspaceId: projectId, + projectId, path, environment } diff --git a/frontend/src/hooks/api/secretImports/queries.tsx b/frontend/src/hooks/api/secretImports/queries.tsx index ea6ea68ef..71716cb7c 100644 --- a/frontend/src/hooks/api/secretImports/queries.tsx +++ b/frontend/src/hooks/api/secretImports/queries.tsx @@ -30,10 +30,10 @@ export const secretImportKeys = { const fetchSecretImport = async ({ projectId, environment, path = "/" }: TGetSecretImports) => { const { data } = await apiRequest.get<{ secretImports: TSecretImport[] }>( - "/api/v1/secret-imports", + "/api/v2/secret-imports", { params: { - workspaceId: projectId, + projectId, environment, path } @@ -65,16 +65,12 @@ export const useGetSecretImports = ({ queryFn: () => fetchSecretImport({ path, projectId, environment }) }); -const fetchImportedSecrets = async ( - workspaceId: string, - environment: string, - directory?: string -) => { +const fetchImportedSecrets = async (projectId: string, environment: string, directory?: string) => { const { data } = await apiRequest.get<{ secrets: TImportedSecrets[] }>( - "/api/v1/secret-imports/secrets/raw", + "/api/v2/secret-imports/secrets", { params: { - workspaceId, + projectId, environment, path: directory } @@ -89,10 +85,10 @@ const fetchImportedFolders = async ({ path }: TGetImportedFoldersByEnvDTO) => { const { data } = await apiRequest.get<{ secretImports: TSecretImport[] }>( - "/api/v1/secret-imports", + "/api/v2/secret-imports", { params: { - workspaceId: projectId, + projectId, environment, path } diff --git a/frontend/src/hooks/api/secretImports/types.ts b/frontend/src/hooks/api/secretImports/types.ts index d950c2ca2..23690b33d 100644 --- a/frontend/src/hooks/api/secretImports/types.ts +++ b/frontend/src/hooks/api/secretImports/types.ts @@ -1,11 +1,11 @@ +import { ProjectEnv } from "../projects/types"; import { SecretV3Raw } from "../secrets/types"; -import { WorkspaceEnv } from "../workspace/types"; export type TSecretImport = { id: string; folderId: string; importPath: string; - importEnv: WorkspaceEnv; + importEnv: ProjectEnv; position: string; createdAt: string; updatedAt: string; @@ -25,7 +25,7 @@ export type TGetImportedFoldersByEnvDTO = { export type TImportedSecrets = { environment: string; - environmentInfo: WorkspaceEnv; + environmentInfo: ProjectEnv; secretPath: string; folderId: string; secrets: SecretV3Raw[]; diff --git a/frontend/src/hooks/api/secretRotation/types.ts b/frontend/src/hooks/api/secretRotation/types.ts index 19c4cdcdf..c5f939c5c 100644 --- a/frontend/src/hooks/api/secretRotation/types.ts +++ b/frontend/src/hooks/api/secretRotation/types.ts @@ -1,4 +1,4 @@ -import { WorkspaceEnv } from "../workspace/types"; +import { ProjectEnv } from "../projects/types"; export enum TProviderFunctionTypes { HTTP = "http", @@ -80,7 +80,7 @@ export type TSecretRotation = { customProvider: string; workspace: string; envId: string; - environment: WorkspaceEnv; + environment: ProjectEnv; secretPath: string; outputs: Array<{ key: string; diff --git a/frontend/src/hooks/api/secretSnapshots/queries.tsx b/frontend/src/hooks/api/secretSnapshots/queries.tsx index daee39054..f5492918c 100644 --- a/frontend/src/hooks/api/secretSnapshots/queries.tsx +++ b/frontend/src/hooks/api/secretSnapshots/queries.tsx @@ -14,25 +14,25 @@ import { } from "./types"; export const secretSnapshotKeys = { - list: ({ workspaceId, environment, directory }: Omit) => - [{ workspaceId, environment, directory }, "secret-snapshot"] as const, + list: ({ projectId, environment, directory }: Omit) => + [{ projectId, environment, directory }, "secret-snapshot"] as const, snapshotData: (snapshotId: string) => [{ snapshotId }, "secret-snapshot"] as const, - count: ({ environment, workspaceId, directory }: Omit) => [ - { workspaceId, environment, directory }, + count: ({ environment, projectId, directory }: Omit) => [ + { projectId, environment, directory }, "count", "secret-snapshot" ] }; const fetchWorkspaceSnaphots = async ({ - workspaceId, + projectId, environment, directory = "/", limit = 10, offset = 0 }: TGetSecretSnapshotsDTO & { offset: number }) => { const res = await apiRequest.get<{ secretSnapshots: TSecretSnapshot[] }>( - `/api/v1/workspace/${workspaceId}/secret-snapshots`, + `/api/v1/projects/${projectId}/secret-snapshots`, { params: { limit, @@ -49,7 +49,7 @@ const fetchWorkspaceSnaphots = async ({ export const useGetWorkspaceSnapshotList = (dto: TGetSecretSnapshotsDTO & { isPaused?: boolean }) => useInfiniteQuery({ initialPageParam: 0, - enabled: Boolean(dto.workspaceId && dto.environment) && !dto.isPaused, + enabled: Boolean(dto.projectId && dto.environment) && !dto.isPaused, queryKey: secretSnapshotKeys.list({ ...dto }), queryFn: ({ pageParam }) => fetchWorkspaceSnaphots({ ...dto, offset: pageParam }), getNextPageParam: (lastPage, pages) => @@ -115,12 +115,12 @@ export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) => }); const fetchWorkspaceSecretSnaphotCount = async ( - workspaceId: string, + projectId: string, environment: string, directory = "/" ) => { const res = await apiRequest.get<{ count: number }>( - `/api/v1/workspace/${workspaceId}/secret-snapshots/count`, + `/api/v1/projects/${projectId}/secret-snapshots/count`, { params: { environment, @@ -132,15 +132,15 @@ const fetchWorkspaceSecretSnaphotCount = async ( }; export const useGetWsSnapshotCount = ({ - workspaceId, + projectId, environment, directory, isPaused }: Omit & { isPaused?: boolean }) => useQuery({ - enabled: Boolean(workspaceId && environment) && !isPaused, - queryKey: secretSnapshotKeys.count({ workspaceId, environment, directory }), - queryFn: () => fetchWorkspaceSecretSnaphotCount(workspaceId, environment, directory) + enabled: Boolean(projectId && environment) && !isPaused, + queryKey: secretSnapshotKeys.count({ projectId, environment, directory }), + queryFn: () => fetchWorkspaceSecretSnaphotCount(projectId, environment, directory) }); export const usePerformSecretRollback = () => { @@ -151,22 +151,22 @@ export const usePerformSecretRollback = () => { const { data } = await apiRequest.post(`/api/v1/secret-snapshot/${snapshotId}/rollback`); return data; }, - onSuccess: (_, { workspaceId, environment, directory }) => { + onSuccess: (_, { projectId, environment, directory }) => { queryClient.invalidateQueries({ - queryKey: [{ workspaceId, environment, secretPath: directory }, "secrets"] + queryKey: [{ projectId, environment, secretPath: directory }, "secrets"] }); queryClient.invalidateQueries({ - queryKey: ["secret-folders", { projectId: workspaceId, environment, path: directory }] + queryKey: ["secret-folders", { projectId, environment, path: directory }] }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ workspaceId, environment, directory }) + queryKey: secretSnapshotKeys.list({ projectId, environment, directory }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ workspaceId, environment, directory }) + queryKey: secretSnapshotKeys.count({ projectId, environment, directory }) }); queryClient.invalidateQueries({ queryKey: dashboardKeys.getDashboardSecrets({ - projectId: workspaceId, + projectId, secretPath: directory ?? "/" }) }); diff --git a/frontend/src/hooks/api/secretSnapshots/types.ts b/frontend/src/hooks/api/secretSnapshots/types.ts index e7ebb18de..9b1f95480 100644 --- a/frontend/src/hooks/api/secretSnapshots/types.ts +++ b/frontend/src/hooks/api/secretSnapshots/types.ts @@ -1,9 +1,9 @@ import { SecretVersions } from "../secrets/types"; -import { WorkspaceEnv } from "../types"; +import { ProjectEnv } from "../types"; export type TSecretSnapshot = { id: string; - workspace: string; + projectId: string; secretVersions: string[]; createdAt: string; updatedAt: string; @@ -13,7 +13,7 @@ export type TSnapshotData = Omit & { id: string; secretVersions: (SecretVersions & { isRotatedSecret?: boolean })[]; folderVersion: Array<{ name: string; id: string }>; - environment: WorkspaceEnv; + environment: ProjectEnv; }; export type TSnapshotDataProps = { @@ -22,7 +22,7 @@ export type TSnapshotDataProps = { }; export type TGetSecretSnapshotsDTO = { - workspaceId: string; + projectId: string; limit: number; environment: string; directory?: string; @@ -30,7 +30,7 @@ export type TGetSecretSnapshotsDTO = { export type TSecretRollbackDTO = { snapshotId: string; - workspaceId: string; + projectId: string; environment: string; directory?: string; }; diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx index 11ebad4b1..de960cd04 100644 --- a/frontend/src/hooks/api/secrets/mutations.tsx +++ b/frontend/src/hooks/api/secrets/mutations.tsx @@ -33,18 +33,18 @@ export const useCreateSecretV3 = ({ secretPath = "/", type, environment, - workspaceId, + projectId, secretKey, secretValue, secretComment, skipMultilineEncoding, tagIds }) => { - const { data } = await apiRequest.post(`/api/v3/secrets/raw/${secretKey}`, { + const { data } = await apiRequest.post(`/api/v4/secrets/${secretKey}`, { secretPath, type, environment, - workspaceId, + projectId, secretValue, secretComment, skipMultilineEncoding, @@ -52,26 +52,26 @@ export const useCreateSecretV3 = ({ }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options }); @@ -88,7 +88,7 @@ export const useUpdateSecretV3 = ({ secretPath = "/", type, environment, - workspaceId, + projectId, secretKey, secretValue, tagIds, @@ -100,8 +100,8 @@ export const useUpdateSecretV3 = ({ skipMultilineEncoding, secretMetadata }) => { - const { data } = await apiRequest.patch(`/api/v3/secrets/raw/${secretKey}`, { - workspaceId, + const { data } = await apiRequest.patch(`/api/v4/secrets/${secretKey}`, { + projectId, environment, type, secretReminderNote, @@ -117,26 +117,26 @@ export const useUpdateSecretV3 = ({ }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options }); @@ -150,17 +150,10 @@ export const useDeleteSecretV3 = ({ const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ - secretPath = "/", - type, - environment, - workspaceId, - secretKey, - secretId - }) => { - const { data } = await apiRequest.delete(`/api/v3/secrets/raw/${secretKey}`, { + mutationFn: async ({ secretPath = "/", type, environment, projectId, secretKey, secretId }) => { + const { data } = await apiRequest.delete(`/api/v4/secrets/${secretKey}`, { data: { - workspaceId, + projectId, environment, type, secretPath, @@ -169,26 +162,26 @@ export const useDeleteSecretV3 = ({ }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options }); @@ -202,35 +195,35 @@ export const useCreateSecretBatch = ({ const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets }) => { - const { data } = await apiRequest.post("/api/v3/secrets/batch/raw", { - workspaceId, + mutationFn: async ({ secretPath = "/", projectId, environment, secrets }) => { + const { data } = await apiRequest.post("/api/v4/secrets/batch", { + projectId, environment, secretPath, secrets }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options }); @@ -244,35 +237,35 @@ export const useUpdateSecretBatch = ({ const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets }) => { - const { data } = await apiRequest.patch("/api/v3/secrets/batch/raw", { - workspaceId, + mutationFn: async ({ secretPath = "/", projectId, environment, secrets }) => { + const { data } = await apiRequest.patch("/api/v4/secrets/batch", { + projectId, environment, secretPath, secrets }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options }); @@ -286,10 +279,10 @@ export const useDeleteSecretBatch = ({ const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets }) => { - const { data } = await apiRequest.delete("/api/v3/secrets/batch/raw", { + mutationFn: async ({ secretPath = "/", projectId, environment, secrets }) => { + const { data } = await apiRequest.delete("/api/v4/secrets/batch", { data: { - workspaceId, + projectId, environment, secretPath, secrets @@ -297,26 +290,26 @@ export const useDeleteSecretBatch = ({ }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options }); @@ -340,23 +333,23 @@ export const useMoveSecrets = ({ mutationFn: async ({ sourceEnvironment, sourceSecretPath, - projectSlug, destinationEnvironment, destinationSecretPath, secretIds, - shouldOverwrite + shouldOverwrite, + projectId }) => { const { data } = await apiRequest.post<{ isSourceUpdated: boolean; isDestinationUpdated: boolean; - }>("/api/v3/secrets/move", { + }>("/api/v4/secrets/move", { sourceEnvironment, sourceSecretPath, - projectSlug, destinationEnvironment, destinationSecretPath, secretIds, - shouldOverwrite + shouldOverwrite, + projectId }); return data; @@ -370,7 +363,7 @@ export const useMoveSecrets = ({ }); queryClient.invalidateQueries({ queryKey: secretKeys.getProjectSecret({ - workspaceId: projectId, + projectId, environment: sourceEnvironment, secretPath: sourceSecretPath }) @@ -378,33 +371,33 @@ export const useMoveSecrets = ({ queryClient.invalidateQueries({ queryKey: secretSnapshotKeys.list({ environment: sourceEnvironment, - workspaceId: projectId, + projectId, directory: sourceSecretPath }) }); queryClient.invalidateQueries({ queryKey: secretSnapshotKeys.count({ environment: sourceEnvironment, - workspaceId: projectId, + projectId, directory: sourceSecretPath }) }); queryClient.invalidateQueries({ queryKey: commitKeys.count({ - workspaceId: projectId, + projectId, environment: sourceEnvironment, directory: sourceSecretPath }) }); queryClient.invalidateQueries({ queryKey: commitKeys.history({ - workspaceId: projectId, + projectId, environment: sourceEnvironment, directory: sourceSecretPath }) }); queryClient.invalidateQueries({ - queryKey: secretApprovalRequestKeys.count({ workspaceId: projectId }) + queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options @@ -412,14 +405,14 @@ export const useMoveSecrets = ({ }; export const createSecret = async (dto: TCreateSecretsV3DTO) => { - const { data } = await apiRequest.post(`/api/v3/secrets/${dto.secretKey}`, dto); + const { data } = await apiRequest.post(`/api/v4/secrets/${dto.secretKey}`, dto); return data; }; export const useBackfillSecretReference = () => useMutation<{ message: string }, object, { projectId: string }>({ mutationFn: async ({ projectId }) => { - const { data } = await apiRequest.post("/api/v3/secrets/backfill-secret-references", { + const { data } = await apiRequest.post("/api/v4/secrets/backfill-secret-references", { projectId }); return data.message; @@ -432,16 +425,16 @@ export const useCreateCommit = () => { object, object, { - workspaceId: string; + projectId: string; environment: string; secretPath: string; pendingChanges: PendingChanges; message: string; } >({ - mutationFn: async ({ workspaceId, environment, secretPath, pendingChanges, message }) => { + mutationFn: async ({ projectId, environment, secretPath, pendingChanges, message }) => { const { data } = await apiRequest.post("/api/v1/pit/batch/commit", { - projectId: workspaceId, + projectId, environment, secretPath, changes: { @@ -501,26 +494,26 @@ export const useCreateCommit = () => { }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); } }); }; diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 443b74df8..df3a78c35 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -27,35 +27,35 @@ import { export const secretKeys = { // this is also used in secretSnapshot part getProjectSecret: ({ - workspaceId, + projectId, environment, secretPath, viewSecretValue }: TGetProjectSecretsKey) => - [{ workspaceId, environment, secretPath, viewSecretValue }, "secrets"] as const, + [{ projectId, environment, secretPath, viewSecretValue }, "secrets"] as const, getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const, getSecretAccessList: ({ - workspaceId, + projectId, environment, secretPath, secretKey }: TGetSecretAccessListDTO) => - ["secret-access-list", { workspaceId, environment, secretPath, secretKey }] as const, + ["secret-access-list", { projectId, environment, secretPath, secretKey }] as const, getSecretReferenceTree: (dto: TGetSecretReferenceTreeDTO) => ["secret-reference-tree", dto] }; export const fetchProjectSecrets = async ({ - workspaceId, + projectId, environment, secretPath, includeImports, expandSecretReferences, viewSecretValue }: TGetProjectSecretsKey) => { - const { data } = await apiRequest.get("/api/v3/secrets/raw", { + const { data } = await apiRequest.get("/api/v4/secrets", { params: { environment, - workspaceId, + projectId, secretPath, viewSecretValue, expandSecretReferences, @@ -116,7 +116,7 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => { }; export const useGetProjectSecrets = ({ - workspaceId, + projectId, environment, secretPath, viewSecretValue, @@ -135,14 +135,14 @@ export const useGetProjectSecrets = ({ useQuery({ ...options, // wait for all values to be available - enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true), + enabled: Boolean(projectId && environment) && (options?.enabled ?? true), queryKey: secretKeys.getProjectSecret({ - workspaceId, + projectId, environment, secretPath, viewSecretValue }), - queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath, viewSecretValue }), + queryFn: () => fetchProjectSecrets({ projectId, environment, secretPath, viewSecretValue }), select: useCallback( (data: Awaited>) => mergePersonalSecrets(data.secrets), [] @@ -150,7 +150,7 @@ export const useGetProjectSecrets = ({ }); export const useGetProjectSecretsAllEnv = ({ - workspaceId, + projectId, envs, secretPath }: TGetProjectSecretsAllEnvDTO) => { @@ -159,11 +159,11 @@ export const useGetProjectSecretsAllEnv = ({ const secrets = useQueries({ queries: envs.map((environment) => ({ queryKey: secretKeys.getProjectSecret({ - workspaceId, + projectId, environment, secretPath }), - enabled: Boolean(workspaceId && environment), + enabled: Boolean(projectId && environment), onError: (error: unknown) => { if (axios.isAxiosError(error) && !isErrorHandled) { const { message, requestId } = error.response?.data as { @@ -187,7 +187,7 @@ export const useGetProjectSecretsAllEnv = ({ setIsErrorHandled.on(); } }, - queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }), + queryFn: () => fetchProjectSecrets({ projectId, environment, secretPath }), staleTime: 60 * 1000, // eslint-disable-next-line react-hooks/rules-of-hooks select: useCallback( @@ -270,7 +270,7 @@ export const useGetSecretAccessList = (dto: TGetSecretAccessListDTO) => users: SecretAccessListEntry[]; }>(`/api/v1/secrets/${dto.secretKey}/access-list`, { params: { - workspaceId: dto.workspaceId, + projectId: dto.projectId, environment: dto.environment, secretPath: dto.secretPath } @@ -287,11 +287,11 @@ const fetchSecretReferenceTree = async ({ environmentSlug }: TGetSecretReferenceTreeDTO) => { const { data } = await apiRequest.get<{ tree: TSecretReferenceTraceNode; value: string }>( - `/api/v3/secrets/raw/${secretKey}/secret-reference-tree`, + `/api/v4/secrets/${secretKey}/secret-reference-tree`, { params: { secretPath, - workspaceId: projectId, + projectId, environment: environmentSlug } } diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 1b232cfac..327d3659d 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -17,30 +17,6 @@ export type SecretReminderRecipient = { }; id: string; }; -export type EncryptedSecret = { - id: string; - version: number; - workspace: string; - type: SecretType; - environment: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretValueHidden: boolean; - __v: number; - createdAt: string; - updatedAt: string; - skipMultilineEncoding?: boolean; - secretCommentCiphertext: string; - secretCommentIV: string; - secretCommentTag: string; - secretReminderRepeatDays?: number | null; - secretReminderNote?: string | null; - tags: WsTag[]; -}; // both personal and shared secret stitched together for dashboard export type SecretV3RawSanitized = { @@ -76,7 +52,7 @@ export type SecretV3RawSanitized = { export type SecretV3Raw = { id: string; _id: string; - workspace: string; + project: string; environment: string; version: number; type: string; @@ -113,7 +89,7 @@ export type SecretVersions = { id: string; secretId: string; version: number; - workspace: string; + project: string; type: SecretType; isDeleted: boolean; envId: string; @@ -136,7 +112,7 @@ export type SecretVersions = { // dto export type TGetProjectSecretsKey = { - workspaceId: string; + projectId: string; environment: string; secretPath?: string; includeImports?: boolean; @@ -148,7 +124,7 @@ export type TGetProjectSecretsKey = { export type TGetProjectSecretsDTO = TGetProjectSecretsKey; export type TGetProjectSecretsAllEnvDTO = { - workspaceId: string; + projectId: string; envs: string[]; folderId?: string; secretPath?: string; @@ -162,7 +138,7 @@ export type GetSecretVersionsDTO = { }; export type TGetSecretAccessListDTO = { - workspaceId: string; + projectId: string; environment: string; secretPath: string; secretKey: string; @@ -174,14 +150,14 @@ export type TCreateSecretsV3DTO = { secretComment: string; skipMultilineEncoding?: boolean; secretPath: string; - workspaceId: string; + projectId: string; environment: string; type: SecretType; tagIds?: string[]; }; export type TUpdateSecretsV3DTO = { - workspaceId: string; + projectId: string; environment: string; secretPath: string; type: SecretType; @@ -198,7 +174,7 @@ export type TUpdateSecretsV3DTO = { }; export type TDeleteSecretsV3DTO = { - workspaceId: string; + projectId: string; environment: string; type: SecretType; secretPath: string; @@ -207,7 +183,7 @@ export type TDeleteSecretsV3DTO = { }; export type TCreateSecretBatchDTO = { - workspaceId: string; + projectId: string; environment: string; secretPath: string; secrets: Array<{ @@ -224,7 +200,7 @@ export type TCreateSecretBatchDTO = { }; export type TUpdateSecretBatchDTO = { - workspaceId: string; + projectId: string; environment: string; secretPath: string; secrets: Array<{ @@ -241,7 +217,7 @@ export type TUpdateSecretBatchDTO = { }; export type TDeleteSecretBatchDTO = { - workspaceId: string; + projectId: string; environment: string; secretPath: string; secrets: Array<{ @@ -251,7 +227,6 @@ export type TDeleteSecretBatchDTO = { }; export type TMoveSecretsDTO = { - projectSlug: string; projectId: string; sourceEnvironment: string; sourceSecretPath: string; diff --git a/frontend/src/hooks/api/serviceTokens/queries.tsx b/frontend/src/hooks/api/serviceTokens/queries.tsx index 06b9b09c6..41421b279 100644 --- a/frontend/src/hooks/api/serviceTokens/queries.tsx +++ b/frontend/src/hooks/api/serviceTokens/queries.tsx @@ -13,9 +13,9 @@ const serviceTokenKeys = { getAllWorkspaceServiceToken: (workspaceID: string) => [{ workspaceID }, "service-tokens"] as const }; -const fetchWorkspaceServiceTokens = async (workspaceID: string) => { +const fetchWorkspaceServiceTokens = async (projectID: string) => { const { data } = await apiRequest.get<{ serviceTokenData: ServiceToken[] }>( - `/api/v1/workspace/${workspaceID}/service-token-data` + `/api/v1/projects/${projectID}/service-token-data` ); return data.serviceTokenData; diff --git a/frontend/src/hooks/api/sshCa/mutations.tsx b/frontend/src/hooks/api/sshCa/mutations.tsx index c7e90c3b9..fc6c43ef4 100644 --- a/frontend/src/hooks/api/sshCa/mutations.tsx +++ b/frontend/src/hooks/api/sshCa/mutations.tsx @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { workspaceKeys } from "../workspace/query-keys"; +import { projectKeys } from "../projects/query-keys"; import { TCreateSshCaDTO, TDeleteSshCaDTO, @@ -28,7 +28,7 @@ export const useCreateSshCa = () => { return ca; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshCas(projectId) }); + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectSshCas(projectId) }); } }); }; @@ -43,7 +43,7 @@ export const useUpdateSshCa = () => { return ca; }, onSuccess: ({ projectId }, { caId }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshCas(projectId) }); + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectSshCas(projectId) }); queryClient.invalidateQueries({ queryKey: sshCaKeys.getSshCaById(caId) }); } }); @@ -59,7 +59,7 @@ export const useDeleteSshCa = () => { return ca; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshCas(projectId) }); + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectSshCas(projectId) }); } }); }; @@ -76,7 +76,7 @@ export const useSignSshKey = () => { }, onSuccess: (_, { projectId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.allWorkspaceSshCertificates(projectId) + queryKey: projectKeys.allProjectSshCertificates(projectId) }); } }); @@ -94,7 +94,7 @@ export const useIssueSshCreds = () => { }, onSuccess: (_, { projectId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.allWorkspaceSshCertificates(projectId) + queryKey: projectKeys.allProjectSshCertificates(projectId) }); } }); diff --git a/frontend/src/hooks/api/sshHost/mutations.tsx b/frontend/src/hooks/api/sshHost/mutations.tsx index f6b831f3e..0c1b67103 100644 --- a/frontend/src/hooks/api/sshHost/mutations.tsx +++ b/frontend/src/hooks/api/sshHost/mutations.tsx @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { workspaceKeys } from "../workspace/query-keys"; +import { projectKeys } from "../projects/query-keys"; import { TCreateSshHostDTO, TDeleteSshHostDTO, TSshHost, TUpdateSshHostDTO } from "./types"; export const useCreateSshHost = () => { @@ -13,7 +13,7 @@ export const useCreateSshHost = () => { return host; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshHosts(projectId) }); + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectSshHosts(projectId) }); } }); }; @@ -26,7 +26,7 @@ export const useUpdateSshHost = () => { return host; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshHosts(projectId) }); + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectSshHosts(projectId) }); } }); }; @@ -39,7 +39,7 @@ export const useDeleteSshHost = () => { return host; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshHosts(projectId) }); + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectSshHosts(projectId) }); } }); }; diff --git a/frontend/src/hooks/api/sshHostGroup/mutations.tsx b/frontend/src/hooks/api/sshHostGroup/mutations.tsx index b75cff187..fa9eebb07 100644 --- a/frontend/src/hooks/api/sshHostGroup/mutations.tsx +++ b/frontend/src/hooks/api/sshHostGroup/mutations.tsx @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { workspaceKeys } from "../workspace/query-keys"; +import { projectKeys } from "../projects/query-keys"; import { sshHostGroupKeys } from "./queries"; import { TCreateSshHostGroupDTO, @@ -20,7 +20,7 @@ export const useCreateSshHostGroup = () => { }, onSuccess: ({ projectId, id }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId) + queryKey: projectKeys.getProjectSshHostGroups(projectId) }); queryClient.invalidateQueries({ queryKey: sshHostGroupKeys.getSshHostGroupById(id) @@ -41,10 +41,10 @@ export const useUpdateSshHostGroup = () => { }, onSuccess: ({ projectId }, { sshHostGroupId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId) + queryKey: projectKeys.getProjectSshHostGroups(projectId) }); queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceSshHosts(projectId) + queryKey: projectKeys.getProjectSshHosts(projectId) }); queryClient.invalidateQueries({ queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId) @@ -64,10 +64,10 @@ export const useDeleteSshHostGroup = () => { }, onSuccess: ({ projectId }, { sshHostGroupId }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId) + queryKey: projectKeys.getProjectSshHostGroups(projectId) }); queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceSshHosts(projectId) + queryKey: projectKeys.getProjectSshHosts(projectId) }); queryClient.invalidateQueries({ queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId) diff --git a/frontend/src/hooks/api/subscriptions/queries.tsx b/frontend/src/hooks/api/subscriptions/queries.tsx index 99b1f4486..f545565eb 100644 --- a/frontend/src/hooks/api/subscriptions/queries.tsx +++ b/frontend/src/hooks/api/subscriptions/queries.tsx @@ -10,9 +10,9 @@ export const subscriptionQueryKeys = { getOrgSubsription: (orgID: string) => ["plan", { orgID }] as const }; -export const fetchOrgSubscription = async (orgID: string) => { +export const fetchOrgSubscription = async (orgID: string, refreshCache: boolean = false) => { const { data } = await apiRequest.get<{ plan: SubscriptionPlan }>( - `/api/v1/organizations/${orgID}/plan` + `/api/v1/organizations/${orgID}/plan${refreshCache ? "?refreshCache=true" : ""}` ); return data.plan; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 338599fe0..0c3733cc8 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -54,5 +54,7 @@ export type SubscriptionPlan = { secretScanning: boolean; enterpriseSecretSyncs: boolean; enterpriseAppConnections: boolean; + cardDeclined?: boolean; + cardDeclinedReason?: string; machineIdentityAuthTemplates: boolean; }; diff --git a/frontend/src/hooks/api/tags/queries.tsx b/frontend/src/hooks/api/tags/queries.tsx index bdbb9659d..0234cec4d 100644 --- a/frontend/src/hooks/api/tags/queries.tsx +++ b/frontend/src/hooks/api/tags/queries.tsx @@ -4,23 +4,21 @@ import { apiRequest } from "@app/config/request"; import { CreateTagDTO, DeleteTagDTO, UserWsTags, WsTag } from "./types"; -const workspaceTags = { - getWsTags: (workspaceID: string) => ["workspace-tags", { workspaceID }] as const +const projectTags = { + getWsTags: (projectID: string) => ["project-tags", { projectID }] as const }; -const fetchWsTag = async (workspaceID: string) => { - const { data } = await apiRequest.get<{ workspaceTags: UserWsTags }>( - `/api/v1/workspace/${workspaceID}/tags` - ); +const fetchWsTag = async (projectID: string) => { + const { data } = await apiRequest.get<{ tags: UserWsTags }>(`/api/v1/projects/${projectID}/tags`); - return data.workspaceTags; + return data.tags; }; -export const useGetWsTags = (workspaceID: string) => { +export const useGetWsTags = (projectID: string) => { return useQuery({ - queryKey: workspaceTags.getWsTags(workspaceID), - queryFn: () => fetchWsTag(workspaceID), - enabled: Boolean(workspaceID) + queryKey: projectTags.getWsTags(projectID), + queryFn: () => fetchWsTag(projectID), + enabled: Boolean(projectID) }); }; @@ -28,18 +26,15 @@ export const useCreateWsTag = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ workspaceID, tagColor, tagSlug }) => { - const { data } = await apiRequest.post<{ workspaceTag: WsTag }>( - `/api/v1/workspace/${workspaceID}/tags`, - { - color: tagColor || "", - slug: tagSlug - } - ); - return data.workspaceTag; + mutationFn: async ({ projectId: projectID, tagColor, tagSlug }) => { + const { data } = await apiRequest.post<{ tag: WsTag }>(`/api/v1/projects/${projectID}/tags`, { + color: tagColor || "", + slug: tagSlug + }); + return data.tag; }, onSuccess: (tagData) => { - queryClient.invalidateQueries({ queryKey: workspaceTags.getWsTags(tagData?.projectId) }); + queryClient.invalidateQueries({ queryKey: projectTags.getWsTags(tagData?.projectId) }); } }); }; @@ -49,13 +44,13 @@ export const useDeleteWsTag = () => { return useMutation({ mutationFn: async ({ tagID, projectId }) => { - const { data } = await apiRequest.delete<{ workspaceTag: WsTag }>( - `/api/v1/workspace/${projectId}/tags/${tagID}` + const { data } = await apiRequest.delete<{ tag: WsTag }>( + `/api/v1/projects/${projectId}/tags/${tagID}` ); - return data.workspaceTag; + return data.tag; }, onSuccess: (tagData) => { - queryClient.invalidateQueries({ queryKey: workspaceTags.getWsTags(tagData?.projectId) }); + queryClient.invalidateQueries({ queryKey: projectTags.getWsTags(tagData?.projectId) }); } }); }; diff --git a/frontend/src/hooks/api/tags/types.ts b/frontend/src/hooks/api/tags/types.ts index 72d710cfa..09ac22147 100644 --- a/frontend/src/hooks/api/tags/types.ts +++ b/frontend/src/hooks/api/tags/types.ts @@ -13,7 +13,7 @@ export type WsTag = { export type WorkspaceTag = { id: string; name: string; slug: string }; export type CreateTagDTO = { - workspaceID: string; + projectId: string; tagSlug: string; tagColor: string; }; diff --git a/frontend/src/hooks/api/trustedIps/queries.ts b/frontend/src/hooks/api/trustedIps/queries.ts index 57a5d5844..36815b313 100644 --- a/frontend/src/hooks/api/trustedIps/queries.ts +++ b/frontend/src/hooks/api/trustedIps/queries.ts @@ -5,15 +5,15 @@ import { apiRequest } from "@app/config/request"; import { TrustedIp } from "./types"; const trustedIps = { - getTrustedIps: (workspaceId: string) => [{ workspaceId }, "trusted-ips"] as const + getTrustedIps: (projectId: string) => [{ projectId }, "trusted-ips"] as const }; -export const useGetTrustedIps = (workspaceId: string) => { +export const useGetTrustedIps = (projectId: string) => { return useQuery({ - queryKey: trustedIps.getTrustedIps(workspaceId), + queryKey: trustedIps.getTrustedIps(projectId), queryFn: async () => { const { data } = await apiRequest.get<{ trustedIps: TrustedIp[] }>( - `/api/v1/workspace/${workspaceId}/trusted-ips` + `/api/v1/projects/${projectId}/trusted-ips` ); return data.trustedIps; @@ -25,17 +25,17 @@ export const useAddTrustedIp = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ - workspaceId, + projectId, ipAddress, comment, isActive }: { - workspaceId: string; + projectId: string; ipAddress: string; comment?: string; isActive: boolean; }) => { - const { data } = await apiRequest.post(`/api/v1/workspace/${workspaceId}/trusted-ips`, { + const { data } = await apiRequest.post(`/api/v1/projects/${projectId}/trusted-ips`, { ipAddress, ...(comment ? { comment } : {}), isActive @@ -44,7 +44,7 @@ export const useAddTrustedIp = () => { return data; }, onSuccess(_, dto) { - queryClient.invalidateQueries({ queryKey: trustedIps.getTrustedIps(dto.workspaceId) }); + queryClient.invalidateQueries({ queryKey: trustedIps.getTrustedIps(dto.projectId) }); } }); }; @@ -53,20 +53,20 @@ export const useUpdateTrustedIp = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ - workspaceId, + projectId, trustedIpId, ipAddress, comment, isActive }: { - workspaceId: string; + projectId: string; trustedIpId: string; ipAddress: string; comment?: string; isActive: boolean; }) => { const { data } = await apiRequest.patch( - `/api/v1/workspace/${workspaceId}/trusted-ips/${trustedIpId}`, + `/api/v1/projects/${projectId}/trusted-ips/${trustedIpId}`, { ipAddress, ...(comment ? { comment } : {}), @@ -77,7 +77,7 @@ export const useUpdateTrustedIp = () => { return data; }, onSuccess(_, dto) { - queryClient.invalidateQueries({ queryKey: trustedIps.getTrustedIps(dto.workspaceId) }); + queryClient.invalidateQueries({ queryKey: trustedIps.getTrustedIps(dto.projectId) }); } }); }; @@ -85,21 +85,15 @@ export const useUpdateTrustedIp = () => { export const useDeleteTrustedIp = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ - workspaceId, - trustedIpId - }: { - workspaceId: string; - trustedIpId: string; - }) => { + mutationFn: async ({ projectId, trustedIpId }: { projectId: string; trustedIpId: string }) => { const { data } = await apiRequest.delete( - `/api/v1/workspace/${workspaceId}/trusted-ips/${trustedIpId}` + `/api/v1/projects/${projectId}/trusted-ips/${trustedIpId}` ); return data; }, onSuccess(_, dto) { - queryClient.invalidateQueries({ queryKey: trustedIps.getTrustedIps(dto.workspaceId) }); + queryClient.invalidateQueries({ queryKey: trustedIps.getTrustedIps(dto.projectId) }); } }); }; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 8c29d4825..4b73eba28 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -7,8 +7,19 @@ export type { GetAuthTokenAPI } from "./auth/types"; export type { IncidentContact } from "./incidentContacts/types"; export type { IntegrationAuth } from "./integrationAuth/types"; export type { TCloudIntegration, TIntegration } from "./integrations/types"; -export type { UserWsKeyPair } from "./keys/types"; export type { Organization } from "./organization/types"; +export type { + CreateEnvironmentDTO, + CreateWorkspaceDTO, + DeleteEnvironmentDTO, + DeleteWorkspaceDTO, + Project, + ProjectEnv, + ProjectTag, + ToggleAutoCapitalizationDTO, + UpdateEnvironmentDTO, + UpdateProjectDTO +} from "./projects/types"; export type { TSecretApprovalPolicy } from "./secretApproval/types"; export type { TGetSecretApprovalRequestDetails, @@ -29,18 +40,6 @@ export type { SubscriptionPlan } from "./subscriptions/types"; export type { WsTag } from "./tags/types"; export type { OrgUser, TWorkspaceUser, User, UserEnc } from "./users/types"; export type { TWebhook } from "./webhooks/types"; -export type { - CreateEnvironmentDTO, - CreateWorkspaceDTO, - DeleteEnvironmentDTO, - DeleteWorkspaceDTO, - ToggleAutoCapitalizationDTO, - UpdateEnvironmentDTO, - UpdateProjectDTO, - Workspace, - WorkspaceEnv, - WorkspaceTag -} from "./workspace/types"; export enum ApiErrorTypes { ValidationError = "ValidationFailure", diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index d20a15bc8..36bccdb8a 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -1,8 +1,10 @@ export { useAddUserToWsNonE2EE, useRemoveMyDuplicateAccounts, + useRequestEmailChangeOTP, useRevokeMySessionById, useSendEmailVerificationCode, + useUpdateUserEmail, useVerifyEmailVerificationCode } from "./mutation"; export { diff --git a/frontend/src/hooks/api/users/mutation.tsx b/frontend/src/hooks/api/users/mutation.tsx index b108a98c4..7acfb8fe0 100644 --- a/frontend/src/hooks/api/users/mutation.tsx +++ b/frontend/src/hooks/api/users/mutation.tsx @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { workspaceKeys } from "../workspace"; +import { projectKeys } from "../projects"; import { userKeys } from "./query-keys"; import { AddUserToWsDTONonE2EE } from "./types"; @@ -11,14 +11,14 @@ export const useAddUserToWsNonE2EE = () => { return useMutation({ mutationFn: async ({ projectId, usernames, roleSlugs }) => { - const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, { + const { data } = await apiRequest.post(`/api/v1/projects/${projectId}/memberships`, { usernames, roleSlugs }); return data; }, onSuccess: (_, { orgId, projectId }) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceUsers(projectId) }); + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectUsers(projectId) }); queryClient.invalidateQueries({ queryKey: userKeys.allOrgMembershipProjectMemberships(orgId) }); @@ -152,3 +152,30 @@ export const useRemoveMyDuplicateAccounts = () => { } }); }; + +export const useRequestEmailChangeOTP = () => { + return useMutation({ + mutationFn: async ({ newEmail }: { newEmail: string }) => { + const { data } = await apiRequest.post("/api/v2/users/me/email-change/otp", { + newEmail + }); + return data; + } + }); +}; + +export const useUpdateUserEmail = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ newEmail, otpCode }: { newEmail: string; otpCode: string }) => { + const { data } = await apiRequest.patch("/api/v2/users/me/email", { + newEmail, + otpCode + }); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: userKeys.getUser }); + } + }); +}; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 77289fee6..2c0125361 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -8,9 +8,9 @@ import { queryClient as qc } from "@app/hooks/api/reactQuery"; import { APIKeyDataV2 } from "../apiKeys/types"; import { MfaMethod } from "../auth/types"; import { TGroupWithProjectMemberships } from "../groups/types"; +import { projectKeys } from "../projects"; import { setAuthToken } from "../reactQuery"; import { subscriptionQueryKeys } from "../subscriptions/queries"; -import { workspaceKeys } from "../workspace"; import { userKeys } from "./query-keys"; import { AddUserToOrgDTO, @@ -197,10 +197,10 @@ export const useAddUsersToOrg = () => { projects?.forEach((project) => { if (project.slug) { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceGroupMemberships(project.slug) + queryKey: projectKeys.getProjectGroupMemberships(project.slug) }); } - queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceUsers(project.id) }); + queryClient.invalidateQueries({ queryKey: projectKeys.getProjectUsers(project.id) }); }); } }); diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 0a43e850e..a59965dfc 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -1,5 +1,5 @@ import { MfaMethod } from "../auth/types"; -import { ProjectType, ProjectUserMembershipTemporaryMode } from "../workspace/types"; +import { ProjectType, ProjectUserMembershipTemporaryMode } from "../projects/types"; export enum AuthMethod { EMAIL = "email", @@ -19,10 +19,10 @@ export type User = { createdAt: Date; updatedAt: Date; username: string; - email?: string; + email?: string | null; superAdmin: boolean; - firstName?: string; - lastName?: string; + firstName?: string | null; + lastName?: string | null; authProvider?: AuthMethod; authMethods: AuthMethod[]; isMfaEnabled: boolean; diff --git a/frontend/src/hooks/api/webhooks/mutation.tsx b/frontend/src/hooks/api/webhooks/mutation.tsx index 10d5e472e..66eb8ae17 100644 --- a/frontend/src/hooks/api/webhooks/mutation.tsx +++ b/frontend/src/hooks/api/webhooks/mutation.tsx @@ -13,8 +13,8 @@ export const useCreateWebhook = () => { const { data } = await apiRequest.post("/api/v1/webhooks", dto); return data; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries({ queryKey: queryKeys.getWebhooks(workspaceId) }); + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ queryKey: queryKeys.getWebhooks(projectId) }); } }); }; @@ -27,11 +27,11 @@ export const useTestWebhook = () => { const { data } = await apiRequest.post(`/api/v1/webhooks/${webhookId}/test`); return data; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries({ queryKey: queryKeys.getWebhooks(workspaceId) }); + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ queryKey: queryKeys.getWebhooks(projectId) }); }, - onError: (_, { workspaceId }) => { - queryClient.invalidateQueries({ queryKey: queryKeys.getWebhooks(workspaceId) }); + onError: (_, { projectId }) => { + queryClient.invalidateQueries({ queryKey: queryKeys.getWebhooks(projectId) }); } }); }; @@ -46,8 +46,8 @@ export const useUpdateWebhook = () => { }); return data; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries({ queryKey: queryKeys.getWebhooks(workspaceId) }); + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ queryKey: queryKeys.getWebhooks(projectId) }); } }); }; @@ -60,8 +60,8 @@ export const useDeleteWebhook = () => { const { data } = await apiRequest.delete(`/api/v1/webhooks/${dto.webhookId}`); return data; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries({ queryKey: queryKeys.getWebhooks(workspaceId) }); + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ queryKey: queryKeys.getWebhooks(projectId) }); } }); }; diff --git a/frontend/src/hooks/api/webhooks/query.tsx b/frontend/src/hooks/api/webhooks/query.tsx index fc1840409..3f92a9695 100644 --- a/frontend/src/hooks/api/webhooks/query.tsx +++ b/frontend/src/hooks/api/webhooks/query.tsx @@ -8,19 +8,19 @@ export const queryKeys = { getWebhooks: (workspaceId: string) => ["webhooks", { workspaceId }] }; -const fetchWebhooks = async (workspaceId: string) => { +const fetchWebhooks = async (projectId: string) => { const { data } = await apiRequest.get<{ webhooks: TWebhook[] }>("/api/v1/webhooks", { params: { - workspaceId + projectId } }); return data.webhooks; }; -export const useGetWebhooks = (workspaceId: string) => +export const useGetWebhooks = (projectId: string) => useQuery({ - queryKey: queryKeys.getWebhooks(workspaceId), - queryFn: () => fetchWebhooks(workspaceId), - enabled: Boolean(workspaceId) + queryKey: queryKeys.getWebhooks(projectId), + queryFn: () => fetchWebhooks(projectId), + enabled: Boolean(projectId) }); diff --git a/frontend/src/hooks/api/webhooks/types.ts b/frontend/src/hooks/api/webhooks/types.ts index 86183bf1b..7e2758ff8 100644 --- a/frontend/src/hooks/api/webhooks/types.ts +++ b/frontend/src/hooks/api/webhooks/types.ts @@ -23,7 +23,7 @@ export type TWebhook = { }; export type TCreateWebhookDto = { - workspaceId: string; + projectId: string; environment: string; webhookUrl: string; webhookSecretKey?: string; @@ -33,16 +33,16 @@ export type TCreateWebhookDto = { export type TUpdateWebhookDto = { webhookId: string; - workspaceId: string; + projectId: string; isDisabled?: boolean; }; export type TDeleteWebhookDto = { webhookId: string; - workspaceId: string; + projectId: string; }; export type TTestWebhookDTO = { webhookId: string; - workspaceId: string; + projectId: string; }; diff --git a/frontend/src/hooks/api/workflowIntegrations/mutation.tsx b/frontend/src/hooks/api/workflowIntegrations/mutation.tsx index 89c65f888..d362c7b82 100644 --- a/frontend/src/hooks/api/workflowIntegrations/mutation.tsx +++ b/frontend/src/hooks/api/workflowIntegrations/mutation.tsx @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { workspaceKeys } from "../workspace/query-keys"; +import { projectKeys } from "../projects/query-keys"; import { workflowIntegrationKeys } from "./queries"; import { TCheckMicrosoftTeamsIntegrationInstallationStatusDTO, @@ -118,15 +118,15 @@ export const useUpdateProjectWorkflowIntegrationConfig = () => { return useMutation({ mutationFn: async (dto: TUpdateProjectWorkflowIntegrationConfigDTO) => { const { data } = await apiRequest.put( - `/api/v1/workspace/${dto.workspaceId}/workflow-integration`, + `/api/v1/projects/${dto.projectId}/workflow-integration`, dto ); return data; }, - onSuccess: (_, { workspaceId, integration }) => { + onSuccess: (_, { projectId: workspaceId, integration }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(workspaceId, integration) + queryKey: projectKeys.getProjectWorkflowIntegrationConfig(workspaceId, integration) }); } }); @@ -138,14 +138,14 @@ export const useDeleteProjectWorkflowIntegration = () => { return useMutation({ mutationFn: async (dto: TDeleteProjectWorkflowIntegrationDTO) => { const { data } = await apiRequest.delete( - `/api/v1/workspace/${dto.projectId}/workflow-integration/${dto.integration}/${dto.integrationId}` + `/api/v1/projects/${dto.projectId}/workflow-integration/${dto.integration}/${dto.integrationId}` ); return data; }, onSuccess: (_, { projectId, integration }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(projectId, integration) + queryKey: projectKeys.getProjectWorkflowIntegrationConfig(projectId, integration) }); } }); diff --git a/frontend/src/hooks/api/workflowIntegrations/types.ts b/frontend/src/hooks/api/workflowIntegrations/types.ts index 4d2baf4bf..668850124 100644 --- a/frontend/src/hooks/api/workflowIntegrations/types.ts +++ b/frontend/src/hooks/api/workflowIntegrations/types.ts @@ -106,7 +106,7 @@ export type ProjectWorkflowIntegrationConfig = export type TUpdateProjectWorkflowIntegrationConfigDTO = | { integration: WorkflowIntegrationPlatform.SLACK; - workspaceId: string; + projectId: string; integrationId: string; isAccessRequestNotificationEnabled: boolean; accessRequestChannels: string; @@ -115,7 +115,7 @@ export type TUpdateProjectWorkflowIntegrationConfigDTO = } | { integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS; - workspaceId: string; + projectId: string; integrationId: string; isAccessRequestNotificationEnabled: boolean; isSecretRequestNotificationEnabled: boolean; diff --git a/frontend/src/hooks/api/workspace/query-keys.tsx b/frontend/src/hooks/api/workspace/query-keys.tsx deleted file mode 100644 index aca6f48b7..000000000 --- a/frontend/src/hooks/api/workspace/query-keys.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "@app/hooks/api/workspace/types"; - -import type { CaStatus } from "../ca"; -import { WorkflowIntegrationPlatform } from "../workflowIntegrations/types"; - -export const workspaceKeys = { - getWorkspaceById: (workspaceId: string) => ["workspaces", { workspaceId }] as const, - getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const, - getWorkspaceIndexStatus: (workspaceId: string) => - [{ workspaceId }, "workspace-index-status"] as const, - getProjectUpgradeStatus: (workspaceId: string) => [{ workspaceId }, "workspace-upgrade-status"], - getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"], - getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"], - getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"], - getAllUserWorkspace: () => ["workspaces"] as const, - getWorkspaceAuditLogs: (workspaceId: string) => - [{ workspaceId }, "workspace-audit-logs"] as const, - getWorkspaceUsers: ( - workspaceId: string, - includeGroupMembers: boolean = false, - roles: string[] = [] - ) => [{ workspaceId, includeGroupMembers, roles }, "workspace-users"] as const, - getWorkspaceUserDetails: (workspaceId: string, membershipId: string) => - [{ workspaceId, membershipId }, "workspace-user-details"] as const, - getWorkspaceIdentityMemberships: (workspaceId: string) => - [{ workspaceId }, "workspace-identity-memberships"] as const, - getWorkspaceIdentityMembershipDetails: (workspaceId: string, identityId: string) => - [{ workspaceId, identityId }, "workspace-identity-membership-details"] as const, - // allows invalidation using above key without knowing params - getWorkspaceIdentityMembershipsWithParams: ({ - workspaceId, - ...params - }: TListProjectIdentitiesDTO) => - [...workspaceKeys.getWorkspaceIdentityMemberships(workspaceId), params] as const, - searchWorkspace: (dto: TSearchProjectsDTO) => ["search-projects", dto] as const, - getWorkspaceGroupMemberships: (workspaceId: string) => - [{ workspaceId }, "workspace-groups"] as const, - getWorkspaceGroupMembershipDetails: (workspaceId: string, groupId: string) => - [{ workspaceId, groupId }, "workspace-group-membership-details"] as const, - getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) => - [{ projectSlug }, "workspace-cas"] as const, - specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) => - [...workspaceKeys.getWorkspaceCas({ projectSlug }), { status }] as const, - allWorkspaceCertificates: () => ["workspace-certificates"] as const, - forWorkspaceCertificates: (slug: string) => - [...workspaceKeys.allWorkspaceCertificates(), slug] as const, - specificWorkspaceCertificates: ({ - slug, - offset, - limit - }: { - slug: string; - offset: number; - limit: number; - }) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const, - getWorkspacePkiAlerts: (workspaceId: string) => - [{ workspaceId }, "workspace-pki-alerts"] as const, - getWorkspacePkiSubscribers: (projectId: string) => - [{ projectId }, "workspace-pki-subscribers"] as const, - getWorkspacePkiCollections: (workspaceId: string) => - [{ workspaceId }, "workspace-pki-collections"] as const, - getWorkspaceCertificateTemplates: (workspaceId: string) => - [{ workspaceId }, "workspace-certificate-templates"] as const, - getWorkspaceWorkflowIntegrationConfig: ( - workspaceId: string, - integration: WorkflowIntegrationPlatform - ) => [{ workspaceId, integration }, "workspace-workflow-integration-config"] as const, - getWorkspaceSshCas: (projectId: string) => [{ projectId }, "workspace-ssh-cas"] as const, - allWorkspaceSshCertificates: (projectId: string) => - [{ projectId }, "workspace-ssh-certificates"] as const, - getWorkspaceSshHosts: (projectId: string) => [{ projectId }, "workspace-ssh-hosts"] as const, - getWorkspaceSshHostGroups: (projectId: string) => - [{ projectId }, "workspace-ssh-host-groups"] as const, - specificWorkspaceSshCertificates: ({ - offset, - limit, - projectId - }: { - offset: number; - limit: number; - projectId: string; - }) => [...workspaceKeys.allWorkspaceSshCertificates(projectId), { offset, limit }] as const, - getWorkspaceSshCertificateTemplates: (projectId: string) => - [{ projectId }, "workspace-ssh-certificate-templates"] as const, - getProjectSshConfig: (projectId: string) => [{ projectId }, "project-ssh-config"] as const -}; diff --git a/frontend/src/hooks/useGetProjectTypeFromRoute.tsx b/frontend/src/hooks/useGetProjectTypeFromRoute.tsx index 3156216db..972671a96 100644 --- a/frontend/src/hooks/useGetProjectTypeFromRoute.tsx +++ b/frontend/src/hooks/useGetProjectTypeFromRoute.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { useRouterState } from "@tanstack/react-router"; -import { ProjectType } from "@app/hooks/api/workspace/types"; +import { ProjectType } from "@app/hooks/api/projects/types"; export const useGetProjectTypeFromRoute = () => { const { location } = useRouterState(); diff --git a/frontend/src/hooks/usePathAccessPolicies.tsx b/frontend/src/hooks/usePathAccessPolicies.tsx index 1fbc5fd52..0ad25b0d2 100644 --- a/frontend/src/hooks/usePathAccessPolicies.tsx +++ b/frontend/src/hooks/usePathAccessPolicies.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; -import { useSubscription, useWorkspace } from "@app/context"; +import { useProject, useSubscription } from "@app/context"; import { useGetAccessApprovalPolicies } from "@app/hooks/api"; const matchesPath = (folderPath: string, pattern: string) => { @@ -37,10 +37,10 @@ type Params = { }; export const usePathAccessPolicies = ({ secretPath, environment }: Params) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { subscription } = useSubscription(); const { data: policies } = useGetAccessApprovalPolicies({ - projectSlug: currentWorkspace.slug, + projectSlug: currentProject.slug, options: { enabled: subscription.secretApproval } diff --git a/frontend/src/layouts/AdminLayout/AdminLayout.tsx b/frontend/src/layouts/AdminLayout/AdminLayout.tsx index 097654f26..21be0bb82 100644 --- a/frontend/src/layouts/AdminLayout/AdminLayout.tsx +++ b/frontend/src/layouts/AdminLayout/AdminLayout.tsx @@ -1,11 +1,15 @@ import { useTranslation } from "react-i18next"; import { faMobile } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Outlet, useRouterState } from "@tanstack/react-router"; +import { Outlet } from "@tanstack/react-router"; import { Banner } from "@app/components/page-frames/Banner"; -import { BreadcrumbContainer, TBreadcrumbFormat } from "@app/components/v2"; -import { useServerConfig } from "@app/context"; +import { useServerConfig, useSubscription } from "@app/context"; +import { useFetchServerStatus } from "@app/hooks/api"; +import { AuditLogBanner } from "@app/layouts/OrganizationLayout/components/AuditLogBanner"; +import { Navbar } from "@app/layouts/OrganizationLayout/components/NavBar"; +import { RedisBanner } from "@app/layouts/OrganizationLayout/components/RedisBanner"; +import { SmtpBanner } from "@app/layouts/OrganizationLayout/components/SmtpBanner"; import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner"; import { AdminSidebar } from "./Sidebar"; @@ -13,26 +17,27 @@ import { AdminSidebar } from "./Sidebar"; export const AdminLayout = () => { const { t } = useTranslation(); const { config } = useServerConfig(); - - const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context }); - - const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined; + const { data: serverDetails, isLoading } = useFetchServerStatus(); + const { subscription } = useSubscription(); const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen"; return ( <> -
diff --git a/frontend/src/layouts/AdminLayout/Sidebar.tsx b/frontend/src/layouts/AdminLayout/Sidebar.tsx index e1cd223f8..25b327102 100644 --- a/frontend/src/layouts/AdminLayout/Sidebar.tsx +++ b/frontend/src/layouts/AdminLayout/Sidebar.tsx @@ -1,57 +1,62 @@ -import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; +import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; +import { + faBuilding, + faChevronLeft, + faCog, + faDatabase, + faKey, + faLock, + faPlug, + faUserTie +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, useMatchRoute } from "@tanstack/react-router"; -import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; +import { Menu, MenuGroup, MenuItem } from "@app/components/v2"; const generalTabs = [ { label: "General", - icon: "settings-cog", + icon: faCog, link: "/admin/" }, { label: "Encryption", - icon: "lock-closed", + icon: faLock, link: "/admin/encryption" }, { label: "Authentication", - icon: "check", + icon: faCheckCircle, link: "/admin/authentication" }, { label: "Integrations", - icon: "sliding-carousel", + icon: faPlug, link: "/admin/integrations" }, { label: "Caching", - icon: "note", + icon: faDatabase, link: "/admin/caching" }, { label: "Environment Variables", - icon: "unlock", + icon: faKey, link: "/admin/environment" } ]; -const resourceTabs = [ +const othersTabs = [ { - label: "Organizations", - icon: "groups", - link: "/admin/resources/organizations" + label: "Access Controls", + icon: faUserTie, + link: "/admin/access-management" }, { - label: "User Identities", - icon: "user", - link: "/admin/resources/user-identities" - }, - { - label: "Machine Identities", - icon: "wrench", - link: "/admin/resources/machine-identities" + label: "Resource Overview", + icon: faBuilding, + link: "/admin/resources/overview" } ]; @@ -61,6 +66,44 @@ export const AdminSidebar = () => { return ( ); diff --git a/frontend/src/layouts/KmsLayout/KmsLayout.tsx b/frontend/src/layouts/KmsLayout/KmsLayout.tsx index c34bc3626..285f3b7f8 100644 --- a/frontend/src/layouts/KmsLayout/KmsLayout.tsx +++ b/frontend/src/layouts/KmsLayout/KmsLayout.tsx @@ -4,12 +4,12 @@ import { Link, Outlet } from "@tanstack/react-router"; import { motion } from "framer-motion"; import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; -import { useProjectPermission, useWorkspace } from "@app/context"; +import { useProject, useProjectPermission } from "@app/context"; import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; export const KmsLayout = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { assumedPrivilegeDetails } = useProjectPermission(); return ( @@ -34,7 +34,7 @@ export const KmsLayout = () => { {({ isActive }) => ( @@ -51,7 +51,7 @@ export const KmsLayout = () => { {({ isActive }) => ( @@ -70,7 +70,7 @@ export const KmsLayout = () => { {({ isActive }) => ( @@ -87,7 +87,7 @@ export const KmsLayout = () => { {({ isActive }) => ( @@ -104,7 +104,7 @@ export const KmsLayout = () => { {({ isActive }) => ( diff --git a/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/AuditLogBanner.tsx b/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/AuditLogBanner.tsx index 9f7494076..755611587 100644 --- a/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/AuditLogBanner.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/AuditLogBanner.tsx @@ -1,12 +1,10 @@ -import { useOrganization } from "@app/context"; -import { useFetchServerStatus, useGetAuditLogStreams } from "@app/hooks/api"; +import { useFetchServerStatus, useListAuditLogStreams } from "@app/hooks/api"; import { OrgAlertBanner } from "../OrgAlertBanner"; export const AuditLogBanner = () => { - const org = useOrganization(); const { data: status, isLoading: isLoadingStatus } = useFetchServerStatus(); - const { data: streams, isLoading: isLoadingStreams } = useGetAuditLogStreams(org.currentOrg.id); + const { data: streams, isLoading: isLoadingStreams } = useListAuditLogStreams(); if (isLoadingStreams || isLoadingStatus || !streams) return null; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 5c37d4404..71be38b11 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; import { faCircleQuestion, faUserCircle } from "@fortawesome/free-regular-svg-icons"; import { @@ -8,15 +8,17 @@ import { faCaretDown, faCheck, faEnvelope, + faExclamationTriangle, faInfo, faInfoCircle, + faServer, faSignOut, faUser, faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQueryClient } from "@tanstack/react-query"; -import { Link, useNavigate, useRouter, useRouterState } from "@tanstack/react-router"; +import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router"; import { Mfa } from "@app/components/auth/Mfa"; import { createNotification } from "@app/components/notifications"; @@ -38,7 +40,7 @@ import { envConfig } from "@app/config/env"; import { useOrganization, useSubscription, useUser } from "@app/context"; import { isInfisicalCloud } from "@app/helpers/platform"; import { useToggle } from "@app/hooks"; -import { useGetOrganizations, useLogoutUser, workspaceKeys } from "@app/hooks/api"; +import { projectKeys, useGetOrganizations, useLogoutUser } from "@app/hooks/api"; import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { getAuthToken } from "@app/hooks/api/reactQuery"; @@ -47,6 +49,7 @@ import { AuthMethod } from "@app/hooks/api/users/types"; import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel"; +import { NotificationDropdown } from "./NotificationDropdown"; const getPlan = (subscription: SubscriptionPlan) => { if (subscription.groups) return "Enterprise"; @@ -109,6 +112,14 @@ export const Navbar = () => { const { subscription } = useSubscription(); const { currentOrg } = useOrganization(); const [showAdminsModal, setShowAdminsModal] = useState(false); + const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false); + + useEffect(() => { + if (subscription?.cardDeclined && !sessionStorage.getItem("paymentFailed")) { + sessionStorage.setItem("paymentFailed", "true"); + setShowCardDeclinedModal(true); + } + }, [subscription]); const { data: orgs } = useGetOrganizations(); const navigate = useNavigate(); @@ -118,12 +129,13 @@ export const Navbar = () => { const router = useRouter(); const queryClient = useQueryClient(); + const location = useLocation(); const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context }); const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined; const handleOrgChange = async (orgId: string) => { queryClient.removeQueries({ queryKey: authKeys.getAuthToken }); - queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); + queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() }); const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId: orgId @@ -179,6 +191,8 @@ export const Navbar = () => { ); } + const isServerAdminPanel = location.pathname.startsWith("/admin"); + return (
@@ -187,100 +201,134 @@ export const Navbar = () => {

/

-
- - -
-
- -
-
{currentOrg?.name}
-
- {getPlan(subscription)} -
-
- - -
- - - -
-
- + -
organizations
- {orgs?.map((org) => { - return ( - - + + +
+ + +
organizations
+ {orgs?.map((org) => { + return ( + + + + ); + })} +
+ } onClick={logOutUser}> + Log Out - ); - })} -
- } onClick={logOutUser}> - Log Out - - - -
-

/

- {breadcrumbs ? ( - - ) : null} + + +
+

/

+ {breadcrumbs ? ( + + ) : null} + + )}
-
+
@@ -334,9 +382,10 @@ export const Navbar = () => { )} + -
+
@@ -401,6 +450,49 @@ export const Navbar = () => { + + + + Your payment could not be processed. +
+ } + > +
+
+
+

+ We were unable to process your last payment + {subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. + Please update your payment information to continue using premium features. +

+
+
+
+ + + + +
+
+
+
+ +
diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Notification.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Notification.tsx new file mode 100644 index 000000000..59e6861fc --- /dev/null +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Notification.tsx @@ -0,0 +1,60 @@ +import Markdown from "react-markdown"; +import { faCircle, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { formatDistance } from "date-fns"; +import { twMerge } from "tailwind-merge"; + +import { IconButton, Tooltip } from "@app/components/v2"; +import { TUserNotification } from "@app/hooks/api/notifications/types"; + +type Props = { + notification: TUserNotification; + onDelete: (notificationId: string) => void; +}; + +export const Notification = ({ notification, onDelete }: Props) => { + return ( +
+
+
+ {!notification.isRead && ( + + )} + {notification.title}} delayDuration={300}> + + {notification.title} + + + + {formatDistance(notification.createdAt, new Date())} ago + +
+ {notification.body && ( + + {notification.body} + + )} +
+
+ { + e.stopPropagation(); + onDelete(notification.id); + }} + > + + +
+
+ ); +}; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NotificationDropdown.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NotificationDropdown.tsx new file mode 100644 index 000000000..a5e9166d9 --- /dev/null +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NotificationDropdown.tsx @@ -0,0 +1,116 @@ +import { useMemo } from "react"; +import { faBell } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useRouter } from "@tanstack/react-router"; + +import { + ContentLoader, + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger +} from "@app/components/v2"; +import { + useDeleteNotification, + useMarkAllNotificationsAsRead, + useUpdateNotification +} from "@app/hooks/api/notifications/mutations"; +import { useGetMyNotifications } from "@app/hooks/api/notifications/queries"; + +import { Notification } from "./Notification"; + +export const NotificationDropdown = () => { + const router = useRouter(); + + const { data: notifications, isLoading } = useGetMyNotifications(); + const { mutate: markAllAsRead } = useMarkAllNotificationsAsRead(); + const { mutate: updateNotification } = useUpdateNotification(); + const { mutate: deleteNotification } = useDeleteNotification(); + + const unreadCount = useMemo( + () => notifications?.filter((n) => !n.isRead).length || 0, + [notifications] + ); + + return ( + + +
+ + {unreadCount > 0 && ( + + {unreadCount > 99 ? "99+" : unreadCount} + + )} +
+
+ +
+
+ Notifications + +
+
+ {isLoading && ( +
+ +
+ )} + {!isLoading && notifications?.length === 0 && ( +
+ + No new notifications + + We'll let you know when something important happens. + +
+ )} + {!isLoading && notifications && notifications.length > 0 && ( +
+ {notifications.map((notification) => ( +
{ + if (!notification.isRead) { + updateNotification({ notificationId: notification.id, isRead: true }); + } + if (notification.link) { + router.navigate({ to: notification.link }); + } + }} + onKeyDown={(e) => { + if (e.key !== "Enter") return; + if (!notification.isRead) { + updateNotification({ notificationId: notification.id, isRead: true }); + } + if (notification.link) { + router.navigate({ to: notification.link }); + } + }} + > + +
+ ))} +
+ )} +
+
+
+
+ ); +}; diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgAlertBanner/OrgAlertBanner.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgAlertBanner/OrgAlertBanner.tsx index de4b65a71..80adce989 100644 --- a/frontend/src/layouts/OrganizationLayout/components/OrgAlertBanner/OrgAlertBanner.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/OrgAlertBanner/OrgAlertBanner.tsx @@ -27,11 +27,11 @@ export const OrgAlertBanner = ({ text, link }: Props) => { target="_blank" className="group flex items-center" > - + here diff --git a/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx b/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx index da9b5acdf..ec4727330 100644 --- a/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx +++ b/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx @@ -7,6 +7,7 @@ import { faFileLines, faHome, faMobile, + faPlug, faSitemap, faStamp, faUsers @@ -16,12 +17,12 @@ import { Link, Outlet } from "@tanstack/react-router"; import { motion } from "framer-motion"; import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; -import { useProjectPermission, useWorkspace } from "@app/context"; +import { useProject, useProjectPermission } from "@app/context"; import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; export const PkiManagerLayout = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { assumedPrivilegeDetails } = useProjectPermission(); const { t } = useTranslation(); @@ -48,7 +49,7 @@ export const PkiManagerLayout = () => { {({ isActive }) => ( @@ -65,7 +66,7 @@ export const PkiManagerLayout = () => { {({ isActive }) => ( @@ -82,7 +83,7 @@ export const PkiManagerLayout = () => { {({ isActive }) => ( @@ -99,7 +100,7 @@ export const PkiManagerLayout = () => { {({ isActive }) => ( @@ -116,7 +117,7 @@ export const PkiManagerLayout = () => { {({ isActive }) => ( @@ -130,12 +131,29 @@ export const PkiManagerLayout = () => { )} + + {({ isActive }) => ( + +
+
+ +
+ App Connections +
+
+ )} + {({ isActive }) => ( @@ -152,7 +170,7 @@ export const PkiManagerLayout = () => { {({ isActive }) => ( @@ -169,7 +187,7 @@ export const PkiManagerLayout = () => { {({ isActive }) => ( diff --git a/frontend/src/layouts/ProjectLayout/components/AssumePrivilegeModeBanner/AssumePrivilegeModeBanner.tsx b/frontend/src/layouts/ProjectLayout/components/AssumePrivilegeModeBanner/AssumePrivilegeModeBanner.tsx index 995083ee7..754d80547 100644 --- a/frontend/src/layouts/ProjectLayout/components/AssumePrivilegeModeBanner/AssumePrivilegeModeBanner.tsx +++ b/frontend/src/layouts/ProjectLayout/components/AssumePrivilegeModeBanner/AssumePrivilegeModeBanner.tsx @@ -2,13 +2,13 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button } from "@app/components/v2"; -import { useProjectPermission, useWorkspace } from "@app/context"; +import { useProject, useProjectPermission } from "@app/context"; import { getProjectHomePage } from "@app/helpers/project"; import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api"; import { ActorType } from "@app/hooks/api/auditLogs/enums"; export const AssumePrivilegeModeBanner = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const exitAssumePrivilegeMode = useRemoveAssumeProjectPrivilege(); const { assumedPrivilegeDetails } = useProjectPermission(); @@ -32,15 +32,12 @@ export const AssumePrivilegeModeBanner = () => { onClick={() => { exitAssumePrivilegeMode.mutate( { - projectId: currentWorkspace.id + projectId: currentProject.id }, { onSuccess: () => { - const url = getProjectHomePage( - currentWorkspace.type, - currentWorkspace.environments - ); - window.location.href = url.replace("$projectId", currentWorkspace.id); + const url = getProjectHomePage(currentProject.type, currentProject.environments); + window.location.href = url.replace("$projectId", currentProject.id); } } ); diff --git a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx index ebc28a227..eb5b22938 100644 --- a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx +++ b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx @@ -28,21 +28,21 @@ import { OrgPermissionActions, OrgPermissionSubjects, useOrganization, - useSubscription, - useWorkspace + useProject, + useSubscription } from "@app/context"; import { getProjectHomePage } from "@app/helpers/project"; import { usePopUp } from "@app/hooks"; -import { useGetUserWorkspaces } from "@app/hooks/api"; +import { useGetUserProjects } from "@app/hooks/api"; +import { Project } from "@app/hooks/api/projects/types"; import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; -import { Workspace } from "@app/hooks/api/workspace/types"; export const ProjectSelect = () => { const [searchProject, setSearchProject] = useState(""); - const { currentWorkspace } = useWorkspace(); + const { currentProject: currentWorkspace } = useProject(); const { currentOrg } = useOrganization(); - const { data: workspaces = [] } = useGetUserWorkspaces(); + const { data: projects = [] } = useGetUserProjects(); const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg.id); const { subscription } = useSubscription(); @@ -86,16 +86,16 @@ export const ProjectSelect = () => { "upgradePlan" ] as const); - const projects = useMemo(() => { - const projectOptions = workspaces - .map((w): Workspace & { isFavorite: boolean } => ({ + const projectsSortedByFav = useMemo(() => { + const projectOptions = projects + .map((w): Project & { isFavorite: boolean } => ({ ...w, isFavorite: Boolean(projectFavorites?.includes(w.id)) })) .sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)); return projectOptions; - }, [workspaces, projectFavorites, currentWorkspace]); + }, [projects, projectFavorites, currentWorkspace]); return (
@@ -147,7 +147,7 @@ export const ProjectSelect = () => { />
- {projects + {projectsSortedByFav ?.filter((el) => el.name?.toLowerCase().includes(searchProject.toLowerCase())) ?.map((workspace) => { return ( @@ -200,16 +200,20 @@ export const ProjectSelect = () => {
- {(isAllowed) => ( - } - onClick={() => - handlePopUpOpen(isAddingProjectsAllowed ? "addNewWs" : "upgradePlan") - } - > - New Project - + {(isOldProjectPermissionAllowed) => ( + + {(isAllowed) => ( + } + onClick={() => + handlePopUpOpen(isAddingProjectsAllowed ? "addNewWs" : "upgradePlan") + } + > + New Project + + )} + )} diff --git a/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx b/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx index 0584e8e9c..0b9b934c3 100644 --- a/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx +++ b/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx @@ -6,6 +6,7 @@ import { faCog, faHome, faMobile, + faPlug, faPuzzlePiece, faUsers, faVault @@ -15,7 +16,7 @@ import { Link, Outlet, useLocation } from "@tanstack/react-router"; import { motion } from "framer-motion"; import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; -import { useProjectPermission, useWorkspace } from "@app/context"; +import { useProject, useProjectPermission } from "@app/context"; import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount, @@ -25,16 +26,15 @@ import { import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; export const SecretManagerLayout = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject, projectId } = useProject(); const { assumedPrivilegeDetails } = useProjectPermission(); const { t } = useTranslation(); - const workspaceId = currentWorkspace?.id || ""; - const projectSlug = currentWorkspace?.slug || ""; + const projectSlug = currentProject?.slug || ""; const location = useLocation(); const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ - workspaceId + projectId }); const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug @@ -42,7 +42,7 @@ export const SecretManagerLayout = () => { // we only show the secret rotations v1 tab if they have existing rotations const { data: secretRotations } = useGetSecretRotations({ - workspaceId, + workspaceId: projectId, options: { refetchOnMount: false } @@ -74,9 +74,9 @@ export const SecretManagerLayout = () => { @@ -85,7 +85,7 @@ export const SecretManagerLayout = () => { isSelected={ isActive || location.pathname.startsWith( - `/projects/secret-management/${currentWorkspace.id}/overview` + `/projects/secret-management/${currentProject.id}/overview` ) } > @@ -101,7 +101,7 @@ export const SecretManagerLayout = () => { {({ isActive }) => ( @@ -119,7 +119,7 @@ export const SecretManagerLayout = () => { {({ isActive }) => ( @@ -137,7 +137,7 @@ export const SecretManagerLayout = () => { {({ isActive }) => ( @@ -159,12 +159,29 @@ export const SecretManagerLayout = () => { )} + + {({ isActive }) => ( + +
+
+ +
+ App Connections +
+
+ )} + {({ isActive }) => ( @@ -181,7 +198,7 @@ export const SecretManagerLayout = () => { {({ isActive }) => ( @@ -198,7 +215,7 @@ export const SecretManagerLayout = () => { {({ isActive }) => ( diff --git a/frontend/src/layouts/SecretScanningLayout/SecretScanningLayout.tsx b/frontend/src/layouts/SecretScanningLayout/SecretScanningLayout.tsx index 265fcc003..af7bba5d1 100644 --- a/frontend/src/layouts/SecretScanningLayout/SecretScanningLayout.tsx +++ b/frontend/src/layouts/SecretScanningLayout/SecretScanningLayout.tsx @@ -4,6 +4,7 @@ import { faDatabase, faHome, faMagnifyingGlass, + faPlug, faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -13,9 +14,9 @@ import { motion } from "framer-motion"; import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; import { ProjectPermissionSub, + useProject, useProjectPermission, - useSubscription, - useWorkspace + useSubscription } from "@app/context"; import { ProjectPermissionSecretScanningFindingActions } from "@app/context/ProjectPermissionContext/types"; import { useGetSecretScanningUnresolvedFindingCount } from "@app/hooks/api/secretScanningV2"; @@ -23,14 +24,14 @@ import { useGetSecretScanningUnresolvedFindingCount } from "@app/hooks/api/secre import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; export const SecretScanningLayout = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { assumedPrivilegeDetails } = useProjectPermission(); const { permission } = useProjectPermission(); const { subscription } = useSubscription(); const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount( - currentWorkspace.id, + currentProject.id, { enabled: subscription.secretScanning && @@ -64,7 +65,7 @@ export const SecretScanningLayout = () => { {({ isActive }) => ( @@ -81,7 +82,7 @@ export const SecretScanningLayout = () => { {({ isActive }) => ( @@ -100,12 +101,29 @@ export const SecretScanningLayout = () => { )} + + {({ isActive }) => ( + +
+
+ +
+ App Connections +
+
+ )} +
{({ isActive }) => ( @@ -122,7 +140,7 @@ export const SecretScanningLayout = () => { {({ isActive }) => ( @@ -139,7 +157,7 @@ export const SecretScanningLayout = () => { {({ isActive }) => ( diff --git a/frontend/src/layouts/SshLayout/SshLayout.tsx b/frontend/src/layouts/SshLayout/SshLayout.tsx index 9c807723e..92872ec1f 100644 --- a/frontend/src/layouts/SshLayout/SshLayout.tsx +++ b/frontend/src/layouts/SshLayout/SshLayout.tsx @@ -15,14 +15,14 @@ import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, - useProjectPermission, - useWorkspace + useProject, + useProjectPermission } from "@app/context"; import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; export const SshLayout = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { assumedPrivilegeDetails } = useProjectPermission(); return ( @@ -47,7 +47,7 @@ export const SshLayout = () => { {({ isActive }) => ( @@ -70,7 +70,7 @@ export const SshLayout = () => { {({ isActive }) => ( @@ -93,7 +93,7 @@ export const SshLayout = () => { {({ isActive }) => ( @@ -110,7 +110,7 @@ export const SshLayout = () => { {({ isActive }) => ( @@ -127,7 +127,7 @@ export const SshLayout = () => { {({ isActive }) => ( diff --git a/frontend/src/pages/admin/OrganizationResourcesPage/OrganizationResourcesPage.tsx b/frontend/src/pages/admin/AccessManagementPage/AccessManagementPage.tsx similarity index 60% rename from frontend/src/pages/admin/OrganizationResourcesPage/OrganizationResourcesPage.tsx rename to frontend/src/pages/admin/AccessManagementPage/AccessManagementPage.tsx index 34bc0e57b..8128a53e0 100644 --- a/frontend/src/pages/admin/OrganizationResourcesPage/OrganizationResourcesPage.tsx +++ b/frontend/src/pages/admin/AccessManagementPage/AccessManagementPage.tsx @@ -3,23 +3,23 @@ import { useTranslation } from "react-i18next"; import { PageHeader } from "@app/components/v2"; -import { OrganizationsTable } from "./components"; +import { ServerAdminsTable } from "./components"; -export const OrganizationResourcesPage = () => { +export const AccessManagementPage = () => { const { t } = useTranslation(); return (
- {t("common.head-title", { title: "Admin" })} + {t("common.head-title", { title: "Access Control" })}
- +
diff --git a/frontend/src/pages/admin/AccessManagementPage/components/AddServerAdminModal.tsx b/frontend/src/pages/admin/AccessManagementPage/components/AddServerAdminModal.tsx new file mode 100644 index 000000000..aa02d40e8 --- /dev/null +++ b/frontend/src/pages/admin/AccessManagementPage/components/AddServerAdminModal.tsx @@ -0,0 +1,141 @@ +import { useMemo, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FilterableSelect, FormControl, Modal, ModalContent } from "@app/components/v2"; +import { useDebounce } from "@app/hooks"; +import { useAdminGetUsers, useAdminGrantServerAdminAccess } from "@app/hooks/api"; +import { User } from "@app/hooks/api/users/types"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +type ContentProps = { + onClose: () => void; +}; + +const getUserLabel = (user: Pick) => { + const { firstName, lastName, username, email } = user; + + const name = `${firstName ?? ""} ${lastName ?? ""}`.trim(); + const userEmail = email || username; + + if (!name) return userEmail; + + return `${name}${userEmail ? ` (${userEmail})` : ""}`; +}; + +const AddServerAdminSchema = z.object({ + user: z.object({ + id: z.string(), + firstName: z.string().nullish(), + lastName: z.string().nullish(), + email: z.string().nullish(), + username: z.string() + }) +}); + +type FormData = z.infer; + +const Content = ({ onClose }: ContentProps) => { + const grantAdmin = useAdminGrantServerAdminAccess(); + + const { + handleSubmit, + control, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(AddServerAdminSchema) + }); + + const [searchUserFilter, setSearchUserFilter] = useState(""); + const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500); + + const { data, isFetching } = useAdminGetUsers( + { + limit: 20, + searchTerm: debouncedSearchTerm, + adminsOnly: false + }, + { + placeholderData: (prev) => prev + } + ); + + const users = useMemo(() => data?.pages.flat().filter((user) => !user.superAdmin) ?? [], [data]); + + const onSubmit = async ({ user }: FormData) => { + try { + await grantAdmin.mutateAsync(user.id); + + createNotification({ + type: "success", + text: "Successfully granted server admin status" + }); + onClose(); + } catch { + createNotification({ + text: "Failed to grant server admin status", + type: "error" + }); + } + }; + + return ( +
+ ( + + getUserLabel(user)} + getOptionValue={(user) => user.id} + value={field.value} + onChange={field.onChange} + onInputChange={(value) => { + setSearchUserFilter(value); + if (!value) setDebouncedSearchTerm(""); + }} + /> + + )} + control={control} + name="user" + /> +
+ + +
+ + ); +}; + +export const AddServerAdminModal = ({ isOpen, onOpenChange }: Props) => { + return ( + + + onOpenChange(false)} /> + + + ); +}; diff --git a/frontend/src/pages/admin/AccessManagementPage/components/ServerAdminsTable.tsx b/frontend/src/pages/admin/AccessManagementPage/components/ServerAdminsTable.tsx new file mode 100644 index 000000000..e9298c62b --- /dev/null +++ b/frontend/src/pages/admin/AccessManagementPage/components/ServerAdminsTable.tsx @@ -0,0 +1,476 @@ +import { Dispatch, SetStateAction, useState } from "react"; +import { + faEllipsisV, + faMagnifyingGlass, + faPlus, + faShieldHalved, + faTrash, + faUsers, + faUserXmark, + faWarning, + faXmark +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { InfiniteData } from "@tanstack/react-query"; +import { twMerge } from "tailwind-merge"; + +import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; +import { createNotification } from "@app/components/notifications"; +import { + Badge, + Button, + Checkbox, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + EmptyState, + IconButton, + Input, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { useSubscription, useUser } from "@app/context"; +import { useDebounce, usePopUp } from "@app/hooks"; +import { + useAdminBulkDeleteUsers, + useAdminDeleteUser, + useAdminGetUsers, + useRemoveUserServerAdminAccess +} from "@app/hooks/api"; +import { User } from "@app/hooks/api/users/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; +import { AddServerAdminModal } from "@app/pages/admin/AccessManagementPage/components/AddServerAdminModal"; + +const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions from user"; + +const ServerAdminsPanelTable = ({ + handlePopUpOpen, + users: usersPages, + isPending, + searchUserFilter, + setSearchUserFilter, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + selectedUsers, + setSelectedUsers +}: { + handlePopUpOpen: ( + popUpName: keyof UsePopUpState< + ["removeUser", "upgradePlan", "addServerAdmin", "removeServerAdmin"] + >, + data?: { + username: string; + id: string; + message?: string; + } + ) => void; + isPending: boolean; + users: InfiniteData | undefined; + searchUserFilter: string; + setSearchUserFilter: (filter: string) => void; + selectedUsers: User[]; + setSelectedUsers: Dispatch>; + isFetchingNextPage: boolean; + fetchNextPage: () => void; + hasNextPage: boolean; +}) => { + const { subscription } = useSubscription(); + + const users = usersPages?.pages.flat(); + + const isEmpty = !isPending && !users?.length; + + const selectedUserIds = selectedUsers.map((user) => user.id); + + const isPageSelected = users?.length + ? users.every((user) => selectedUserIds.includes(user.id)) + : false; + + // eslint-disable-next-line no-nested-ternary + const isPageIndeterminate = isPageSelected + ? false + : users?.length + ? users?.some((user) => selectedUserIds.includes(user.id)) + : false; + + return ( + <> +
+ setSearchUserFilter(e.target.value)} + leftIcon={} + placeholder="Search admins..." + className="flex-1" + /> + +
+
+ + + + + + + + + + + {isPending && } + {!isPending && + users?.map((user) => { + const { username, email, firstName, lastName, id } = user; + const name = firstName || lastName ? `${firstName} ${lastName}` : null; + + const isSelected = selectedUserIds.includes(id); + return ( + + + + + + + ); + })} + +
+ { + if (isPageSelected) { + setSelectedUsers((prev) => + prev.filter((u) => !users?.find((user) => user.id === u.id)) + ); + } else { + setSelectedUsers((prev) => [ + ...prev, + ...(users?.filter((u) => !prev.find((user) => user.id === u.id)) ?? []) + ]); + } + }} + /> + NameUsername +
+ { + e.stopPropagation(); + setSelectedUsers((prev) => + isSelected ? prev.filter((u) => u.id !== id) : [...prev, user] + ); + }} + /> + +

+ {name ?? Not Set} +

+
+

{username || email}

+
+
+ + + + + + + + { + e.stopPropagation(); + handlePopUpOpen("removeUser", { username, id }); + }} + icon={} + > + Remove User + + + + +
+ } + onClick={(e) => { + e.stopPropagation(); + if (!subscription?.instanceUserManagement) { + handlePopUpOpen("upgradePlan", { + username, + id, + message: removeServerAdminUpgradePlanMessage + }); + return; + } + handlePopUpOpen("removeServerAdmin", { username, id }); + }} + > + Remove Server Admin + + + + +
+ {!isPending && isEmpty && } +
+ {!isEmpty && ( + + )} +
+ + ); +}; + +export const ServerAdminsTable = () => { + const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ + "removeUser", + "upgradePlan", + "addServerAdmin", + "removeServerAdmin", + "removeUsers" + ] as const); + + const { + user: { id: userId } + } = useUser(); + + const { mutateAsync: deleteUser } = useAdminDeleteUser(); + const { mutateAsync: deleteUsers } = useAdminBulkDeleteUsers(); + const { mutateAsync: removeAdminAccess } = useRemoveUserServerAdminAccess(); + + const [selectedUsers, setSelectedUsers] = useState([]); + const [searchUserFilter, setSearchUserFilter] = useState(""); + const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500); + + const { + data: users, + isPending, + isFetchingNextPage, + hasNextPage, + fetchNextPage + } = useAdminGetUsers({ + limit: 20, + searchTerm: debouncedSearchTerm, + adminsOnly: true + }); + + const handleRemoveUser = async () => { + const { id } = popUp?.removeUser?.data as { id: string; username: string }; + + try { + await deleteUser(id); + createNotification({ + type: "success", + text: "Successfully deleted user" + }); + } catch { + createNotification({ + type: "error", + text: "Error deleting user" + }); + } + + handlePopUpClose("removeUser"); + }; + + const handleRemoveServerAdminAccess = async () => { + const { id } = popUp?.removeServerAdmin?.data as { id: string; username: string }; + + try { + await removeAdminAccess(id); + createNotification({ + type: "success", + text: "Successfully removed server admin access from user" + }); + } catch { + createNotification({ + type: "error", + text: "Error removing server admin access from user" + }); + } + + handlePopUpClose("removeServerAdmin"); + }; + + const handleRemoveUsers = async () => { + try { + await deleteUsers(selectedUsers.map((user) => user.id)); + + createNotification({ + text: "Successfully removed users", + type: "success" + }); + + setSelectedUsers([]); + handlePopUpClose("removeUsers"); + } catch { + createNotification({ + text: "Failed to remove users", + type: "error" + }); + } + }; + + return ( + <> +
0 && "h-16" + )} + > +
+
{selectedUsers.length} Selected
+ + +
+
+
+ + handlePopUpToggle("removeUser", isOpen)} + onDeleteApproved={handleRemoveUser} + /> + handlePopUpToggle("removeServerAdmin", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleRemoveServerAdminAccess} + buttonText="Remove Access" + /> + handlePopUpToggle("addServerAdmin", isOpen)} + /> + handlePopUpToggle("upgradePlan", isOpen)} + text={`${popUp?.upgradePlan?.data?.message} is only available on Infisical's Pro plan and above.`} + /> + handlePopUpToggle("removeUsers", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => handleRemoveUsers()} + buttonText="Remove" + > +
+ The following users will be deleted: +
+
+
    + {selectedUsers?.map((user) => { + const email = user.email ?? user.username; + return ( +
  • +
    +

    + {user.firstName || user.lastName ? ( + <> + {`${`${user.firstName} ${user.lastName}`.trim()} `}( + {email}) + + ) : ( + {email} + )}{" "} +

    + {userId === user.id && ( + +
    + + + Deleting Yourself + +
    +
    + )} +
    +
  • + ); + })} +
+
+
+
+ + ); +}; diff --git a/frontend/src/pages/admin/AccessManagementPage/components/index.tsx b/frontend/src/pages/admin/AccessManagementPage/components/index.tsx new file mode 100644 index 000000000..fa73b9d70 --- /dev/null +++ b/frontend/src/pages/admin/AccessManagementPage/components/index.tsx @@ -0,0 +1 @@ +export * from "./ServerAdminsTable"; diff --git a/frontend/src/pages/admin/UserIdentitiesResourcesPage/route.tsx b/frontend/src/pages/admin/AccessManagementPage/route.tsx similarity index 55% rename from frontend/src/pages/admin/UserIdentitiesResourcesPage/route.tsx rename to frontend/src/pages/admin/AccessManagementPage/route.tsx index 6d1420ee5..0980fd7e3 100644 --- a/frontend/src/pages/admin/UserIdentitiesResourcesPage/route.tsx +++ b/frontend/src/pages/admin/AccessManagementPage/route.tsx @@ -1,11 +1,11 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; -import { UserIdentitiesResourcesPage } from "./UserIdentitiesResourcesPage"; +import { AccessManagementPage } from "./AccessManagementPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/admin/_admin-layout/resources/user-identities" + "/_authenticate/_inject-org-details/admin/_admin-layout/access-management" )({ - component: UserIdentitiesResourcesPage, + component: AccessManagementPage, beforeLoad: async () => { return { breadcrumbs: [ @@ -14,9 +14,9 @@ export const Route = createFileRoute( link: linkOptions({ to: "/admin" }) }, { - label: "User Identities", + label: "Access Control", link: linkOptions({ - to: "/admin/resources/user-identities" + to: "/admin/access-management" }) } ] diff --git a/frontend/src/pages/admin/GeneralPage/GeneralPage.tsx b/frontend/src/pages/admin/GeneralPage/GeneralPage.tsx index c9215a165..e5a76e93c 100644 --- a/frontend/src/pages/admin/GeneralPage/GeneralPage.tsx +++ b/frontend/src/pages/admin/GeneralPage/GeneralPage.tsx @@ -2,11 +2,13 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; import { PageHeader } from "@app/components/v2"; +import { useGetServerConfig } from "@app/hooks/api/admin"; -import { GeneralPageForm } from "./components"; +import { GeneralPageForm, UsageReportSection } from "./components"; export const GeneralPage = () => { const { t } = useTranslation(); + const { data: serverConfig } = useGetServerConfig(); return (
@@ -19,7 +21,10 @@ export const GeneralPage = () => { title="General" description="Manage general settings for your Infisical instance." /> - +
+ + {serverConfig?.isOfflineUsageReportsEnabled && } +
diff --git a/frontend/src/pages/admin/GeneralPage/components/GeneralPageForm.tsx b/frontend/src/pages/admin/GeneralPage/components/GeneralPageForm.tsx index febc7f328..da15f5eea 100644 --- a/frontend/src/pages/admin/GeneralPage/components/GeneralPageForm.tsx +++ b/frontend/src/pages/admin/GeneralPage/components/GeneralPageForm.tsx @@ -103,7 +103,7 @@ export const GeneralPageForm = () => { return (
diff --git a/frontend/src/pages/admin/GeneralPage/components/UsageReportSection.tsx b/frontend/src/pages/admin/GeneralPage/components/UsageReportSection.tsx new file mode 100644 index 000000000..d05800996 --- /dev/null +++ b/frontend/src/pages/admin/GeneralPage/components/UsageReportSection.tsx @@ -0,0 +1,53 @@ +import { faDownload, faFileAlt, faSpinner } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { Button, Card, CardTitle } from "@app/components/v2"; +import { downloadFile } from "@app/helpers/download"; +import { useGenerateUsageReport } from "@app/hooks/api/admin/mutation"; + +export const UsageReportSection = () => { + const generateUsageReport = useGenerateUsageReport(); + + const handleGenerateReport = async () => { + try { + const response = await generateUsageReport.mutateAsync(); + const { csvContent, filename } = response; + + downloadFile(csvContent, filename, "text/csv"); + + createNotification({ + text: `Usage report downloaded: "${filename}"`, + type: "success" + }); + } catch (error) { + console.error("Failed to generate usage report:", error); + createNotification({ + text: "Failed to generate usage report. Please try again.", + type: "error" + }); + } + }; + + return ( + + + + Offline Usage Reports + + +
+ Generate secure usage reports for offline license compliance and billing verification. +
+ + +
+ ); +}; diff --git a/frontend/src/pages/admin/GeneralPage/components/index.ts b/frontend/src/pages/admin/GeneralPage/components/index.ts index 15ea2f5ba..2ec57dadf 100644 --- a/frontend/src/pages/admin/GeneralPage/components/index.ts +++ b/frontend/src/pages/admin/GeneralPage/components/index.ts @@ -1 +1,2 @@ export { GeneralPageForm } from "./GeneralPageForm"; +export { UsageReportSection } from "./UsageReportSection"; diff --git a/frontend/src/pages/admin/MachineIdentitiesResourcesPage/MachineIdentitiesResourcesPage.tsx b/frontend/src/pages/admin/MachineIdentitiesResourcesPage/MachineIdentitiesResourcesPage.tsx deleted file mode 100644 index b3faefcbf..000000000 --- a/frontend/src/pages/admin/MachineIdentitiesResourcesPage/MachineIdentitiesResourcesPage.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Helmet } from "react-helmet"; -import { useTranslation } from "react-i18next"; - -import { PageHeader } from "@app/components/v2"; - -import { MachineIdentitiesTable } from "./components"; - -export const MachineIdentitiesResourcesPage = () => { - const { t } = useTranslation(); - - return ( -
- - {t("common.head-title", { title: "Admin" })} - -
-
- - -
-
-
- ); -}; diff --git a/frontend/src/pages/admin/MachineIdentitiesResourcesPage/components/index.tsx b/frontend/src/pages/admin/MachineIdentitiesResourcesPage/components/index.tsx deleted file mode 100644 index 007cc568a..000000000 --- a/frontend/src/pages/admin/MachineIdentitiesResourcesPage/components/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MachineIdentitiesTable } from "./MachineIdentitiesTable"; diff --git a/frontend/src/pages/admin/MachineIdentitiesResourcesPage/route.tsx b/frontend/src/pages/admin/MachineIdentitiesResourcesPage/route.tsx deleted file mode 100644 index 359db53b2..000000000 --- a/frontend/src/pages/admin/MachineIdentitiesResourcesPage/route.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { createFileRoute, linkOptions } from "@tanstack/react-router"; - -import { MachineIdentitiesResourcesPage } from "./MachineIdentitiesResourcesPage"; - -export const Route = createFileRoute( - "/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities" -)({ - component: MachineIdentitiesResourcesPage, - beforeLoad: async () => { - return { - breadcrumbs: [ - { - label: "Admin", - link: linkOptions({ to: "/admin" }) - }, - { - label: "Machine Identities", - link: linkOptions({ - to: "/admin/resources/machine-identities" - }) - } - ] - }; - } -}); diff --git a/frontend/src/pages/admin/OrganizationResourcesPage/components/OrganizationsTable.tsx b/frontend/src/pages/admin/OrganizationResourcesPage/components/OrganizationsTable.tsx deleted file mode 100644 index 0740d43bc..000000000 --- a/frontend/src/pages/admin/OrganizationResourcesPage/components/OrganizationsTable.tsx +++ /dev/null @@ -1,396 +0,0 @@ -import { useState } from "react"; -import { - faBuilding, - faCircleQuestion, - faEllipsis, - faMagnifyingGlass -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { createNotification } from "@app/components/notifications"; -import { - Badge, - Button, - DeleteActionModal, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - EmptyState, - Input, - Modal, - ModalContent, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tooltip, - Tr -} from "@app/components/v2"; -import { useDebounce, usePopUp } from "@app/hooks"; -import { - useAdminDeleteOrganization, - useAdminDeleteOrganizationMembership, - useAdminDeleteUser, - useAdminGetOrganizations -} from "@app/hooks/api"; -import { OrganizationWithProjects } from "@app/hooks/api/admin/types"; -import { UsePopUpState } from "@app/hooks/usePopUp"; - -const ViewMembersModalContent = ({ - popUp, - handlePopUpOpen -}: { - popUp: UsePopUpState<["viewMembers"]>; - handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["deleteOrganizationMembership", "deleteUser"]>, - data?: { - username?: string; - membershipId?: string; - userId?: string; - orgName?: string; - orgId?: string; - organization?: OrganizationWithProjects; - } - ) => void; -}) => { - const organization = popUp.viewMembers?.data?.organization as OrganizationWithProjects; - - return ( -
- {organization?.members?.map((member) => ( -
-
-
-

-

- {member.user.firstName ? ( -
- {member.user.firstName} {member.user.lastName} -
- ) : ( -

Not set

- )} -
-
-
{member.user.username || member.user.email}
- -
- {member.role.replace("-", " ")} - {Boolean(member.roleId) && ( - - - - )} -
-
-
-

-
-
-
- - - - - - - handlePopUpOpen("deleteOrganizationMembership", { - membershipId: member.membershipId, - orgId: organization.id, - username: member.user.username, - orgName: organization.name - }) - } - > - Remove From Organization - - handlePopUpOpen("deleteUser", { userId: member.user.id })} - > - Delete User - - - -
-
- ))} -
- ); -}; - -const ViewMembersModal = ({ - isOpen, - onOpenChange, - popUp, - handlePopUpOpen -}: { - isOpen: boolean; - onOpenChange: (isOpen: boolean) => void; - popUp: UsePopUpState<["viewMembers", "deleteOrganizationMembership", "deleteUser"]>; - handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["deleteOrganizationMembership", "deleteUser"]> - ) => void; -}) => { - return ( - - { - event.preventDefault(); - }} - title="Organization Members" - subTitle="View the members of the organization." - > - - - - ); -}; - -const OrganizationsPanelTable = ({ - popUp, - handlePopUpOpen, - handlePopUpToggle -}: { - popUp: UsePopUpState< - ["deleteOrganization", "viewMembers", "deleteOrganizationMembership", "deleteUser"] - >; - handlePopUpOpen: ( - popUpName: keyof UsePopUpState< - ["deleteOrganization", "viewMembers", "deleteOrganizationMembership", "deleteUser"] - >, - data?: { - orgName?: string; - orgId?: string; - message?: string; - organization?: OrganizationWithProjects; - } - ) => void; - handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["deleteOrganization", "viewMembers"]>, - isOpen?: boolean - ) => void; -}) => { - const [searchOrganizationsFilter, setSearchOrganizationsFilter] = useState(""); - const [debouncedSearchTerm] = useDebounce(searchOrganizationsFilter, 500); - - const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = - useAdminGetOrganizations({ - limit: 20, - searchTerm: debouncedSearchTerm - }); - - const isEmpty = !isPending && !data?.pages?.[0].length; - - return ( - <> -
- setSearchOrganizationsFilter(e.target.value)} - leftIcon={} - placeholder="Search organizations..." - className="flex-1" - /> -
-
- - - - - - - - - - - {isPending && } - {!isPending && - data?.pages?.map((orgs) => - orgs.map((org) => { - return ( - - - - - - - ); - }) - )} - -
NameMembersProjects -
- {org.name ? ( - org.name - ) : ( - Not set - )} - - {org.members.length} {org.members.length === 1 ? "member" : "members"} - - - {org.projects.length} {org.projects.length === 1 ? "project" : "projects"} - -
- - -
- -
-
- - { - e.stopPropagation(); - handlePopUpOpen("deleteOrganization", { - orgId: org.id, - orgName: org.name - }); - }} - > - Delete Organization - - -
-
-
- {!isPending && isEmpty && } -
- {!isEmpty && ( - - )} -
- handlePopUpToggle("viewMembers", isOpen)} - /> - - ); -}; - -export const OrganizationsTable = () => { - const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "deleteOrganization", - "deleteOrganizationMembership", - "deleteUser", - "viewMembers" - ] as const); - - const { mutateAsync: deleteOrganization } = useAdminDeleteOrganization(); - const { mutateAsync: deleteOrganizationMembership } = useAdminDeleteOrganizationMembership(); - const { mutateAsync: deleteUser } = useAdminDeleteUser(); - - const handleDeleteOrganization = async () => { - const { orgId } = popUp?.deleteOrganization?.data as { orgId: string }; - - await deleteOrganization(orgId); - createNotification({ - type: "success", - text: "Successfully deleted organization" - }); - - handlePopUpClose("deleteOrganization"); - }; - - const handleDeleteOrganizationMembership = async () => { - const { orgId, membershipId } = popUp?.deleteOrganizationMembership?.data as { - orgId: string; - membershipId: string; - }; - - if (!orgId || !membershipId) { - return; - } - - await deleteOrganizationMembership({ organizationId: orgId, membershipId }); - createNotification({ - type: "success", - text: "Successfully removed user from organization" - }); - - handlePopUpClose("viewMembers"); - handlePopUpClose("deleteOrganizationMembership"); - }; - - const handleDeleteUser = async () => { - const { userId } = popUp?.deleteUser?.data as { userId: string }; - - if (!userId) { - return; - } - - await deleteUser(userId); - createNotification({ - type: "success", - text: "Successfully deleted user" - }); - - handlePopUpClose("viewMembers"); - handlePopUpClose("deleteUser"); - }; - - return ( -
- - handlePopUpToggle("deleteOrganization", isOpen)} - onDeleteApproved={handleDeleteOrganization} - /> - handlePopUpToggle("deleteOrganizationMembership", isOpen)} - onDeleteApproved={handleDeleteOrganizationMembership} - /> - handlePopUpToggle("deleteUser", isOpen)} - onDeleteApproved={handleDeleteUser} - /> -
- ); -}; diff --git a/frontend/src/pages/admin/OrganizationResourcesPage/components/index.tsx b/frontend/src/pages/admin/OrganizationResourcesPage/components/index.tsx deleted file mode 100644 index 9a054c599..000000000 --- a/frontend/src/pages/admin/OrganizationResourcesPage/components/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { OrganizationsTable } from "./OrganizationsTable"; diff --git a/frontend/src/pages/admin/ResourceOverviewPage/ResourceOverviewPage.tsx b/frontend/src/pages/admin/ResourceOverviewPage/ResourceOverviewPage.tsx new file mode 100644 index 000000000..e9096d39a --- /dev/null +++ b/frontend/src/pages/admin/ResourceOverviewPage/ResourceOverviewPage.tsx @@ -0,0 +1,42 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; + +import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; + +import { MachineIdentitiesTable, OrganizationsTable, UserIdentitiesTable } from "./components"; + +export const ResourceOverviewPage = () => { + const { t } = useTranslation(); + + return ( +
+ + {t("common.head-title", { title: "Resource Overview" })} + +
+
+ + + + Organizations + Users + Identities + + + + + + + + + + + +
+
+
+ ); +}; diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/AddOrganizationModal.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/AddOrganizationModal.tsx new file mode 100644 index 000000000..9b96ac5c3 --- /dev/null +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/AddOrganizationModal.tsx @@ -0,0 +1,203 @@ +import { useMemo, useState } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; +import { CreatableSelect } from "@app/components/v2/CreatableSelect"; +import { useDebounce } from "@app/hooks"; +import { useAdminGetUsers, useServerAdminCreateOrganization } from "@app/hooks/api"; +import { User } from "@app/hooks/api/users/types"; +import { GenericResourceNameSchema } from "@app/lib/schemas"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +type ContentProps = { + onClose: () => void; +}; + +type Invitee = Pick; +type NewOption = { label: string; value: string }; + +const getUserLabel = (user: Invitee | NewOption) => { + if (Object.prototype.hasOwnProperty.call(user, "value")) { + return (user as NewOption).label; + } + + const { firstName, lastName, username, email } = user as Invitee; + + const name = `${firstName ?? ""} ${lastName ?? ""}`.trim(); + const userEmail = email || username; + + if (!name) return userEmail; + + return `${name}${userEmail ? ` (${userEmail})` : ""}`; +}; + +const AddOrgSchema = z.object({ + name: GenericResourceNameSchema.nonempty("Organization name required"), + invitees: z + .object({ + id: z.string(), + firstName: z.string().nullish(), + lastName: z.string().nullish(), + email: z.string().nullish(), + username: z.string().nullish() + }) + .array() + .min(1, "At least one admin is required") +}); + +type FormData = z.infer; + +const Content = ({ onClose }: ContentProps) => { + const createOrg = useServerAdminCreateOrganization(); + + const { + handleSubmit, + control, + formState: { isSubmitting } + } = useForm({ + defaultValues: { + name: "", + invitees: [] + }, + resolver: zodResolver(AddOrgSchema) + }); + + const [searchUserFilter, setSearchUserFilter] = useState(""); + const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500); + + const { data, isFetching } = useAdminGetUsers( + { + limit: 20, + searchTerm: debouncedSearchTerm, + adminsOnly: false + }, + { + placeholderData: (prev) => prev + } + ); + + const users = useMemo(() => data?.pages.flat() ?? [], [data]); + + const onSubmit = async ({ name, invitees }: FormData) => { + try { + await createOrg.mutateAsync({ + name, + inviteAdminEmails: invitees + .filter((user) => Boolean(user.email)) + .map((user) => user.email) as string[] + }); + + createNotification({ + type: "success", + text: "Successfully created organization" + }); + onClose(); + } catch { + createNotification({ + text: "Failed to create organization", + type: "error" + }); + } + }; + + const { append } = useFieldArray({ control, name: "invitees" }); + + return ( + + ( + + + + )} + control={control} + name="name" + /> + ( + + ( +

Invite new users to this organization by typing out their email address.

+ )} + onCreateOption={(inputValue) => + append({ id: `${inputValue}_${Math.random()}`, email: inputValue }) + } + formatCreateLabel={(inputValue) => `Invite "${inputValue}"`} + isValidNewOption={(input) => + Boolean(input) && + z.string().email().safeParse(input).success && + !users + ?.flatMap((user) => { + const emails: string[] = []; + + if (user.email) { + emails.push(user.email); + } + + if (user.username) { + emails.push(user.username); + } + + return emails; + }) + .includes(input) + } + isLoading={searchUserFilter !== debouncedSearchTerm || isFetching} + className="w-full" + placeholder="Search users or invite new ones..." + isMulti + name="members" + options={users} + getOptionLabel={(user) => getUserLabel(user)} + getOptionValue={(user) => user.id} + value={field.value} + onChange={field.onChange} + onInputChange={(value) => { + setSearchUserFilter(value); + if (!value) setDebouncedSearchTerm(""); + }} + /> +
+ )} + control={control} + name="invitees" + /> +
+ + +
+ + ); +}; + +export const AddOrganizationModal = ({ isOpen, onOpenChange }: Props) => { + return ( + + + onOpenChange(false)} /> + + + ); +}; diff --git a/frontend/src/pages/admin/MachineIdentitiesResourcesPage/components/MachineIdentitiesTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx similarity index 95% rename from frontend/src/pages/admin/MachineIdentitiesResourcesPage/components/MachineIdentitiesTable.tsx rename to frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx index 9c44b72c4..6370bab54 100644 --- a/frontend/src/pages/admin/MachineIdentitiesResourcesPage/components/MachineIdentitiesTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { faEllipsisV, faMagnifyingGlass, - faServer, faShieldHalved, + faWrench, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -136,7 +136,7 @@ const IdentityPanelTable = ({ )} - {!isPending && isEmpty && } + {!isPending && isEmpty && } {!isEmpty && ( + )} +
+ handlePopUpToggle("viewMembers", isOpen)} + /> + + ); +}; + +export const OrganizationsTable = () => { + const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ + "deleteOrganization", + "deleteOrganizationMembership", + "deleteUser", + "viewMembers", + "createOrganization" + ] as const); + + const { mutateAsync: deleteOrganization } = useAdminDeleteOrganization(); + const { mutateAsync: deleteOrganizationMembership } = useAdminDeleteOrganizationMembership(); + const { mutateAsync: deleteUser } = useAdminDeleteUser(); + + const handleDeleteOrganization = async () => { + const { orgId } = popUp?.deleteOrganization?.data as { orgId: string }; + + await deleteOrganization(orgId); + createNotification({ + type: "success", + text: "Successfully deleted organization" + }); + + handlePopUpClose("deleteOrganization"); + }; + + const handleDeleteOrganizationMembership = async () => { + const { orgId, membershipId } = popUp?.deleteOrganizationMembership?.data as { + orgId: string; + membershipId: string; + }; + + if (!orgId || !membershipId) { + return; + } + + await deleteOrganizationMembership({ organizationId: orgId, membershipId }); + createNotification({ + type: "success", + text: "Successfully removed user from organization" + }); + + handlePopUpClose("viewMembers"); + handlePopUpClose("deleteOrganizationMembership"); + }; + + const handleDeleteUser = async () => { + const { userId } = popUp?.deleteUser?.data as { userId: string }; + + if (!userId) { + return; + } + + await deleteUser(userId); + createNotification({ + type: "success", + text: "Successfully deleted user" + }); + + handlePopUpClose("viewMembers"); + handlePopUpClose("deleteUser"); + }; + + return ( +
+
+
+

Organizations

+

+ Manage, join and view organizations across your instance. +

+
+ +
+ + handlePopUpToggle("deleteOrganization", isOpen)} + onDeleteApproved={handleDeleteOrganization} + /> + handlePopUpToggle("deleteOrganizationMembership", isOpen)} + onDeleteApproved={handleDeleteOrganizationMembership} + /> + handlePopUpToggle("deleteUser", isOpen)} + onDeleteApproved={handleDeleteUser} + /> + handlePopUpToggle("createOrganization", isOpen)} + /> +
+ ); +}; diff --git a/frontend/src/pages/admin/UserIdentitiesResourcesPage/components/UserIdentitiesTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx similarity index 97% rename from frontend/src/pages/admin/UserIdentitiesResourcesPage/components/UserIdentitiesTable.tsx rename to frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx index 1f2471aa2..9856ee497 100644 --- a/frontend/src/pages/admin/UserIdentitiesResourcesPage/components/UserIdentitiesTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx @@ -218,7 +218,7 @@ const UserPanelTable = ({
-

{email}

+

{username || email}

@@ -463,6 +463,12 @@ export const UserIdentitiesTable = () => {
+
+
+

User Identities

+

Manage user identities across your instance.

+
+
{ /> handlePopUpToggle("removeUsers", isOpen)} deleteKey="confirm" onDeleteApproved={() => handleRemoveUsers()} - buttonText="Remove" + buttonText="Delete" >
- The following members will be removed: + The following users will be deleted:
    @@ -549,7 +555,7 @@ export const UserIdentitiesTable = () => { className="ml-1 mt-[0.05rem] inline-flex w-min items-center gap-1.5 whitespace-nowrap" > - Removing Yourself + Deleting Yourself
diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/index.ts b/frontend/src/pages/admin/ResourceOverviewPage/components/index.ts new file mode 100644 index 000000000..ce70dee05 --- /dev/null +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/index.ts @@ -0,0 +1,3 @@ +export * from "./MachineIdentitiesTable"; +export * from "./OrganizationsTable"; +export * from "./UserIdentitiesTable"; diff --git a/frontend/src/pages/admin/OrganizationResourcesPage/route.tsx b/frontend/src/pages/admin/ResourceOverviewPage/route.tsx similarity index 66% rename from frontend/src/pages/admin/OrganizationResourcesPage/route.tsx rename to frontend/src/pages/admin/ResourceOverviewPage/route.tsx index 9b0bf8f12..ea27c1a06 100644 --- a/frontend/src/pages/admin/OrganizationResourcesPage/route.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/route.tsx @@ -1,11 +1,11 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; -import { OrganizationResourcesPage } from "./OrganizationResourcesPage"; +import { ResourceOverviewPage } from "./ResourceOverviewPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations" + "/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview" )({ - component: OrganizationResourcesPage, + component: ResourceOverviewPage, beforeLoad: async () => { return { breadcrumbs: [ @@ -14,9 +14,9 @@ export const Route = createFileRoute( link: linkOptions({ to: "/admin" }) }, { - label: "Organizations", + label: "Resource Overview", link: linkOptions({ - to: "/admin/resources/organizations" + to: "/admin/resources/overview" }) } ] diff --git a/frontend/src/pages/admin/UserIdentitiesResourcesPage/UserIdentitiesResourcesPage.tsx b/frontend/src/pages/admin/UserIdentitiesResourcesPage/UserIdentitiesResourcesPage.tsx deleted file mode 100644 index 35c4d9466..000000000 --- a/frontend/src/pages/admin/UserIdentitiesResourcesPage/UserIdentitiesResourcesPage.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Helmet } from "react-helmet"; -import { useTranslation } from "react-i18next"; - -import { PageHeader } from "@app/components/v2"; - -import { UserIdentitiesTable } from "./components"; - -export const UserIdentitiesResourcesPage = () => { - const { t } = useTranslation(); - - return ( -
- - {t("common.head-title", { title: "Admin" })} - -
-
- - -
-
-
- ); -}; diff --git a/frontend/src/pages/admin/UserIdentitiesResourcesPage/components/index.tsx b/frontend/src/pages/admin/UserIdentitiesResourcesPage/components/index.tsx deleted file mode 100644 index 3fe6b81f3..000000000 --- a/frontend/src/pages/admin/UserIdentitiesResourcesPage/components/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { UserIdentitiesTable } from "./UserIdentitiesTable"; diff --git a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx index 9e6850f82..4dd0bbe24 100644 --- a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx +++ b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx @@ -88,7 +88,15 @@ export const SelectOrganizationSection = () => { // org has an org-level auth method enabled (e.g. SAML) // -> logout + redirect to SAML SSO let url = ""; - if (organization.orgAuthMethod === AuthMethod.OIDC) { + if (organization.googleSsoAuthEnforced) { + if (authToken.authMethod !== AuthMethod.GOOGLE) { + url = `/api/v1/sso/redirect/google?org_slug=${organization.slug}`; + + if (callbackPort) { + url += `&callback_port=${callbackPort}`; + } + } + } else if (organization.orgAuthMethod === AuthMethod.OIDC) { url = `/api/v1/sso/oidc/login?orgSlug=${organization.slug}${ callbackPort ? `&callbackPort=${callbackPort}` : "" }`; @@ -98,15 +106,6 @@ export const SelectOrganizationSection = () => { if (callbackPort) { url += `?callback_port=${callbackPort}`; } - } else if ( - organization.googleSsoAuthEnforced && - authToken.authMethod !== AuthMethod.GOOGLE - ) { - url = `/api/v1/sso/redirect/google?org_slug=${organization.slug}`; - - if (callbackPort) { - url += `&callback_port=${callbackPort}`; - } } // we are conditionally checking if the url is set because it may not be set if google SSO is enforced, but the user is already logged in with google SSO diff --git a/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertModal.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertModal.tsx index 04fafa0b2..5601e48db 100644 --- a/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertModal.tsx +++ b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertModal.tsx @@ -13,7 +13,7 @@ import { Select, SelectItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreatePkiAlert, useGetPkiAlertById, @@ -60,15 +60,15 @@ type Props = { }; export const PkiAlertModal = ({ popUp, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { data: alert } = useGetPkiAlertById( (popUp?.pkiAlert?.data as { alertId: string })?.alertId || "" ); const { data: pkiCollections } = useListWorkspacePkiCollections({ - workspaceId: projectId + projectId }); const { mutateAsync: createPkiAlert } = useCreatePkiAlert(); diff --git a/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsSection.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsSection.tsx index 497f10c39..d3aeefab7 100644 --- a/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsSection.tsx +++ b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsSection.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { useDeletePkiAlert } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -12,8 +12,8 @@ import { PkiAlertModal } from "./PkiAlertModal"; import { PkiAlertsTable } from "./PkiAlertsTable"; export const PkiAlertsSection = () => { - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { mutateAsync: deletePkiAlert } = useDeletePkiAlert(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ diff --git a/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsTable.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsTable.tsx index b67c5a2c6..4ddbd4671 100644 --- a/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsTable.tsx +++ b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsTable.tsx @@ -10,7 +10,7 @@ import { THead, Tr } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useListWorkspacePkiAlerts } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -24,11 +24,11 @@ type Props = { }; export const PkiAlertsTable = ({ handlePopUpOpen }: Props) => { - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { data, isPending } = useListWorkspacePkiAlerts({ - workspaceId: projectId + projectId }); return ( diff --git a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx index 366f51c62..5900a1ba9 100644 --- a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx +++ b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreatePkiCollection, useGetPkiCollectionById, @@ -28,8 +28,8 @@ type Props = { export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { data: pkiCollection } = useGetPkiCollectionById( (popUp?.pkiCollection?.data as { collectionId: string })?.collectionId || "" diff --git a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionSection.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionSection.tsx index 81f5a8609..66732ad88 100644 --- a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionSection.tsx +++ b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionSection.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { useDeletePkiCollection } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -12,8 +12,8 @@ import { PkiCollectionModal } from "./PkiCollectionModal"; import { PkiCollectionTable } from "./PkiCollectionTable"; export const PkiCollectionSection = () => { - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { mutateAsync: deletePkiCollection } = useDeletePkiCollection(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ diff --git a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionTable.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionTable.tsx index ff525b1f4..ebd86973f 100644 --- a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionTable.tsx +++ b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionTable.tsx @@ -19,7 +19,7 @@ import { THead, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { useListWorkspacePkiCollections } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -32,11 +32,11 @@ type Props = { export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { data, isPending } = useListWorkspacePkiCollections({ - workspaceId: projectId + projectId }); return ( diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx index 0bf3c4431..4e5859215 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx @@ -15,7 +15,7 @@ import { Tooltip } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { CaType, useDeleteCa, useGetCa } from "@app/hooks/api"; import { TInternalCertificateAuthority } from "@app/hooks/api/ca/types"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -30,7 +30,7 @@ import { } from "./components"; const Page = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const navigate = useNavigate(); const params = useParams({ from: ROUTE_PATHS.CertManager.CertAuthDetailsByIDPage.id @@ -38,11 +38,11 @@ const Page = () => { const { caName } = params as { caName: string }; const { data } = useGetCa({ caName, - projectId: currentWorkspace?.id || "", + projectId: currentProject?.id || "", type: CaType.INTERNAL }) as { data: TInternalCertificateAuthority }; - const projectId = currentWorkspace?.id || ""; + const projectId = currentProject?.id || ""; const { mutateAsync: deleteCa } = useDeleteCa(); @@ -55,11 +55,11 @@ const Page = () => { const onRemoveCaSubmit = async () => { try { - if (!currentWorkspace?.slug) return; + if (!currentProject?.slug) return; await deleteCa({ caName, - projectId: currentWorkspace.id, + projectId: currentProject.id, type: CaType.INTERNAL }); diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx index f12fcfd28..9ee5c1b8b 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx @@ -4,7 +4,7 @@ import { format } from "date-fns"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, IconButton, Tooltip } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { useTimedReset } from "@app/hooks"; import { CaStatus, CaType, InternalCaType, useGetCa } from "@app/hooks/api"; import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants"; @@ -21,7 +21,7 @@ type Props = { }; export const CaDetailsSection = ({ caName, handlePopUpOpen }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ initialState: "Copy ID to clipboard" }); @@ -31,7 +31,7 @@ export const CaDetailsSection = ({ caName, handlePopUpOpen }: Props) => { const { data } = useGetCa({ caName, - projectId: currentWorkspace.id, + projectId: currentProject.id, type: CaType.INTERNAL }); diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaRenewalModal.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaRenewalModal.tsx index 6f9cf3a1c..857586a58 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaRenewalModal.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaRenewalModal.tsx @@ -13,7 +13,7 @@ import { Select, SelectItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { CaRenewalType, useRenewCa @@ -45,8 +45,8 @@ type Props = { }; export const CaRenewalModal = ({ popUp, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); - const projectSlug = currentWorkspace?.slug || ""; + const { currentProject } = useProject(); + const projectSlug = currentProject?.slug || ""; const popUpData = popUp?.renewCa?.data as { caId: string; diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/ExternalCaInstallForm.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/ExternalCaInstallForm.tsx index 6f79cbb24..d737e6c45 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/ExternalCaInstallForm.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/ExternalCaInstallForm.tsx @@ -8,7 +8,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, IconButton, TextArea, Tooltip } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useTimedReset } from "@app/hooks"; import { useGetCaCsr, useImportCaCertificate } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -26,7 +26,7 @@ type Props = { }; export const ExternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [copyTextCaCsr, isCopyingCaCsr, setCopyTextCaCsr] = useTimedReset({ initialState: "Copy to clipboard" }); @@ -41,7 +41,7 @@ export const ExternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => { }); const { data: csr } = useGetCaCsr(caId); - const { mutateAsync: importCaCertificate } = useImportCaCertificate(currentWorkspace.id); + const { mutateAsync: importCaCertificate } = useImportCaCertificate(currentProject.id); useEffect(() => { reset(); @@ -49,11 +49,11 @@ export const ExternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => { const onFormSubmit = async ({ certificate, certificateChain }: FormData) => { try { - if (!csr || !caId || !currentWorkspace?.slug) return; + if (!csr || !caId || !currentProject?.slug) return; await importCaCertificate({ caId, - projectSlug: currentWorkspace?.slug, + projectSlug: currentProject?.slug, certificate, certificateChain }); diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx index 677407dc8..5979d9711 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { CaStatus, useGetCaById, @@ -46,16 +46,16 @@ type Props = { }; export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: cas } = useListWorkspaceCas({ - projectSlug: currentWorkspace?.slug ?? "", + projectId: currentProject.id, status: CaStatus.ACTIVE }); const { data: ca } = useGetCaById(caId); const { data: csr } = useGetCaCsr(caId); const { mutateAsync: signIntermediate } = useSignIntermediate(); - const { mutateAsync: importCaCertificate } = useImportCaCertificate(currentWorkspace.id); + const { mutateAsync: importCaCertificate } = useImportCaCertificate(currentProject.id); const { control, @@ -102,7 +102,7 @@ export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => { const onFormSubmit = async ({ notAfter, maxPathLength }: FormData) => { try { - if (!csr || !caId || !currentWorkspace?.slug) return; + if (!csr || !caId || !currentProject?.slug) return; const { certificate, certificateChain } = await signIntermediate({ caId: parentCaId, @@ -114,7 +114,7 @@ export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => { await importCaCertificate({ caId, - projectSlug: currentWorkspace?.slug, + projectSlug: currentProject?.slug, certificate, certificateChain }); diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx index d3040576f..2a976d682 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx @@ -16,7 +16,7 @@ import { Switch // DatePicker } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { CaStatus, CaType, @@ -84,10 +84,10 @@ const caTypes = [ ]; export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: ca } = useGetCa({ caName: (popUp?.ca?.data as { name: string })?.name || "", - projectId: currentWorkspace?.id || "", + projectId: currentProject?.id || "", type: CaType.INTERNAL }); @@ -178,13 +178,13 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { configuration }: FormData) => { try { - if (!currentWorkspace?.slug) return; + if (!currentProject?.slug) return; if (ca) { // update await updateMutateAsync({ caName: ca.name, - projectId: currentWorkspace.id, + projectId: currentProject.id, name, type: CaType.INTERNAL, status, @@ -193,7 +193,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { } else { // create await createMutateAsync({ - projectId: currentWorkspace.id, + projectId: currentProject.id, name, type, status, diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx index 5de6027fa..ed4e8f847 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx @@ -5,7 +5,7 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { CaStatus, CaType, useDeleteCa, useUpdateCa } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -15,7 +15,7 @@ import { CaModal } from "./CaModal"; import { CaTable } from "./CaTable"; export const CaSection = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync: deleteCa } = useDeleteCa(); const { mutateAsync: updateCa } = useUpdateCa(); @@ -30,9 +30,9 @@ export const CaSection = () => { const onRemoveCaSubmit = async (caName: string) => { try { - if (!currentWorkspace?.slug) return; + if (!currentProject?.slug) return; - await deleteCa({ caName, projectId: currentWorkspace.id, type: CaType.INTERNAL }); + await deleteCa({ caName, projectId: currentProject.id, type: CaType.INTERNAL }); createNotification({ text: "Successfully deleted CA", @@ -50,9 +50,9 @@ export const CaSection = () => { const onUpdateCaStatus = async ({ caName, status }: { caName: string; status: CaStatus }) => { try { - if (!currentWorkspace?.slug) return; + if (!currentProject?.slug) return; - await updateCa({ caName, projectId: currentWorkspace.id, type: CaType.INTERNAL, status }); + await updateCa({ caName, projectId: currentProject.id, type: CaType.INTERNAL, status }); createNotification({ text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`, diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx index 00ab03625..81fce98c6 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx @@ -22,7 +22,7 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api"; import { caStatusToNameMap, @@ -49,8 +49,8 @@ type Props = { export const CaTable = ({ handlePopUpOpen }: Props) => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); - const { data, isPending } = useListCasByTypeAndProjectId(CaType.INTERNAL, currentWorkspace.id); + const { currentProject } = useProject(); + const { data, isPending } = useListCasByTypeAndProjectId(CaType.INTERNAL, currentProject.id); const cas = data as TInternalCertificateAuthority[]; return ( @@ -80,7 +80,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { navigate({ to: "/projects/cert-management/$projectId/ca/$caName", params: { - projectId: currentWorkspace.id, + projectId: currentProject.id, caName: ca.name } }) diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx index 68757f549..121c32e5f 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx @@ -4,6 +4,7 @@ import { SingleValue } from "react-select"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; +import { AppConnectionOption } from "@app/components/app-connections"; import { createNotification } from "@app/components/notifications"; import { Button, @@ -16,7 +17,7 @@ import { SelectItem, Switch } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { TAvailableAppConnection, @@ -127,11 +128,11 @@ const caTypes = [ ]; export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: ca, isLoading: isCaLoading } = useGetCa({ caName: (popUp?.ca?.data as { name: string })?.name || "", - projectId: currentWorkspace?.id || "", + projectId: currentProject?.id || "", type: (popUp?.ca?.data as { type: CaType })?.type || "" }); @@ -201,17 +202,17 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { }, [popUp?.ca?.isOpen, popUp?.ca?.data, reset, ca]); const { data: availableRoute53Connections, isPending: isRoute53Pending } = - useListAvailableAppConnections(AppConnection.AWS, { + useListAvailableAppConnections(AppConnection.AWS, currentProject.id, { enabled: caType === CaType.ACME }); const { data: availableCloudflareConnections, isPending: isCloudflarePending } = - useListAvailableAppConnections(AppConnection.Cloudflare, { + useListAvailableAppConnections(AppConnection.Cloudflare, currentProject.id, { enabled: caType === CaType.ACME }); const { data: availableAzureConnections, isPending: isAzurePending } = - useListAvailableAppConnections(AppConnection.AzureADCS, { + useListAvailableAppConnections(AppConnection.AzureADCS, currentProject.id, { enabled: caType === CaType.AZURE_AD_CS }); @@ -297,7 +298,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { configuration: formConfiguration }: FormData) => { try { - if (!currentWorkspace?.slug) return; + if (!currentProject?.slug) return; let configPayload: any; @@ -321,7 +322,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { if (ca) { await updateMutateAsync({ caName: ca.name, - projectId: currentWorkspace.id, + projectId: currentProject.id, name, type, status, @@ -330,7 +331,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { }); } else { await createMutateAsync({ - projectId: currentWorkspace.id, + projectId: currentProject.id, name, type, status, @@ -457,6 +458,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { placeholder="Select connection..." getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} + components={{ Option: AppConnectionOption }} /> )} @@ -599,6 +601,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { placeholder="Select connection..." getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} + components={{ Option: AppConnectionOption }} /> )} diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaSection.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaSection.tsx index c8d117e21..7e60b0373 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaSection.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaSection.tsx @@ -5,7 +5,7 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { CaStatus, CaType, useDeleteCa, useUpdateCa } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -13,7 +13,7 @@ import { ExternalCaModal } from "./ExternalCaModal"; import { ExternalCaTable } from "./ExternalCaTable"; export const ExternalCaSection = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync: deleteCa } = useDeleteCa(); const { mutateAsync: updateCa } = useUpdateCa(); @@ -26,9 +26,9 @@ export const ExternalCaSection = () => { const onRemoveCaSubmit = async (caName: string, type: CaType) => { try { - if (!currentWorkspace?.id) return; + if (!currentProject?.id) return; - await deleteCa({ caName, type, projectId: currentWorkspace.id }); + await deleteCa({ caName, type, projectId: currentProject.id }); createNotification({ text: "Successfully deleted CA", @@ -54,9 +54,9 @@ export const ExternalCaSection = () => { status: CaStatus; }) => { try { - if (!currentWorkspace?.slug) return; + if (!currentProject?.slug) return; - await updateCa({ caName: name, type, status, projectId: currentWorkspace.id }); + await updateCa({ caName: name, type, status, projectId: currentProject.id }); createNotification({ text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`, diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx index e7b18f429..a948473e8 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx @@ -26,7 +26,7 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { CaStatus, CaType, useListExternalCasByProjectId } from "@app/hooks/api"; import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -44,8 +44,8 @@ type Props = { }; export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { - const { currentWorkspace } = useWorkspace(); - const { data, isPending } = useListExternalCasByProjectId(currentWorkspace.id); + const { currentProject } = useProject(); + const { data, isPending } = useListExternalCasByProjectId(currentProject.id); return (
diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateImportModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateImportModal.tsx index c4cffe7b0..70b51cbbe 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateImportModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateImportModal.tsx @@ -14,7 +14,7 @@ import { SelectItem, TextArea } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useGetCert, useImportCertificate, useListWorkspacePkiCollections } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -48,13 +48,13 @@ type TCertificateDetails = { export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => { const [certificateDetails, setCertificateDetails] = useState(null); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: cert } = useGetCert( (popUp?.certificateImport?.data as { serialNumber: string })?.serialNumber || "" ); const { data } = useListWorkspacePkiCollections({ - workspaceId: currentWorkspace?.id || "" + projectId: currentProject?.id || "" }); const { mutateAsync: importCertificate } = useImportCertificate(); @@ -76,10 +76,10 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => { collectionId }: FormData) => { try { - if (!currentWorkspace?.slug) return; + if (!currentProject?.slug) return; const { serialNumber, certificate, certificateChain, privateKey } = await importCertificate({ - projectSlug: currentWorkspace.slug, + projectSlug: currentProject.slug, certificatePem, privateKeyPem, diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx index a10743023..a8509fe17 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx @@ -22,7 +22,7 @@ import { SelectItem, Tooltip } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { CaStatus, useCreateCertificate, @@ -89,22 +89,22 @@ const CERT_TEMPLATE_NONE_VALUE = "none"; export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { const [certificateDetails, setCertificateDetails] = useState(null); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: cert } = useGetCert( (popUp?.certificate?.data as { serialNumber: string })?.serialNumber || "" ); const { data: cas } = useListWorkspaceCas({ - projectSlug: currentWorkspace?.slug ?? "", + projectId: currentProject.id, status: CaStatus.ACTIVE }); const { data } = useListWorkspacePkiCollections({ - workspaceId: currentWorkspace?.id || "" + projectId: currentProject?.id || "" }); const { data: templatesData } = useListWorkspaceCertificateTemplates({ - workspaceId: currentWorkspace?.id || "" + projectId: currentProject?.id || "" }); const { mutateAsync: createCertificate } = useCreateCertificate(); @@ -191,12 +191,12 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { extendedKeyUsages }: FormData) => { try { - if (!currentWorkspace?.slug) return; + if (!currentProject?.slug) return; const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({ caId: !selectedCertTemplate ? caId : undefined, certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined, - projectSlug: currentWorkspace.slug, + projectSlug: currentProject.slug, pkiCollectionId: collectionId, friendlyName, commonName, diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx index 2f14fda3f..1d1539296 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx @@ -4,7 +4,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useRevokeCert } from "@app/hooks/api"; import { crlReasons } from "@app/hooks/api/certificates/constants"; import { CrlReason } from "@app/hooks/api/certificates/enums"; @@ -35,7 +35,7 @@ type Props = { }; export const CertificateRevocationModal = ({ popUp, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync: revokeCertificate } = useRevokeCert(); const { @@ -49,12 +49,12 @@ export const CertificateRevocationModal = ({ popUp, handlePopUpToggle }: Props) const onFormSubmit = async ({ revocationReason }: FormData) => { try { - if (!currentWorkspace?.slug) return; + if (!currentProject?.slug) return; const { serialNumber } = popUp.revokeCertificate.data as { serialNumber: string }; await revokeCertificate({ - projectSlug: currentWorkspace.slug, + projectSlug: currentProject.slug, serialNumber, revocationReason }); diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx index d709458f0..8f547e573 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx @@ -22,7 +22,7 @@ import { SelectItem, Tooltip } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { CaStatus, useCreateCertTemplate, @@ -82,7 +82,7 @@ type Props = { }; export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: ca } = useGetCaById(caId); @@ -91,12 +91,12 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro ); const { data: cas } = useListWorkspaceCas({ - projectSlug: currentWorkspace?.slug ?? "", + projectId: currentProject?.id, status: CaStatus.ACTIVE }); const { data: collectionsData } = useListWorkspacePkiCollections({ - workspaceId: currentWorkspace?.id || "" + projectId: currentProject?.id || "" }); const { mutateAsync: createCertTemplate } = useCreateCertTemplate(); @@ -155,7 +155,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro keyUsages, extendedKeyUsages }: FormData) => { - if (!currentWorkspace?.id) { + if (!currentProject?.id) { return; } @@ -163,7 +163,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro if (certTemplate) { await updateCertTemplate({ id: certTemplate.id, - projectId: currentWorkspace.id, + projectId: currentProject.id, pkiCollectionId: collectionId, caId, name, @@ -184,7 +184,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro }); } else { await createCertTemplate({ - projectId: currentWorkspace.id, + projectId: currentProject.id, pkiCollectionId: collectionId, caId, name, diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesSection.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesSection.tsx index 0db7ba923..89f9780e8 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesSection.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesSection.tsx @@ -12,7 +12,7 @@ import { DeleteActionModal, IconButton } from "@app/components/v2"; import { ProjectPermissionPkiTemplateActions, ProjectPermissionSub, - useWorkspace + useProject } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteCertTemplate } from "@app/hooks/api"; @@ -33,18 +33,18 @@ export const CertificateTemplatesSection = ({ caId }: Props) => { "upgradePlan" ] as const); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync: deleteCertTemplate } = useDeleteCertTemplate(); const onRemoveCertificateTemplateSubmit = async (id: string) => { - if (!currentWorkspace?.id) { + if (!currentProject?.id) { return; } try { await deleteCertTemplate({ id, - projectId: currentWorkspace.id + projectId: currentProject.id }); createNotification({ diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx index 44ddbc8ba..696d07762 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx @@ -7,7 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2"; import { ProjectPermissionCertificateActions, ProjectPermissionSub, - useWorkspace + useProject } from "@app/context"; import { useDeleteCert } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -19,7 +19,7 @@ import { CertificateRevocationModal } from "./CertificateRevocationModal"; import { CertificatesTable } from "./CertificatesTable"; export const CertificatesSection = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync: deleteCert } = useDeleteCert(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -32,9 +32,9 @@ export const CertificatesSection = () => { const onRemoveCertificateSubmit = async (serialNumber: string) => { try { - if (!currentWorkspace?.slug) return; + if (!currentProject?.slug) return; - await deleteCert({ serialNumber, projectSlug: currentWorkspace.slug }); + await deleteCert({ serialNumber, projectSlug: currentProject.slug }); createNotification({ text: "Successfully deleted certificate", diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx index 98f83bc02..3cf35ebd6 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx @@ -33,7 +33,7 @@ import { import { ProjectPermissionCertificateActions, ProjectPermissionSub, - useWorkspace + useProject } from "@app/context"; import { useListWorkspaceCertificates } from "@app/hooks/api"; import { caSupportsCapability } from "@app/hooks/api/ca/constants"; @@ -62,15 +62,15 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(PER_PAGE_INIT); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data, isPending } = useListWorkspaceCertificates({ - projectSlug: currentWorkspace?.slug ?? "", + projectId: currentProject?.slug ?? "", offset: (page - 1) * perPage, limit: perPage }); // Fetch CA data to determine capabilities - const { data: caData } = useListCasByProjectId(currentWorkspace?.id ?? ""); + const { data: caData } = useListCasByProjectId(currentProject?.id ?? ""); // Create mapping from caId to CA type for capability checking const caCapabilityMap = useMemo(() => { diff --git a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx index 0261d55fd..c53df83be 100644 --- a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx @@ -16,7 +16,7 @@ import { Tooltip } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { useDeletePkiCollection, useGetPkiCollectionById } from "@app/hooks/api"; import { PkiItemType } from "@app/hooks/api/pkiCollections/constants"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -30,8 +30,8 @@ export const PkiCollectionPage = () => { from: ROUTE_PATHS.CertManager.PkiCollectionDetailsByIDPage.id }); const collectionId = params.collectionId as string; - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { data } = useGetPkiCollectionById(collectionId); const { mutateAsync: deletePkiCollection } = useDeletePkiCollection(); diff --git a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/components/AddPkiCollectionItemModal.tsx b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/components/AddPkiCollectionItemModal.tsx index f356f2190..3bcc3dc31 100644 --- a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/components/AddPkiCollectionItemModal.tsx +++ b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/components/AddPkiCollectionItemModal.tsx @@ -4,7 +4,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { CaStatus, useAddItemToPkiCollection, @@ -41,15 +41,15 @@ export const AddPkiCollectionItemModal = ({ popUp, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: cas } = useListWorkspaceCas({ - projectSlug: currentWorkspace?.slug || "", + projectId: currentProject?.id || "", status: CaStatus.ACTIVE }); const { data } = useListWorkspaceCertificates({ - projectSlug: currentWorkspace?.slug || "", + projectId: currentProject?.slug || "", offset: 0, limit: 25 }); diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx index 04db9861b..d7d44d4b5 100644 --- a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx @@ -19,7 +19,7 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionPkiSubscriberActions, ProjectPermissionSub, - useWorkspace + useProject } from "@app/context"; import { useDeletePkiSubscriber, useGetPkiSubscriber } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -29,8 +29,8 @@ import { PkiSubscriberCertificatesSection, PkiSubscriberDetailsSection } from ". const Page = () => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace.id; + const { currentProject } = useProject(); + const projectId = currentProject.id; const subscriberName = useParams({ from: ROUTE_PATHS.CertManager.PkiSubscriberDetailsByIDPage.id, select: (el) => el.subscriberName diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx index 62e7337bd..d3533d71d 100644 --- a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx @@ -27,8 +27,8 @@ import { import { ProjectPermissionPkiSubscriberActions, ProjectPermissionSub, - useProjectPermission, - useWorkspace + useProject, + useProjectPermission } from "@app/context"; import { useGetPkiSubscriberCertificates } from "@app/hooks/api"; import { caSupportsCapability } from "@app/hooks/api/ca/constants"; @@ -45,8 +45,8 @@ type Props = { const PER_PAGE_INIT = 25; export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen }: Props) => { - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace.id; + const { currentProject } = useProject(); + const projectId = currentProject.id; const { permission } = useProjectPermission(); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(PER_PAGE_INIT); @@ -64,7 +64,7 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen ); // Fetch CA data to determine capabilities - const { data: caData } = useListCasByProjectId(currentWorkspace.id); + const { data: caData } = useListCasByProjectId(currentProject.id); // Create mapping from caId to CA type for capability checking const caCapabilityMap = useMemo(() => { diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx index 1f15982a4..bb928dcc2 100644 --- a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx @@ -16,8 +16,8 @@ import { import { ProjectPermissionPkiSubscriberActions, ProjectPermissionSub, - useProjectPermission, - useWorkspace + useProject, + useProjectPermission } from "@app/context"; import { useTimedReset } from "@app/hooks"; import { @@ -44,8 +44,8 @@ type TCertificateDetails = { }; export const PkiSubscriberDetailsSection = ({ subscriberName, handlePopUpOpen }: Props) => { - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace.id; + const { currentProject } = useProject(); + const projectId = currentProject.id; const { permission } = useProjectPermission(); const [certificateDetails, setCertificateDetails] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx index f6deb97cb..932f3201b 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx @@ -22,7 +22,7 @@ import { TabPanel, Tabs } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { CaType, useCreatePkiSubscriber, @@ -158,8 +158,8 @@ const schema = z export type FormData = z.infer; export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace.id; + const { currentProject } = useProject(); + const projectId = currentProject.id; const { data: subscribers } = useListWorkspacePkiSubscribers(projectId); const { data: cas } = useListCasByProjectId(projectId); const [tabValue, setTabValue] = useState(FormTab.Configuration); diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx index f81636e49..f9680af9b 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx @@ -7,7 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2"; import { ProjectPermissionPkiSubscriberActions, ProjectPermissionSub, - useWorkspace + useProject } from "@app/context"; import { useDeletePkiSubscriber, useUpdatePkiSubscriber } from "@app/hooks/api"; import { PkiSubscriberStatus } from "@app/hooks/api/pkiSubscriber/types"; @@ -17,8 +17,8 @@ import { PkiSubscriberModal } from "./PkiSubscriberModal"; import { PkiSubscribersTable } from "./PkiSubscribersTable"; export const PkiSubscriberSection = () => { - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace.id; + const { currentProject } = useProject(); + const projectId = currentProject.id; const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber(); const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber(); @@ -55,7 +55,7 @@ export const PkiSubscriberSection = () => { status: PkiSubscriberStatus; }) => { try { - if (!currentWorkspace?.slug) return; + if (!currentProject?.slug) return; await updatePkiSubscriber({ subscriberName, projectId, status }); diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx index d649c43a7..d28c2d3a2 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx @@ -30,7 +30,7 @@ import { import { ProjectPermissionPkiSubscriberActions, ProjectPermissionSub, - useWorkspace + useProject } from "@app/context"; import { useListWorkspacePkiSubscribers } from "@app/hooks/api"; import { @@ -49,8 +49,8 @@ type Props = { export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); - const { data, isPending } = useListWorkspacePkiSubscribers(currentWorkspace?.id || ""); + const { currentProject } = useProject(); + const { data, isPending } = useListWorkspacePkiSubscribers(currentProject?.id || ""); return (
@@ -77,7 +77,7 @@ export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => { navigate({ to: "/projects/cert-management/$projectId/subscribers/$subscriberName", params: { - projectId: currentWorkspace.id, + projectId: currentProject.id, subscriberName: subscriber.name } }) diff --git a/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx b/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx index 4449e9a97..2ffe68813 100644 --- a/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx +++ b/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx @@ -42,8 +42,8 @@ import { import { ProjectPermissionPkiTemplateActions, ProjectPermissionSub, - useSubscription, - useWorkspace + useProject, + useSubscription } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteCertTemplateV2 } from "@app/hooks/api"; @@ -55,7 +55,7 @@ import { PkiTemplateForm } from "./components/PkiTemplateForm"; const PER_PAGE_INIT = 25; export const PkiTemplateListPage = () => { const { t } = useTranslation(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(PER_PAGE_INIT); const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ @@ -68,7 +68,7 @@ export const PkiTemplateListPage = () => { const { subscription } = useSubscription(); const { data, isPending } = useListCertificateTemplates({ - projectId: currentWorkspace.id, + projectId: currentProject.id, offset: (page - 1) * perPage, limit: perPage }); @@ -78,7 +78,7 @@ export const PkiTemplateListPage = () => { const onRemovePkiSubscriberSubmit = async () => { try { const pkiTemplate = await deleteCertTemplate.mutateAsync({ - projectId: currentWorkspace.id, + projectId: currentProject.id, templateName: popUp?.deleteTemplate?.data?.name }); diff --git a/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx b/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx index f52c83725..b8cdbfe1c 100644 --- a/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx +++ b/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx @@ -18,7 +18,7 @@ import { Input, Tooltip } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateCertTemplateV2, useListCasByProjectId, @@ -72,9 +72,9 @@ type Props = { }; export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); - const { data: cas, isPending: isCaLoading } = useListCasByProjectId(currentWorkspace.id); + const { data: cas, isPending: isCaLoading } = useListCasByProjectId(currentProject.id); const { mutateAsync: createCertTemplate } = useCreateCertTemplateV2(); const { mutateAsync: updateCertTemplate } = useUpdateCertTemplateV2(); @@ -124,7 +124,7 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => { extendedKeyUsages, ca }: FormData) => { - if (!currentWorkspace?.id) { + if (!currentProject?.id) { return; } @@ -132,7 +132,7 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => { if (certTemplate) { await updateCertTemplate({ templateName: certTemplate.name, - projectId: currentWorkspace.id, + projectId: currentProject.id, caName: ca.name, name, commonName, @@ -152,7 +152,7 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => { }); } else { await createCertTemplate({ - projectId: currentWorkspace.id, + projectId: currentProject.id, caName: ca.name, name, commonName, diff --git a/frontend/src/pages/cert-manager/layout.tsx b/frontend/src/pages/cert-manager/layout.tsx index 6b846909e..c8ec6a23b 100644 --- a/frontend/src/pages/cert-manager/layout.tsx +++ b/frontend/src/pages/cert-manager/layout.tsx @@ -1,9 +1,9 @@ import { createFileRoute } from "@tanstack/react-router"; import { BreadcrumbTypes } from "@app/components/v2"; -import { workspaceKeys } from "@app/hooks/api"; +import { projectKeys } from "@app/hooks/api"; +import { fetchProjectById } from "@app/hooks/api/projects/queries"; import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; -import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries"; import { PkiManagerLayout } from "@app/layouts/PkiManagerLayout"; import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect"; @@ -13,15 +13,15 @@ export const Route = createFileRoute( component: PkiManagerLayout, beforeLoad: async ({ params, context }) => { const project = await context.queryClient.ensureQueryData({ - queryKey: workspaceKeys.getWorkspaceById(params.projectId), - queryFn: () => fetchWorkspaceById(params.projectId) + queryKey: projectKeys.getProjectById(params.projectId), + queryFn: () => fetchProjectById(params.projectId) }); await context.queryClient.ensureQueryData({ queryKey: roleQueryKeys.getUserProjectPermissions({ - workspaceId: params.projectId + projectId: params.projectId }), - queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId }) + queryFn: () => fetchUserProjectPermissions({ projectId: params.projectId }) }); return { diff --git a/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx b/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx index 8c959c7b0..7abbee35f 100644 --- a/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx +++ b/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx @@ -13,7 +13,7 @@ import { ModalContent, TextArea } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateKmipClient, useUpdateKmipClient } from "@app/hooks/api/kmip"; import { KmipPermission, TKmipClient } from "@app/hooks/api/kmip/types"; @@ -60,8 +60,8 @@ type FormProps = Pick & { const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => { const createKmipClient = useCreateKmipClient(); const updateKmipClient = useUpdateKmipClient(); - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace.id; + const { currentProject } = useProject(); + const projectId = currentProject.id; const isUpdate = !!kmipClient; const { diff --git a/frontend/src/pages/kms/KmipPage/components/KmipClientTable.tsx b/frontend/src/pages/kms/KmipPage/components/KmipClientTable.tsx index bcc49b11a..5abad9f18 100644 --- a/frontend/src/pages/kms/KmipPage/components/KmipClientTable.tsx +++ b/frontend/src/pages/kms/KmipPage/components/KmipClientTable.tsx @@ -39,9 +39,9 @@ import { import { ProjectPermissionKmipActions, ProjectPermissionSub, + useProject, useProjectPermission, - useSubscription, - useWorkspace + useSubscription } from "@app/context"; import { getUserTablePreference, @@ -59,9 +59,9 @@ import { KmipClientCertificateModal } from "./KmipClientCertificateModal"; import { KmipClientModal } from "./KmipClientModal"; export const KmipClientTable = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); - const projectId = currentWorkspace?.id ?? ""; + const projectId = currentProject?.id ?? ""; const { offset, diff --git a/frontend/src/pages/kms/OverviewPage/components/CmekModal.tsx b/frontend/src/pages/kms/OverviewPage/components/CmekModal.tsx index c43b6c8d0..93c0102b1 100644 --- a/frontend/src/pages/kms/OverviewPage/components/CmekModal.tsx +++ b/frontend/src/pages/kms/OverviewPage/components/CmekModal.tsx @@ -14,7 +14,7 @@ import { SelectItem, TextArea } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { keyUsageDefaultOption, kmsKeyUsageOptions } from "@app/helpers/kms"; import { AllowedEncryptionKeyAlgorithms, @@ -49,8 +49,8 @@ type FormProps = Pick & { const CmekForm = ({ onComplete, cmek }: FormProps) => { const createCmek = useCreateCmek(); const updateCmek = useUpdateCmek(); - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace.id; + const { currentProject } = useProject(); + const projectId = currentProject.id; const isUpdate = !!cmek; const { diff --git a/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx b/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx index e1a2fe789..b1a7c409c 100644 --- a/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx +++ b/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx @@ -49,8 +49,8 @@ import { ProjectPermissionActions, ProjectPermissionCmekActions, ProjectPermissionSub, - useProjectPermission, - useWorkspace + useProject, + useProjectPermission } from "@app/context"; import { kmsKeyUsageOptions } from "@app/helpers/kms"; import { @@ -87,10 +87,10 @@ const getStatusBadgeProps = ( }; export const CmekTable = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { permission } = useProjectPermission(); - const projectId = currentWorkspace?.id ?? ""; + const projectId = currentProject?.id ?? ""; const { offset, diff --git a/frontend/src/pages/kms/layout.tsx b/frontend/src/pages/kms/layout.tsx index 60bc35ab7..f29a7627a 100644 --- a/frontend/src/pages/kms/layout.tsx +++ b/frontend/src/pages/kms/layout.tsx @@ -1,9 +1,9 @@ import { createFileRoute } from "@tanstack/react-router"; import { BreadcrumbTypes } from "@app/components/v2"; -import { workspaceKeys } from "@app/hooks/api"; +import { projectKeys } from "@app/hooks/api"; +import { fetchProjectById } from "@app/hooks/api/projects/queries"; import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; -import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries"; import { KmsLayout } from "@app/layouts/KmsLayout"; import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect"; @@ -13,15 +13,15 @@ export const Route = createFileRoute( component: KmsLayout, beforeLoad: async ({ params, context }) => { const project = await context.queryClient.ensureQueryData({ - queryKey: workspaceKeys.getWorkspaceById(params.projectId), - queryFn: () => fetchWorkspaceById(params.projectId) + queryKey: projectKeys.getProjectById(params.projectId), + queryFn: () => fetchProjectById(params.projectId) }); await context.queryClient.ensureQueryData({ queryKey: roleQueryKeys.getUserProjectPermissions({ - workspaceId: params.projectId + projectId: params.projectId }), - queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId }) + queryFn: () => fetchUserProjectPermissions({ projectId: params.projectId }) }); return { diff --git a/frontend/src/pages/middlewares/restrict-login-signup.tsx b/frontend/src/pages/middlewares/restrict-login-signup.tsx index c3d58f248..294bece26 100644 --- a/frontend/src/pages/middlewares/restrict-login-signup.tsx +++ b/frontend/src/pages/middlewares/restrict-login-signup.tsx @@ -15,7 +15,8 @@ import { setAuthToken } from "@app/hooks/api/reactQuery"; const QueryParamsSchema = z.object({ callback_port: z.coerce.number().optional().catch(undefined), - force: z.boolean().optional() + force: z.boolean().optional(), + org_id: z.string().optional().catch(undefined) }); export const AuthConsentWrapper = () => { @@ -102,6 +103,12 @@ export const Route = createFileRoute("/_restrict-login-signup")({ return; } + if (search.org_id) { + if (location.pathname.endsWith("select-organization")) return; + + throw redirect({ to: "/login/select-organization", search: { org_id: search.org_id } }); + } + if (!data.organizationId) { if ( location.pathname.endsWith("select-organization") || diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx index ee0cddc5c..31f7e56eb 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx @@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faQuestionCircle, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -25,6 +26,7 @@ import { OrgPermissionMachineIdentityAuthTemplateActions, OrgPermissionSubjects } from "@app/context/OrgPermissionContext/types"; +import { getObjectFromSeconds } from "@app/helpers/datetime"; import { MachineIdentityAuthMethod, useAddIdentityLdapAuth, @@ -35,6 +37,8 @@ import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { useGetAvailableTemplates } from "@app/hooks/api/identityAuthTemplates/queries"; import { UsePopUpState } from "@app/hooks/usePopUp"; +import { LockoutTab } from "./lockout/LockoutTab"; +import { superRefineLockout } from "./lockout/super-refine"; import { IdentityFormTab } from "./types"; const schema = z @@ -74,9 +78,28 @@ const schema = z ipAddress: z.string().max(50) }) ) - .min(1) + .min(1), + + lockoutEnabled: z.boolean().default(true), + lockoutThreshold: z + .string() + .refine( + (value) => Number(value) <= 30 && Number(value) >= 1, + "Lockout threshold must be between 1 and 30" + ), + lockoutDurationValue: z.string(), + lockoutDurationUnit: z.enum(["s", "m", "h", "d"], { + invalid_type_error: "Please select a valid time unit" + }), + lockoutCounterResetValue: z.string(), + lockoutCounterResetUnit: z.enum(["s", "m", "h"], { + invalid_type_error: "Please select a valid time unit" + }) }) + .required() .superRefine((data, ctx) => { + superRefineLockout(data, ctx); + // Validation based on scope if (data.scope === "template") { if (!data.templateId) { @@ -178,12 +201,25 @@ export const IdentityLdapAuthForm = ({ accessTokenTTL: "2592000", accessTokenMaxTTL: "2592000", accessTokenNumUsesLimit: "0", - accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + lockoutEnabled: true, + lockoutThreshold: "3", + lockoutDurationValue: "5", + lockoutDurationUnit: "m", + lockoutCounterResetValue: "30", + lockoutCounterResetUnit: "s" } }); const scope = watch("scope"); + const lockoutEnabledWatch = watch("lockoutEnabled"); + const lockoutThresholdWatch = watch("lockoutThreshold"); + const lockoutDurationValueWatch = watch("lockoutDurationValue"); + const lockoutDurationUnitWatch = watch("lockoutDurationUnit"); + const lockoutCounterResetValueWatch = watch("lockoutCounterResetValue"); + const lockoutCounterResetUnitWatch = watch("lockoutCounterResetUnit"); + const { fields: accessTokenTrustedIpsFields, append: appendAccessTokenTrustedIp, @@ -210,6 +246,9 @@ export const IdentityLdapAuthForm = ({ if (data) { const detectedScope = determineScope(data); + const lockoutDurationObj = getObjectFromSeconds(data.lockoutDurationSeconds); + const lockoutCounterResetObj = getObjectFromSeconds(data.lockoutCounterResetSeconds); + reset({ scope: detectedScope, templateId: data.templateId || "", @@ -229,7 +268,13 @@ export const IdentityLdapAuthForm = ({ ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` }; } - ) + ), + lockoutEnabled: data.lockoutEnabled, + lockoutThreshold: String(data.lockoutThreshold), + lockoutDurationValue: String(lockoutDurationObj.value), + lockoutDurationUnit: lockoutDurationObj.unit as "s" | "m" | "h" | "d", + lockoutCounterResetValue: String(lockoutCounterResetObj.value), + lockoutCounterResetUnit: lockoutCounterResetObj.unit as "s" | "m" | "h" }); return; } @@ -247,7 +292,13 @@ export const IdentityLdapAuthForm = ({ accessTokenTTL: "2592000", accessTokenMaxTTL: "2592000", accessTokenNumUsesLimit: "0", - accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + lockoutEnabled: true, + lockoutThreshold: "3", + lockoutDurationValue: "5", + lockoutDurationUnit: "m", + lockoutCounterResetValue: "30", + lockoutCounterResetUnit: "s" }); }, [data, reset]); @@ -275,9 +326,19 @@ export const IdentityLdapAuthForm = ({ accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, - accessTokenTrustedIps + accessTokenTrustedIps, + lockoutEnabled, + lockoutThreshold, + lockoutDurationValue, + lockoutDurationUnit, + lockoutCounterResetValue, + lockoutCounterResetUnit } = formData; + const lockoutDurationSeconds = ms(`${lockoutDurationValue}${lockoutDurationUnit}`) / 1000; + const lockoutCounterResetSeconds = + ms(`${lockoutCounterResetValue}${lockoutCounterResetUnit}`) / 1000; + const basePayload = { organizationId: orgId, identityId, @@ -287,7 +348,11 @@ export const IdentityLdapAuthForm = ({ accessTokenTTL: Number(accessTokenTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps + accessTokenTrustedIps, + lockoutEnabled, + lockoutThreshold: Number(lockoutThreshold), + lockoutDurationSeconds, + lockoutCounterResetSeconds }; // Add scope-specific fields @@ -327,7 +392,10 @@ export const IdentityLdapAuthForm = ({ return (
{ - setTabValue( + const firstErrorField = Object.keys(fields)[0]; + let tab = IdentityFormTab.Advanced; + + if ( [ "scope", "templateId", @@ -340,15 +408,29 @@ export const IdentityLdapAuthForm = ({ "allowedFields", "accessTokenMaxTTL", "accessTokenNumUsesLimit" - ].includes(Object.keys(fields)[0]) - ? IdentityFormTab.Configuration - : IdentityFormTab.Advanced - ); + ].includes(firstErrorField) + ) { + tab = IdentityFormTab.Configuration; + } else if ( + [ + "lockoutEnabled", + "lockoutThreshold", + "lockoutDurationValue", + "lockoutDurationUnit", + "lockoutCounterResetValue", + "lockoutCounterResetUnit" + ].includes(firstErrorField) + ) { + tab = IdentityFormTab.Lockout; + } + + setTabValue(tab); })} > setTabValue(value as IdentityFormTab)}> Configuration + Lockout Advanced @@ -691,6 +773,15 @@ export const IdentityLdapAuthForm = ({ )} /> + { - const { - lockoutDurationValue, - lockoutCounterResetValue, - lockoutDurationUnit, - lockoutCounterResetUnit, - lockoutEnabled - } = data; - - if (!lockoutEnabled) return; - - let isAnyParseError = false; - - const parsedLockoutDuration = parseInt(lockoutDurationValue, 10); - if (Number.isNaN(parsedLockoutDuration)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Lockout duration must be a number", - path: ["lockoutDurationValue"] - }); - isAnyParseError = true; - } - - const parsedLockoutCounterReset = parseInt(lockoutCounterResetValue, 10); - if (Number.isNaN(parsedLockoutCounterReset)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Lockout counter reset must be a number", - path: ["lockoutCounterResetValue"] - }); - isAnyParseError = true; - } - - if (isAnyParseError) return; - - const lockoutDurationInSeconds = ms(`${parsedLockoutDuration}${lockoutDurationUnit}`) / 1000; - const lockoutCounterResetInSeconds = - ms(`${parsedLockoutCounterReset}${lockoutCounterResetUnit}`) / 1000; - - if (lockoutDurationInSeconds > 86400 || lockoutDurationInSeconds < 30) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Lockout duration must be between 30 seconds and 1 day", - path: ["lockoutDurationValue"] - }); - } - - if (lockoutCounterResetInSeconds > 3600 || lockoutCounterResetInSeconds < 5) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Lockout counter reset must be between 5 seconds and 1 hour", - path: ["lockoutCounterResetValue"] - }); - } - }); + .superRefine(superRefineLockout); export type FormData = z.infer; @@ -432,187 +377,15 @@ export const IdentityUniversalAuthForm = ({ )} /> - -
- { - return ( - - - Lockout - - - ); - }} - /> -
- { - return ( - - - - ); - }} - /> -
- { - return ( - - - - ); - }} - /> - ( - - - - )} - /> -
-
- { - return ( - - - - ); - }} - /> - ( - - - - )} - /> -
-
-
-
- + {clientSecretTrustedIpsFields.map(({ id }, index) => (
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/lockout/LockoutTab.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/lockout/LockoutTab.tsx new file mode 100644 index 000000000..3d386a20f --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/lockout/LockoutTab.tsx @@ -0,0 +1,205 @@ +import { Control, Controller } from "react-hook-form"; + +import { FormControl, Input, Select, SelectItem, Switch, TabPanel } from "@app/components/v2"; + +import { IdentityFormTab } from "../types"; + +export const LockoutTab = ({ + control, + lockoutEnabled, + lockoutThreshold, + lockoutDurationValue, + lockoutDurationUnit, + lockoutCounterResetValue, + lockoutCounterResetUnit +}: { + control: Control; + lockoutEnabled: boolean; + lockoutThreshold: string; + lockoutDurationValue: string; + lockoutDurationUnit: "s" | "m" | "h" | "d"; + lockoutCounterResetValue: string; + lockoutCounterResetUnit: "s" | "m" | "h"; +}) => { + return ( + +
+ { + return ( + + + Lockout + + + ); + }} + /> +
+ { + return ( + + + + ); + }} + /> +
+ { + return ( + + + + ); + }} + /> + ( + + + + )} + /> +
+
+ { + return ( + + + + ); + }} + /> + ( + + + + )} + /> +
+
+
+
+ ); +}; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/lockout/super-refine.ts b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/lockout/super-refine.ts new file mode 100644 index 000000000..a597f810f --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/lockout/super-refine.ts @@ -0,0 +1,67 @@ +import ms from "ms"; +import { z } from "zod"; + +export function superRefineLockout( + data: { + lockoutDurationValue: string; + lockoutCounterResetValue: string; + lockoutDurationUnit: "s" | "m" | "h" | "d"; + lockoutCounterResetUnit: "s" | "m" | "h"; + lockoutEnabled: boolean; + }, + ctx: z.RefinementCtx +) { + const { + lockoutDurationValue, + lockoutCounterResetValue, + lockoutDurationUnit, + lockoutCounterResetUnit, + lockoutEnabled + } = data; + + if (lockoutEnabled) { + let isAnyParseError = false; + + const parsedLockoutDuration = parseInt(lockoutDurationValue, 10); + if (Number.isNaN(parsedLockoutDuration)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Lockout duration must be a number", + path: ["lockoutDurationValue"] + }); + isAnyParseError = true; + } + + const parsedLockoutCounterReset = parseInt(lockoutCounterResetValue, 10); + if (Number.isNaN(parsedLockoutCounterReset)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Lockout counter reset must be a number", + path: ["lockoutCounterResetValue"] + }); + isAnyParseError = true; + } + + if (!isAnyParseError) { + const lockoutDurationInSeconds = ms(`${parsedLockoutDuration}${lockoutDurationUnit}`) / 1000; + const lockoutCounterResetInSeconds = + ms(`${parsedLockoutCounterReset}${lockoutCounterResetUnit}`) / 1000; + + if (lockoutDurationInSeconds > 86400 || lockoutDurationInSeconds < 30) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Lockout duration must be between 30 seconds and 1 day", + path: ["lockoutDurationValue"] + }); + } + + if (lockoutCounterResetInSeconds > 3600 || lockoutCounterResetInSeconds < 5) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Lockout counter reset must be between 5 seconds and 1 hour", + path: ["lockoutCounterResetValue"] + }); + } + } + } +} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx index 7f751fc7d..c1c9f0b0a 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx @@ -21,10 +21,10 @@ import { useAddUsersToOrg, useFetchServerStatus, useGetOrgRoles, - useGetUserWorkspaces + useGetUserProjects } from "@app/hooks/api"; +import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; -import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { OrgInviteLink } from "./OrgInviteLink"; @@ -76,7 +76,7 @@ export const AddOrgMemberModal = ({ const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? ""); const { data: serverDetails } = useFetchServerStatus(); const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg(); - const { data: projects, isPending: isProjectsLoading } = useGetUserWorkspaces({ + const { data: projects, isPending: isProjectsLoading } = useGetUserProjects({ includeRoles: true }); diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/AppConnectionsPage.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/AppConnectionsPage.tsx index ff3f31c9e..80aefe3f2 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/AppConnectionsPage.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/AppConnectionsPage.tsx @@ -1,24 +1,17 @@ import { Helmet } from "react-helmet"; -import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { OrgPermissionCan } from "@app/components/permissions"; -import { Button, PageHeader } from "@app/components/v2"; +import { PageHeader } from "@app/components/v2"; import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/context/OrgPermissionContext/types"; import { withPermission } from "@app/hoc"; -import { usePopUp } from "@app/hooks"; -import { - AddAppConnectionModal, - AppConnectionsTable -} from "@app/pages/organization/AppConnections/AppConnectionsPage/components"; +import { AppConnectionsTable } from "@app/pages/organization/AppConnections/AppConnectionsPage/components"; export const AppConnectionsPage = withPermission( () => { - const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addConnection"] as const); - return (
@@ -30,54 +23,18 @@ export const AppConnectionsPage = withPermission(
- App Connections - -
- - Docs - -
-
- - {(isAllowed) => ( - - )} - -
- } - description="Create and configure connections with third-party apps for re-use across Infisical projects" + title="App Connections" + description="Manage organization App Connections" /> -
- - handlePopUpToggle("addConnection", isOpen)} - /> +
+
+ + + App connections can also be created and managed independently in projects now. + +
+
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AddAppConnectionModal.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AddAppConnectionModal.tsx index c74985a3f..c52f89dc9 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AddAppConnectionModal.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AddAppConnectionModal.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { Modal, ModalContent } from "@app/components/v2"; import { TAppConnection } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { ProjectType } from "@app/hooks/api/projects/types"; import { AppConnectionForm } from "./AppConnectionForm"; import { AppConnectionsSelect } from "./AppConnectionList"; @@ -10,29 +11,44 @@ import { AppConnectionsSelect } from "./AppConnectionList"; type Props = { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; + projectId?: string; + projectType?: ProjectType; + app?: AppConnection; + onComplete?: (appConnection: TAppConnection) => void; }; type ContentProps = { onComplete: (appConnection: TAppConnection) => void; + projectId?: string; + projectType?: ProjectType; + app?: AppConnection; }; -const Content = ({ onComplete }: ContentProps) => { +const Content = ({ onComplete, projectId, projectType, app }: ContentProps) => { const [selectedApp, setSelectedApp] = useState(null); - if (selectedApp) { + if (app ?? selectedApp) { return ( setSelectedApp(null)} - app={selectedApp} + onBack={app ? undefined : () => setSelectedApp(null)} + app={(app ?? selectedApp)!} + projectId={projectId} /> ); } - return ; + return ; }; -export const AddAppConnectionModal = ({ isOpen, onOpenChange }: Props) => { +export const AddAppConnectionModal = ({ + isOpen, + onOpenChange, + projectId, + projectType, + app, + onComplete +}: Props) => { return ( { title="Add Connection" subTitle="Select a third-party app to connect to." > - onOpenChange(false)} /> + { + if (onComplete) onComplete(appConnection); + onOpenChange(false); + }} + /> ); diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index f1826e5ae..f82e4107d 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -52,12 +52,15 @@ type FormProps = { onComplete: (appConnection: TAppConnection) => void; } & ({ appConnection: TAppConnection } | { app: AppConnection }); -type CreateFormProps = FormProps & { app: AppConnection }; +type CreateFormProps = FormProps & { + app: AppConnection; + projectId?: string; +}; type UpdateFormProps = FormProps & { appConnection: TAppConnection; }; -const CreateForm = ({ app, onComplete }: CreateFormProps) => { +const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => { const createAppConnection = useCreateAppConnection(); const { name: appName } = APP_CONNECTION_MAP[app]; @@ -68,7 +71,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { > ) => { try { - const connection = await createAppConnection.mutateAsync(formData); + const connection = await createAppConnection.mutateAsync({ + ...formData, + projectId + }); createNotification({ text: `Successfully added ${appName} Connection`, type: "success" @@ -88,15 +94,15 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { case AppConnection.AWS: return ; case AppConnection.GitHub: - return ; + return ; case AppConnection.GitHubRadar: - return ; + return ; case AppConnection.GCP: return ; case AppConnection.AzureKeyVault: - return ; + return ; case AppConnection.AzureAppConfiguration: - return ; + return ; case AppConnection.AzureADCS: return ; case AppConnection.Databricks: @@ -118,9 +124,9 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { case AppConnection.Camunda: return ; case AppConnection.AzureClientSecrets: - return ; + return ; case AppConnection.AzureDevOps: - return ; + return ; case AppConnection.Windmill: return ; case AppConnection.Auth0: @@ -142,7 +148,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { case AppConnection.Flyio: return ; case AppConnection.GitLab: - return ; + return ; case AppConnection.Cloudflare: return ; case AppConnection.Bitbucket: @@ -200,16 +206,33 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { case AppConnection.AWS: return ; case AppConnection.GitHub: - return ; + return ( + + ); case AppConnection.GitHubRadar: - return ; + return ( + + ); case AppConnection.GCP: return ; case AppConnection.AzureKeyVault: - return ; + return ( + + ); case AppConnection.AzureAppConfiguration: return ( - + ); case AppConnection.AzureADCS: return ; @@ -232,9 +255,21 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { case AppConnection.Camunda: return ; case AppConnection.AzureClientSecrets: - return ; + return ( + + ); case AppConnection.AzureDevOps: - return ; + return ( + + ); case AppConnection.Windmill: return ; case AppConnection.Auth0: @@ -256,7 +291,13 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { case AppConnection.Flyio: return ; case AppConnection.GitLab: - return ; + return ( + + ); case AppConnection.Cloudflare: return ; case AppConnection.Bitbucket: @@ -278,12 +319,12 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { } }; -type Props = { onBack?: () => void } & Pick & +type Props = { onBack?: () => void; projectId?: string } & Pick & ( | { app: AppConnection; appConnection?: undefined } | { app?: undefined; appConnection: TAppConnection } ); -export const AppConnectionForm = ({ onBack, ...props }: Props) => { +export const AppConnectionForm = ({ onBack, projectId, ...props }: Props) => { const { app, appConnection } = props; return ( @@ -296,7 +337,7 @@ export const AppConnectionForm = ({ onBack, ...props }: Props) => { {appConnection ? ( ) : ( - + )}
); diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx index 433abf076..3772478f0 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx @@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2"; -import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { + APP_CONNECTION_MAP, + getAppConnectionMethodDetails, + useGetAppConnectionOauthReturnUrl +} from "@app/helpers/appConnections"; import { isInfisicalCloud } from "@app/helpers/platform"; import { AzureAppConfigurationConnectionMethod, @@ -15,6 +19,7 @@ import { } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { AzureAppConfigurationFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types"; import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields @@ -25,6 +30,7 @@ type ClientSecretForm = z.infer; type Props = { appConnection?: TAzureAppConfigurationConnection; onSubmit: (formData: ClientSecretForm) => Promise; + projectId: string | undefined | null; }; const baseSchema = genericAppConnectionFieldsSchema.extend({ @@ -96,7 +102,11 @@ const getDefaultValues = (appConnection?: TAzureAppConfigurationConnection): Par return base; }; -export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }: Props) => { +export const AzureAppConfigurationConnectionForm = ({ + appConnection, + onSubmit, + projectId +}: Props) => { const isUpdate = Boolean(appConnection); const [isRedirecting, setIsRedirecting] = useState(false); @@ -110,6 +120,8 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }: defaultValues: getDefaultValues(appConnection) }); + const returnUrl = useGetAppConnectionOauthReturnUrl(); + const { handleSubmit, control, @@ -128,7 +140,12 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }: localStorage.setItem("latestCSRFToken", state); localStorage.setItem( "azureAppConfigurationConnectionFormData", - JSON.stringify({ ...formData, connectionId: appConnection?.id }) + JSON.stringify({ + ...formData, + connectionId: appConnection?.id, + projectId, + returnUrl + } as AzureAppConfigurationFormData) ); window.location.assign( `https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-app-configuration` diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx index 6f70f789e..dcfdbaa08 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx @@ -7,7 +7,11 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2"; -import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { + APP_CONNECTION_MAP, + getAppConnectionMethodDetails, + useGetAppConnectionOauthReturnUrl +} from "@app/helpers/appConnections"; import { isInfisicalCloud } from "@app/helpers/platform"; import { AzureClientSecretsConnectionMethod, @@ -16,6 +20,7 @@ import { } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { AzureClientSecretsFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types"; import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields @@ -26,6 +31,7 @@ type ClientSecretForm = z.infer; type Props = { appConnection?: TAzureClientSecretsConnection; onSubmit: (formData: ClientSecretForm) => Promise; + projectId: string | undefined | null; }; const baseSchema = genericAppConnectionFieldsSchema.extend({ @@ -97,7 +103,7 @@ const getDefaultValues = (appConnection?: TAzureClientSecretsConnection): Partia return base; }; -export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Props) => { +export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => { const isUpdate = Boolean(appConnection); const [isRedirecting, setIsRedirecting] = useState(false); @@ -111,6 +117,8 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr defaultValues: getDefaultValues(appConnection) }); + const returnUrl = useGetAppConnectionOauthReturnUrl(); + const { handleSubmit, control, @@ -129,7 +137,12 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr localStorage.setItem("latestCSRFToken", state); localStorage.setItem( "azureClientSecretsConnectionFormData", - JSON.stringify({ ...formData, connectionId: appConnection?.id }) + JSON.stringify({ + ...formData, + connectionId: appConnection?.id, + projectId, + returnUrl + } as AzureClientSecretsFormData) ); window.location.assign( `https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://graph.microsoft.com/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets` diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureDevOpsConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureDevOpsConnectionForm.tsx index c4511a0da..7d2a4ce7d 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureDevOpsConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureDevOpsConnectionForm.tsx @@ -7,7 +7,11 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2"; -import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { + APP_CONNECTION_MAP, + getAppConnectionMethodDetails, + useGetAppConnectionOauthReturnUrl +} from "@app/helpers/appConnections"; import { isInfisicalCloud } from "@app/helpers/platform"; import { AzureDevOpsConnectionMethod, @@ -16,6 +20,7 @@ import { } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { AzureDevOpsFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types"; import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields @@ -65,6 +70,7 @@ type OnSubmitForm = z.infer | z.infer Promise; + projectId: string | undefined | null; }; const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial => { @@ -132,7 +138,7 @@ const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial { +export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => { const isUpdate = Boolean(appConnection); const [isRedirecting, setIsRedirecting] = useState(false); @@ -146,6 +152,8 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) => defaultValues: getDefaultValues(appConnection) }); + const returnUrl = useGetAppConnectionOauthReturnUrl(); + const { handleSubmit, control, @@ -164,7 +172,12 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) => localStorage.setItem("latestCSRFToken", state); localStorage.setItem( "azureDevOpsConnectionFormData", - JSON.stringify({ ...formData, connectionId: appConnection?.id }) + JSON.stringify({ + ...formData, + connectionId: appConnection?.id, + projectId, + returnUrl + } as AzureDevOpsFormData) ); window.location.assign( diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx index ed99fda01..8c0f00acf 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx @@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2"; -import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { + APP_CONNECTION_MAP, + getAppConnectionMethodDetails, + useGetAppConnectionOauthReturnUrl +} from "@app/helpers/appConnections"; import { isInfisicalCloud } from "@app/helpers/platform"; import { useGetAppConnectionOption } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; @@ -15,6 +19,7 @@ import { TAzureKeyVaultConnection } from "@app/hooks/api/appConnections/types/azure-key-vault-connection"; +import { AzureKeyVaultFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types"; import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields @@ -25,6 +30,7 @@ type ClientSecretForm = z.infer; type Props = { appConnection?: TAzureKeyVaultConnection; onSubmit: (formData: ClientSecretForm) => Promise; + projectId: string | undefined | null; }; const baseSchema = genericAppConnectionFieldsSchema.extend({ @@ -96,7 +102,7 @@ const getDefaultValues = (appConnection?: TAzureKeyVaultConnection): Partial { +export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => { const isUpdate = Boolean(appConnection); const [isRedirecting, setIsRedirecting] = useState(false); @@ -110,6 +116,8 @@ export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props) defaultValues: getDefaultValues(appConnection) }); + const returnUrl = useGetAppConnectionOauthReturnUrl(); + const { handleSubmit, control, @@ -129,7 +137,12 @@ export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props) localStorage.setItem("latestCSRFToken", state); localStorage.setItem( "azureKeyVaultConnectionFormData", - JSON.stringify({ ...formData, connectionId: appConnection?.id }) + JSON.stringify({ + ...formData, + connectionId: appConnection?.id, + projectId, + returnUrl + } as AzureKeyVaultFormData) ); window.location.assign( `https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://vault.azure.net/.default%20openid%20offline_access&state=${state}<:>azure-key-vault` diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx index 5c9a5bcd3..c69f662c6 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx @@ -25,7 +25,11 @@ import { OrgGatewayPermissionActions, OrgPermissionSubjects } from "@app/context/OrgPermissionContext/types"; -import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { + APP_CONNECTION_MAP, + getAppConnectionMethodDetails, + useGetAppConnectionOauthReturnUrl +} from "@app/helpers/appConnections"; import { isInfisicalCloud } from "@app/helpers/platform"; import { gatewaysQueryKeys } from "@app/hooks/api"; import { @@ -35,6 +39,7 @@ import { } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { GithubFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types"; import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields @@ -42,6 +47,7 @@ import { type Props = { appConnection?: TGitHubConnection; + projectId: string | undefined | null; }; const formSchema = genericAppConnectionFieldsSchema.extend({ @@ -63,7 +69,7 @@ const formSchema = genericAppConnectionFieldsSchema.extend({ type FormData = z.infer; -export const GitHubConnectionForm = ({ appConnection }: Props) => { +export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => { const isUpdate = Boolean(appConnection); const [isRedirecting, setIsRedirecting] = useState(false); @@ -98,13 +104,21 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => { const selectedMethod = watch("method"); const instanceType = watch("credentials.instanceType"); + const returnUrl = useGetAppConnectionOauthReturnUrl(); + const onSubmit = (formData: FormData) => { setIsRedirecting(true); const state = crypto.randomBytes(16).toString("hex"); localStorage.setItem("latestCSRFToken", state); localStorage.setItem( "githubConnectionFormData", - JSON.stringify({ ...formData, connectionId: appConnection?.id }) + JSON.stringify({ + ...formData, + credentials: formData.credentials as TGitHubConnection["credentials"], + connectionId: appConnection?.id, + projectId, + returnUrl + } as GithubFormData) ); const githubHost = diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubRadarConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubRadarConnectionForm.tsx index f9c39a502..446eeb4cc 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubRadarConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubRadarConnectionForm.tsx @@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2"; -import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { + APP_CONNECTION_MAP, + getAppConnectionMethodDetails, + useGetAppConnectionOauthReturnUrl +} from "@app/helpers/appConnections"; import { isInfisicalCloud } from "@app/helpers/platform"; import { GitHubRadarConnectionMethod, @@ -15,6 +19,7 @@ import { } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { GithubRadarFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types"; import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields @@ -22,6 +27,7 @@ import { type Props = { appConnection?: TGitHubRadarConnection; + projectId: string | undefined | null; }; const formSchema = genericAppConnectionFieldsSchema.extend({ @@ -31,7 +37,7 @@ const formSchema = genericAppConnectionFieldsSchema.extend({ type FormData = z.infer; -export const GitHubRadarConnectionForm = ({ appConnection }: Props) => { +export const GitHubRadarConnectionForm = ({ appConnection, projectId }: Props) => { const isUpdate = Boolean(appConnection); const [isRedirecting, setIsRedirecting] = useState(false); @@ -48,6 +54,8 @@ export const GitHubRadarConnectionForm = ({ appConnection }: Props) => { } }); + const returnUrl = useGetAppConnectionOauthReturnUrl(); + const { handleSubmit, control, @@ -63,7 +71,12 @@ export const GitHubRadarConnectionForm = ({ appConnection }: Props) => { localStorage.setItem("latestCSRFToken", state); localStorage.setItem( "githubRadarConnectionFormData", - JSON.stringify({ ...formData, connectionId: appConnection?.id }) + JSON.stringify({ + ...formData, + connectionId: appConnection?.id, + projectId, + returnUrl + } as GithubRadarFormData) ); switch (formData.method) { diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitLabConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitLabConnectionForm.tsx index 8cbc2c54c..ed4e022c4 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitLabConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitLabConnectionForm.tsx @@ -16,7 +16,11 @@ import { Select, SelectItem } from "@app/components/v2"; -import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { + APP_CONNECTION_MAP, + getAppConnectionMethodDetails, + useGetAppConnectionOauthReturnUrl +} from "@app/helpers/appConnections"; import { isInfisicalCloud } from "@app/helpers/platform"; import { useGetAppConnectionOption } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; @@ -26,6 +30,7 @@ import { TGitLabConnection } from "@app/hooks/api/appConnections/types/gitlab-connection"; +import { GitLabFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types"; import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields @@ -34,6 +39,7 @@ import { type Props = { appConnection?: TGitLabConnection; onSubmit: (formData: FormData) => Promise; + projectId: string | undefined | null; }; const formSchema = z.discriminatedUnion("method", [ @@ -72,7 +78,7 @@ const formSchema = z.discriminatedUnion("method", [ type FormData = z.infer; -export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Props) => { +export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit, projectId }: Props) => { const isUpdate = Boolean(appConnection); const [isRedirecting, setIsRedirecting] = useState(false); @@ -98,6 +104,8 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr } as FormData)) }); + const returnUrl = useGetAppConnectionOauthReturnUrl(); + const { handleSubmit, control, @@ -132,8 +140,10 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr JSON.stringify({ ...formData, connectionId: appConnection?.id, - isUpdate - }) + isUpdate, + projectId, + returnUrl + } as GitLabFormData) ); // Redirect to Gitlab OAuth diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx index 2087ebf63..0137d5a9e 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx @@ -9,14 +9,16 @@ import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useAppConnectionOptions } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { ProjectType } from "@app/hooks/api/projects/types"; type Props = { onSelect: (app: AppConnection) => void; + projectType?: ProjectType; }; -export const AppConnectionsSelect = ({ onSelect }: Props) => { +export const AppConnectionsSelect = ({ onSelect, projectType }: Props) => { const { subscription } = useSubscription(); - const { isPending, data: appConnectionOptions } = useAppConnectionOptions(); + const { isPending, data: appConnectionOptions } = useAppConnectionOptions(projectType); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionRow.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionRow.tsx index fb7f880c4..941a90db4 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionRow.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionRow.tsx @@ -1,19 +1,23 @@ import { useCallback } from "react"; +import { subject } from "@casl/ability"; import { faAsterisk, + faBuilding, faCheck, faCopy, faEdit, faEllipsisV, faInfoCircle, faServer, + faTable, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Link } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; -import { OrgPermissionCan } from "@app/components/permissions"; +import { VariablePermissionCan } from "@app/components/permissions"; import { Badge, DropdownMenu, @@ -25,9 +29,11 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context"; import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types"; +import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { getProjectBaseURL } from "@app/helpers/project"; import { useToggle } from "@app/hooks"; import { TAppConnection } from "@app/hooks/api/appConnections"; @@ -36,15 +42,18 @@ type Props = { onDelete: (appConnection: TAppConnection) => void; onEditCredentials: (appConnection: TAppConnection) => void; onEditDetails: (appConnection: TAppConnection) => void; + isProjectView: boolean; }; export const AppConnectionRow = ({ appConnection, onDelete, onEditCredentials, - onEditDetails + onEditDetails, + isProjectView }: Props) => { - const { id, name, method, app, description, isPlatformManagedCredentials } = appConnection; + const { id, name, method, app, description, isPlatformManagedCredentials, project } = + appConnection; const [isIdCopied, setIsIdCopied] = useToggle(false); @@ -111,7 +120,38 @@ export const AppConnectionRow = ({ {methodDetails.name}

- + {!isProjectView && ( + + {project ? ( + +

+ + {project.name} +

+ + ) : ( +

+ + Organization +

+ )} + + )}
{isPlatformManagedCredentials && ( @@ -143,48 +183,91 @@ export const AppConnectionRow = ({ > Copy Connection ID - - {(isAllowed: boolean) => ( - } - onClick={() => onEditDetails(appConnection)} + {(isProjectView || !project) && ( + <> + - Edit Details - - )} - - - {(isAllowed: boolean) => ( - } - onClick={() => onEditCredentials(appConnection)} + {(isAllowed: boolean) => ( + } + onClick={() => onEditDetails(appConnection)} + > + Edit Details + + )} + + - {isPlatformManagedCredentials ? "View" : "Edit"} Credentials - - )} - - - {(isAllowed: boolean) => ( - } - onClick={() => onDelete(appConnection)} + {(isAllowed: boolean) => ( + } + onClick={() => onEditCredentials(appConnection)} + > + {isPlatformManagedCredentials ? "View" : "Edit"} Credentials + + )} + + - Delete Connection - - )} - + {(isAllowed: boolean) => ( + } + onClick={() => onDelete(appConnection)} + > + Delete Connection + + )} + + + )} diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionsTable.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionsTable.tsx index bc448663a..91ad35d4a 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionsTable.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionsTable.tsx @@ -2,16 +2,21 @@ import { useMemo, useState } from "react"; import { faArrowDown, faArrowUp, + faArrowUpRightFromSquare, + faBookOpen, faCheckCircle, faFilter, faMagnifyingGlass, faPlug, + faPlus, faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; +import { VariablePermissionCan } from "@app/components/permissions"; import { + Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -29,6 +34,9 @@ import { THead, Tr } from "@app/components/v2"; +import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context"; +import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types"; +import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; import { getUserTablePreference, @@ -39,7 +47,9 @@ import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { TAppConnection, useListAppConnections } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { ProjectType } from "@app/hooks/api/projects/types"; +import { AddAppConnectionModal } from "./AddAppConnectionModal"; import { AppConnectionRow } from "./AppConnectionRow"; import { DeleteAppConnectionModal } from "./DeleteAppConnectionModal"; import { EditAppConnectionCredentialsModal } from "./EditAppConnectionCredentialsModal"; @@ -48,22 +58,45 @@ import { EditAppConnectionDetailsModal } from "./EditAppConnectionDetailsModal"; enum AppConnectionsOrderBy { App = "app", Name = "name", - Method = "method" + Method = "method", + ManagedBy = "managed-by" } type AppConnectionFilters = { apps: AppConnection[]; }; -export const AppConnectionsTable = () => { - const { isPending, data: appConnections = [] } = useListAppConnections(); +enum View { + All = "all", + Scope = "scope" +} + +const APP_CONNECTION_VIEW_STORAGE_KEY = "app-connection-view"; + +type Props = { + projectId?: string; + projectType?: ProjectType; +}; + +export const AppConnectionsTable = ({ projectId, projectType }: Props) => { + const isProjectView = Boolean(projectId); + const { isPending, data: appConnections = [] } = useListAppConnections(projectId); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "addConnection", "deleteConnection", "editCredentials", "editDetails" ] as const); + const [view, setView] = useState(() => { + const storedView = localStorage.getItem(APP_CONNECTION_VIEW_STORAGE_KEY) as View | null; + + if (storedView && Object.values(View).includes(storedView)) return storedView; + + return View.Scope; + }); + const [filters, setFilters] = useState({ apps: [] }); @@ -96,6 +129,10 @@ export const AppConnectionsTable = () => { .filter((appConnection) => { const { app, method, name } = appConnection; + if (view === View.Scope && !isProjectView && appConnection.projectId) { + return false; + } + if (filters.apps.length && !filters.apps.includes(app)) return false; const searchValue = search.trim().toLowerCase(); @@ -121,6 +158,13 @@ export const AppConnectionsTable = () => { .localeCompare( getAppConnectionMethodDetails(connectionTwo.method).name.toLowerCase() ); + case AppConnectionsOrderBy.ManagedBy: + if (!connectionOne.project) return 1; + if (!connectionTwo.project) return -1; + + return connectionOne.project.name + .toLowerCase() + .localeCompare(connectionTwo.project.name.toLowerCase()); case AppConnectionsOrderBy.App: default: return APP_CONNECTION_MAP[connectionOne.app].name @@ -128,7 +172,7 @@ export const AppConnectionsTable = () => { .localeCompare(APP_CONNECTION_MAP[connectionTwo.app].name.toLowerCase()); } }), - [appConnections, orderDirection, search, orderBy, filters] + [appConnections, orderDirection, search, orderBy, filters, view] ); useResetPageHelper({ @@ -165,8 +209,87 @@ export const AppConnectionsTable = () => { handlePopUpOpen("editDetails", appConnection); return ( -
+
+
+
+
+

App Connections

+ +
+ + Docs + +
+
+
+

+ Create and configure connections with third-party apps for re-use across your project + {isProjectView ? "" : "s"}. +

+
+ + {(isAllowed) => ( + + )} + +
+ {!isProjectView && ( +
+ + +
+ )} setSearch(e.target.value)} @@ -188,38 +311,46 @@ export const AppConnectionsTable = () => { - + Filter by Apps {appConnections.length ? ( - [...new Set(appConnections.map(({ app }) => app))].map((app) => ( - { - e.preventDefault(); - setFilters((prev) => ({ - ...prev, - apps: prev.apps.includes(app) - ? prev.apps.filter((a) => a !== app) - : [...prev.apps, app] - })); - }} - key={app} - icon={ - filters.apps.includes(app) && ( - - ) - } - iconPos="right" - > -
- {`${APP_CONNECTION_MAP[app].name} - {APP_CONNECTION_MAP[app].name} -
-
- )) + [...new Set(appConnections.map(({ app }) => app))] + .sort((a, b) => { + return a.toLowerCase().localeCompare(b.toLowerCase()); + }) + .map((app) => ( + { + e.preventDefault(); + setFilters((prev) => ({ + ...prev, + apps: prev.apps.includes(app) + ? prev.apps.filter((a) => a !== app) + : [...prev.apps, app] + })); + }} + key={app} + icon={ + filters.apps.includes(app) && ( + + ) + } + iconPos="right" + > +
+ {`${APP_CONNECTION_MAP[app].name} + {APP_CONNECTION_MAP[app].name} +
+
+ )) ) : ( No Connections Configured )} @@ -269,7 +400,21 @@ export const AppConnectionsTable = () => {
- + {!isProjectView && ( + +
+ Managed By + handleSort(AppConnectionsOrderBy.ManagedBy)} + > + + +
+ + )} @@ -284,6 +429,7 @@ export const AppConnectionsTable = () => { onDelete={handleDelete} onEditCredentials={handleEditCredentials} onEditDetails={handleEditDetails} + isProjectView={isProjectView} /> ))} @@ -323,6 +469,12 @@ export const AppConnectionsTable = () => { onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)} appConnection={popUp.editDetails.data} /> + handlePopUpToggle("addConnection", isOpen)} + projectId={projectId} + projectType={projectType} + />
); }; diff --git a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx index 2798a7121..f5f6d2c88 100644 --- a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx +++ b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx @@ -12,73 +12,14 @@ import { AzureKeyVaultConnectionMethod, GitHubConnectionMethod, GitLabConnectionMethod, - TAzureAppConfigurationConnection, - TAzureClientSecretsConnection, - TAzureDevOpsConnection, - TAzureKeyVaultConnection, - TGitHubConnection, - TGitHubRadarConnection, - TGitLabConnection, + TAppConnection, useCreateAppConnection, useUpdateAppConnection } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; -type BaseFormData = { - returnUrl?: string; - connectionId?: string; - isUpdate?: boolean; -}; - -type GithubFormData = BaseFormData & - Pick; - -type GithubRadarFormData = BaseFormData & - Pick; - -type GitLabFormData = BaseFormData & - Pick; - -type AzureKeyVaultFormData = BaseFormData & - Pick & - Pick; - -type AzureAppConfigurationFormData = BaseFormData & - Pick & - Pick; - -type AzureClientSecretsFormData = BaseFormData & - Pick & - Pick; - -type OAuthCredentials = Extract< - TAzureDevOpsConnection, - { method: AzureDevOpsConnectionMethod.OAuth } ->["credentials"]; -type AccessTokenCredentials = Extract< - TAzureDevOpsConnection, - { method: AzureDevOpsConnectionMethod.AccessToken } ->["credentials"]; - -type AzureDevOpsFormData = BaseFormData & - Pick & - (Pick | Pick); - -type FormDataMap = { - [AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub }; - [AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar }; - [AppConnection.GitLab]: GitLabFormData & { app: AppConnection.GitLab }; - [AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault }; - [AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & { - app: AppConnection.AzureAppConfiguration; - }; - [AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & { - app: AppConnection.AzureClientSecrets; - }; - [AppConnection.AzureDevOps]: AzureDevOpsFormData & { - app: AppConnection.AzureDevOps; - }; -}; +import { FormDataMap } from "./OauthCallbackPage.types"; const formDataStorageFieldMap: Partial> = { [AppConnection.GitHub]: "githubConnectionFormData", @@ -148,11 +89,14 @@ export const OAuthCallbackPage = () => { clearState(AppConnection.GitLab); - const { connectionId, name, description, returnUrl, isUpdate, credentials } = formData; + const { connectionId, name, description, returnUrl, isUpdate, projectId, credentials } = + formData; + + let connection: TAppConnection; try { if (isUpdate && connectionId) { - await updateAppConnection.mutateAsync({ + connection = await updateAppConnection.mutateAsync({ app: AppConnection.GitLab, connectionId, credentials: { @@ -161,10 +105,11 @@ export const OAuthCallbackPage = () => { } }); } else { - await createAppConnection.mutateAsync({ + connection = await createAppConnection.mutateAsync({ app: AppConnection.GitLab, name, description, + projectId, method: GitLabConnectionMethod.OAuth, credentials: { code: code as string, @@ -173,14 +118,12 @@ export const OAuthCallbackPage = () => { }); } - navigate({ - to: returnUrl ?? "/organization/app-connections" - }); - return { connectionId, returnUrl, - appConnectionName: formData.app + appConnectionName: formData.app, + projectId, + connection }; } catch (err: any) { createNotification({ @@ -189,7 +132,10 @@ export const OAuthCallbackPage = () => { type: "error" }); navigate({ - to: returnUrl ?? "/organization/app-connections" + to: returnUrl, + params: { + projectId + } }); return null; } @@ -201,11 +147,13 @@ export const OAuthCallbackPage = () => { clearState(AppConnection.AzureKeyVault); - const { connectionId, name, description, returnUrl } = formData; + const { connectionId, name, description, returnUrl, projectId } = formData; + + let connection: TAppConnection; try { if (connectionId) { - await updateAppConnection.mutateAsync({ + connection = await updateAppConnection.mutateAsync({ app: AppConnection.AzureKeyVault, connectionId, credentials: { @@ -214,10 +162,11 @@ export const OAuthCallbackPage = () => { } }); } else { - await createAppConnection.mutateAsync({ + connection = await createAppConnection.mutateAsync({ app: AppConnection.AzureKeyVault, name, description, + projectId, method: AzureKeyVaultConnectionMethod.OAuth, credentials: { tenantId: formData.tenantId, @@ -232,14 +181,20 @@ export const OAuthCallbackPage = () => { type: "error" }); navigate({ - to: returnUrl ?? "/organization/app-connections" + to: returnUrl, + params: { + projectId + } }); + return null; } return { connectionId, returnUrl, - appConnectionName: formData.app + appConnectionName: formData.app, + projectId, + connection }; }, []); @@ -249,11 +204,13 @@ export const OAuthCallbackPage = () => { clearState(AppConnection.AzureAppConfiguration); - const { connectionId, name, description, returnUrl } = formData; + const { connectionId, name, description, returnUrl, projectId } = formData; + + let connection: TAppConnection; try { if (connectionId) { - await updateAppConnection.mutateAsync({ + connection = await updateAppConnection.mutateAsync({ app: AppConnection.AzureAppConfiguration, connectionId, credentials: { @@ -262,10 +219,11 @@ export const OAuthCallbackPage = () => { } }); } else { - await createAppConnection.mutateAsync({ + connection = await createAppConnection.mutateAsync({ app: AppConnection.AzureAppConfiguration, name, description, + projectId, method: AzureAppConfigurationConnectionMethod.OAuth, credentials: { code: code as string, @@ -280,14 +238,20 @@ export const OAuthCallbackPage = () => { type: "error" }); navigate({ - to: returnUrl ?? "/organization/app-connections" + to: returnUrl, + params: { + projectId + } }); + return null; } return { connectionId, returnUrl, - appConnectionName: formData.app + appConnectionName: formData.app, + projectId, + connection }; }, []); @@ -297,11 +261,13 @@ export const OAuthCallbackPage = () => { clearState(AppConnection.AzureClientSecrets); - const { connectionId, name, description, returnUrl } = formData; + const { connectionId, name, description, returnUrl, projectId } = formData; + + let connection: TAppConnection; try { if (connectionId) { - await updateAppConnection.mutateAsync({ + connection = await updateAppConnection.mutateAsync({ app: AppConnection.AzureClientSecrets, connectionId, credentials: { @@ -310,11 +276,12 @@ export const OAuthCallbackPage = () => { } }); } else { - await createAppConnection.mutateAsync({ + connection = await createAppConnection.mutateAsync({ app: AppConnection.AzureClientSecrets, name, description, method: AzureClientSecretsConnectionMethod.OAuth, + projectId, credentials: { code: code as string, tenantId: formData.tenantId @@ -328,14 +295,20 @@ export const OAuthCallbackPage = () => { type: "error" }); navigate({ - to: returnUrl ?? "/organization/app-connections" + to: returnUrl, + params: { + projectId + } }); + return null; } return { connectionId, returnUrl, - appConnectionName: formData.app + appConnectionName: formData.app, + projectId, + connection }; }, []); @@ -345,7 +318,9 @@ export const OAuthCallbackPage = () => { clearState(AppConnection.AzureDevOps); - const { connectionId, name, description, returnUrl } = formData; + const { connectionId, name, description, returnUrl, projectId } = formData; + + let connection: TAppConnection; try { if (!("tenantId" in formData)) { @@ -353,7 +328,7 @@ export const OAuthCallbackPage = () => { } if (connectionId) { - await updateAppConnection.mutateAsync({ + connection = await updateAppConnection.mutateAsync({ app: AppConnection.AzureDevOps, connectionId, credentials: { @@ -363,11 +338,12 @@ export const OAuthCallbackPage = () => { } }); } else { - await createAppConnection.mutateAsync({ + connection = await createAppConnection.mutateAsync({ app: AppConnection.AzureDevOps, name, description, method: AzureDevOpsConnectionMethod.OAuth, + projectId, credentials: { code: code as string, tenantId: formData.tenantId as string, @@ -382,14 +358,20 @@ export const OAuthCallbackPage = () => { type: "error" }); navigate({ - to: returnUrl ?? "/organization/app-connections" + to: returnUrl, + params: { + projectId + } }); + return null; } return { connectionId, returnUrl, - appConnectionName: formData.app + appConnectionName: formData.app, + projectId, + connection }; }, []); @@ -399,11 +381,14 @@ export const OAuthCallbackPage = () => { clearState(AppConnection.GitHub); - const { connectionId, name, description, returnUrl, gatewayId, credentials } = formData; + const { connectionId, name, description, returnUrl, gatewayId, credentials, projectId } = + formData; + + let connection: TAppConnection; try { if (connectionId) { - await updateAppConnection.mutateAsync({ + connection = await updateAppConnection.mutateAsync({ app: AppConnection.GitHub, ...(installationId ? { @@ -427,10 +412,11 @@ export const OAuthCallbackPage = () => { }) }); } else { - await createAppConnection.mutateAsync({ + connection = await createAppConnection.mutateAsync({ app: AppConnection.GitHub, name, description, + projectId, ...(installationId ? { method: GitHubConnectionMethod.App, @@ -460,14 +446,20 @@ export const OAuthCallbackPage = () => { type: "error" }); navigate({ - to: returnUrl ?? "/organization/app-connections" + to: returnUrl, + params: { + projectId + } }); + return null; } return { connectionId, returnUrl, - appConnectionName: formData.app + appConnectionName: formData.app, + projectId, + connection }; }, []); @@ -477,11 +469,13 @@ export const OAuthCallbackPage = () => { clearState(AppConnection.GitHubRadar); - const { connectionId, name, description, returnUrl } = formData; + const { connectionId, name, description, returnUrl, projectId } = formData; + + let connection: TAppConnection; try { if (connectionId) { - await updateAppConnection.mutateAsync({ + connection = await updateAppConnection.mutateAsync({ app: AppConnection.GitHubRadar, connectionId, credentials: { @@ -490,11 +484,12 @@ export const OAuthCallbackPage = () => { } }); } else { - await createAppConnection.mutateAsync({ + connection = await createAppConnection.mutateAsync({ app: AppConnection.GitHubRadar, name, description, method: GitHubConnectionMethod.App, + projectId, credentials: { code: code as string, installationId: installationId as string @@ -508,14 +503,20 @@ export const OAuthCallbackPage = () => { type: "error" }); navigate({ - to: returnUrl ?? "/organization/app-connections" + to: returnUrl, + params: { + projectId + } }); + return null; } return { connectionId, returnUrl, - appConnectionName: formData.app + appConnectionName: formData.app, + projectId, + connection }; }, []); @@ -530,8 +531,13 @@ export const OAuthCallbackPage = () => { if (!isReady) return; (async () => { - let data: { connectionId?: string; returnUrl?: string; appConnectionName?: string } | null = - null; + let data: { + returnUrl: string; + appConnectionName: string; + connectionId?: string; + projectId?: string; + connection: TAppConnection; + } | null = null; if (appConnection === AppConnection.GitHub) { data = await handleGithub(); @@ -554,16 +560,26 @@ export const OAuthCallbackPage = () => { text: `Successfully ${data.connectionId ? "updated" : "added"} ${data.appConnectionName ? APP_CONNECTION_MAP[data.appConnectionName as AppConnection].name : ""} Connection`, type: "success" }); - } else { - createNotification({ - text: "Failed to add connection", - type: "error" + + await navigate({ + to: data.returnUrl, + params: { + projectId: data.projectId ?? undefined + }, + // scott: if it's not an app connection page we need to pass connection details as it's an inline creation + search: data.returnUrl.includes("app-connections") + ? undefined + : { + connectionId: data.connection.id, + connectionName: data.connection.name, + ...(data.returnUrl.includes("integrations") + ? { + selectedTab: IntegrationsListPageTabs.SecretSyncs + } + : {}) + } }); } - - await navigate({ - to: data?.returnUrl ?? "/organization/app-connections" - }); })(); }, [isReady]); diff --git a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.types.ts b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.types.ts new file mode 100644 index 000000000..6c76a60b3 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.types.ts @@ -0,0 +1,68 @@ +import { + AzureDevOpsConnectionMethod, + TAzureAppConfigurationConnection, + TAzureClientSecretsConnection, + TAzureDevOpsConnection, + TAzureKeyVaultConnection, + TGitHubConnection, + TGitHubRadarConnection, + TGitLabConnection +} from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +type BaseFormData = { + returnUrl: string; + connectionId?: string; + isUpdate?: boolean; + projectId: string; +}; + +export type GithubFormData = BaseFormData & + Pick; + +export type GithubRadarFormData = BaseFormData & + Pick; + +export type GitLabFormData = BaseFormData & + Pick; + +export type AzureKeyVaultFormData = BaseFormData & + Pick & + Pick; + +export type AzureAppConfigurationFormData = BaseFormData & + Pick & + Pick; + +export type AzureClientSecretsFormData = BaseFormData & + Pick & + Pick; + +type OAuthCredentials = Extract< + TAzureDevOpsConnection, + { method: AzureDevOpsConnectionMethod.OAuth } +>["credentials"]; +type AccessTokenCredentials = Extract< + TAzureDevOpsConnection, + { method: AzureDevOpsConnectionMethod.AccessToken } +>["credentials"]; + +export type AzureDevOpsFormData = BaseFormData & + Pick & + (Pick | Pick); + +export type FormDataMap = { + [AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub }; + [AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar }; + [AppConnection.GitLab]: GitLabFormData & { app: AppConnection.GitLab }; + [AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault }; + [AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & { + app: AppConnection.AzureAppConfiguration; + }; + [AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & { + app: AppConnection.AzureClientSecrets; + }; + [AppConnection.AzureDevOps]: AzureDevOpsFormData & { + app: AppConnection.AzureDevOps; + }; +}; diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index 715f9c0b2..40b2f973f 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -21,7 +21,7 @@ import { SelectItem } from "@app/components/v2"; import { useOrganization } from "@app/context"; -import { useGetUserWorkspaces } from "@app/hooks/api"; +import { useGetUserProjects } from "@app/hooks/api"; import { eventToNameMap, secretEvents, @@ -29,7 +29,7 @@ import { } from "@app/hooks/api/auditLogs/constants"; import { EventType } from "@app/hooks/api/auditLogs/enums"; import { UserAgentType } from "@app/hooks/api/auth/types"; -import { Workspace } from "@app/hooks/api/workspace/types"; +import { Project } from "@app/hooks/api/projects/types"; import { LogFilterItem } from "./LogFilterItem"; import { auditLogFilterFormSchema, Presets, TAuditLogFilterFormData } from "./types"; @@ -44,7 +44,7 @@ type Props = { presets?: Presets; setFilter: (data: TAuditLogFilterFormData) => void; filter: TAuditLogFilterFormData; - project?: Workspace; + project?: Project; }; const getActiveFilterCount = (filter: TAuditLogFilterFormData) => { @@ -72,7 +72,7 @@ const getActiveFilterCount = (filter: TAuditLogFilterFormData) => { }; export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => { - const { data: workspaces = [] } = useGetUserWorkspaces(); + const { data: workspaces = [] } = useGetUserProjects(); const { currentOrg } = useOrganization(); const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id); diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx index 5bbb42d32..f1173447c 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx @@ -13,7 +13,7 @@ import { } from "@app/context"; import { Timezone } from "@app/helpers/datetime"; import { withPermission, withProjectPermission } from "@app/hoc"; -import { Workspace } from "@app/hooks/api/workspace/types"; +import { Project } from "@app/hooks/api/projects/types"; import { usePopUp } from "@app/hooks/usePopUp"; import { LogsDateFilter } from "./LogsDateFilter"; @@ -31,7 +31,7 @@ type Props = { refetchInterval?: number; showFilters?: boolean; pageView?: boolean; - project?: Workspace; + project?: Project; }; const LogsSectionComponent = ({ diff --git a/frontend/src/pages/organization/AuditLogsPage/components/types.tsx b/frontend/src/pages/organization/AuditLogsPage/components/types.tsx index 8e0ec9ba9..b1338be24 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/types.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/types.tsx @@ -1,7 +1,7 @@ import { z } from "zod"; import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums"; -import { ProjectType } from "@app/hooks/api/workspace/types"; +import { ProjectType } from "@app/hooks/api/projects/types"; export enum AuditLogDateFilterType { Relative = "relative", diff --git a/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx b/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx index eaa90450e..6ab0429ee 100644 --- a/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx +++ b/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx @@ -1,5 +1,7 @@ +import { useEffect } from "react"; import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useQueryClient } from "@tanstack/react-query"; import { OrgPermissionCan } from "@app/components/permissions"; import { Button } from "@app/components/v2"; @@ -15,13 +17,15 @@ import { useGetOrgPlanBillingInfo, useGetOrgTrialUrl } from "@app/hooks/api"; +import { subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries"; import { usePopUp } from "@app/hooks/usePopUp"; import { ManagePlansModal } from "./ManagePlansModal"; export const PreviewSection = () => { const { currentOrg } = useOrganization(); - const { subscription } = useSubscription(); + const { subscription } = useSubscription(true); + const queryClient = useQueryClient(); const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? ""); const getOrgTrialUrl = useGetOrgTrialUrl(); const createCustomerPortalSession = useCreateCustomerPortalSession(); @@ -37,6 +41,12 @@ export const PreviewSection = () => { return formattedTotal; }; + useEffect(() => { + queryClient.invalidateQueries({ + queryKey: subscriptionQueryKeys.getOrgSubsription(currentOrg?.id ?? "") + }); + }, []); + const formatDate = (date: number) => { const createdDate = new Date(date * 1000); const day: number = createdDate.getDate(); diff --git a/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx b/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx index 1552f8548..a0d440872 100644 --- a/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx +++ b/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx @@ -4,17 +4,17 @@ import { faArrowUpRightFromSquare, faBookOpen, faCopy, + faDoorClosed, faEdit, faEllipsisV, faInfoCircle, faMagnifyingGlass, - faPlug, faSearch, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQuery } from "@tanstack/react-query"; -import { format, formatRelative } from "date-fns"; +import { formatRelative } from "date-fns"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; @@ -48,13 +48,14 @@ import { import { withPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks"; import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways"; +import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2"; import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal"; export const GatewayListPage = withPermission( () => { const [search, setSearch] = useState(""); - const { data: gateways, isPending: isGatewayLoading } = useQuery(gatewaysQueryKeys.list()); + const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ "deleteGateway", @@ -62,16 +63,20 @@ export const GatewayListPage = withPermission( ] as const); const deleteGatewayById = useDeleteGatewayById(); + const deleteGatewayV2ById = useDeleteGatewayV2ById(); const handleDeleteGateway = async () => { - await deleteGatewayById.mutateAsync((popUp.deleteGateway.data as { id: string }).id, { - onSuccess: () => { - handlePopUpToggle("deleteGateway"); - createNotification({ - type: "success", - text: "Successfully delete gateway" - }); - } + const data = popUp.deleteGateway.data as { id: string; isV1: boolean }; + if (data.isV1) { + await deleteGatewayById.mutateAsync(data.id); + } else { + await deleteGatewayV2ById.mutateAsync(data.id); + } + + handlePopUpToggle("deleteGateway"); + createNotification({ + type: "success", + text: "Successfully deleted gateway" }); }; @@ -127,7 +132,6 @@ export const GatewayListPage = withPermission( Name - Cert Issued At Identity Health Check @@ -143,13 +147,19 @@ export const GatewayListPage = withPermission( - {isGatewayLoading && ( + {isGatewaysLoading && ( )} {filteredGateway?.map((el) => ( - {el.name} - {format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")} + +
+ {el.name} + + Gateway v{el.isV1 ? "1" : "2"} + +
+ {el.identity.name} {el.heartbeat @@ -176,20 +186,22 @@ export const GatewayListPage = withPermission( > Copy ID - - {(isAllowed: boolean) => ( - } - onClick={() => handlePopUpOpen("editDetails", el)} - > - Edit Details - - )} - + {el.isV1 && ( + + {(isAllowed: boolean) => ( + } + onClick={() => handlePopUpOpen("editDetails", el)} + > + Edit Details + + )} + + )} - {!isGatewayLoading && !filteredGateway?.length && ( + {!isGatewaysLoading && !filteredGateway?.length && ( )} ); }, - { - action: OrgPermissionAppConnectionActions.Read, - subject: OrgPermissionSubjects.AppConnections - } + { action: OrgGatewayPermissionActions.ListGateways, subject: OrgPermissionSubjects.Gateway } ); diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx index a0565d50d..2e5c7101c 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx @@ -17,7 +17,7 @@ import { useAddIdentityToWorkspace, useGetIdentityProjectMemberships, useGetProjectRoles, - useGetUserWorkspaces, + useGetUserProjects, useGetWorkspaceById } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -44,7 +44,7 @@ type Props = { const Content = ({ identityId, handlePopUpToggle }: Omit) => { const { currentOrg } = useOrganization(); - const { data: workspaces = [] } = useGetUserWorkspaces(); + const { data: workspaces = [] } = useGetUserProjects(); const { mutateAsync: addIdentityToWorkspace } = useAddIdentityToWorkspace(); const { @@ -77,7 +77,7 @@ const Content = ({ identityId, handlePopUpToggle }: Omit) => { const onFormSubmit = async ({ project: selectedProject, role }: FormData) => { try { await addIdentityToWorkspace({ - workspaceId: selectedProject.id, + projectId: selectedProject.id, identityId, role: role.slug || undefined }); diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx index 5b0aac0b7..1622962ea 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx @@ -8,7 +8,7 @@ import { createNotification } from "@app/components/notifications"; import { IconButton, Td, Tooltip, Tr } from "@app/components/v2"; import { getProjectBaseURL } from "@app/helpers/project"; import { formatProjectRoleName } from "@app/helpers/roles"; -import { useGetUserWorkspaces } from "@app/hooks/api"; +import { useGetUserProjects } from "@app/hooks/api"; import { IdentityMembership } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -31,7 +31,7 @@ export const IdentityProjectRow = ({ membership: { id, createdAt, identity, project, roles }, handlePopUpOpen }: Props) => { - const { data: workspaces } = useGetUserWorkspaces(); + const { data: workspaces } = useGetUserProjects(); const navigate = useNavigate(); const isAccessible = useMemo(() => { diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx index 0ea97c4d3..321a84ccb 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx @@ -25,7 +25,7 @@ export const IdentityProjectsSection = ({ identityId }: Props) => { try { await deleteMutateAsync({ identityId: id, - workspaceId: projectId + projectId }); createNotification({ diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx new file mode 100644 index 000000000..b5cb2d901 --- /dev/null +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx @@ -0,0 +1,81 @@ +import { useState } from "react"; +import { UseMutationResult } from "@tanstack/react-query"; +import ms from "ms"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button } from "@app/components/v2"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context"; + +import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; + +export const LockoutFields = ({ + clearLockoutsResult, + lockedOut, + identityId, + data, + onResetAllLockouts +}: { + clearLockoutsResult: UseMutationResult; + lockedOut: boolean; + identityId: string; + data: { + lockoutEnabled: boolean; + lockoutThreshold: number; + lockoutDurationSeconds: number; + lockoutCounterResetSeconds: number; + }; + onResetAllLockouts: () => void; +}) => { + const { mutateAsync, isPending } = clearLockoutsResult; + + const [lockedOutState, setLockedOutState] = useState(lockedOut); + + async function clearLockouts() { + try { + const deleted = await mutateAsync({ identityId }); + createNotification({ + text: `Successfully cleared ${deleted} lockout${deleted === 1 ? "" : "s"}`, + type: "success" + }); + setLockedOutState(false); + onResetAllLockouts(); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to clear lockouts. Please try again.", + type: "error" + }); + } + } + + return ( + <> +
+ Lockout Options + + {(isAllowed) => ( + + )} + +
+ + {data.lockoutThreshold} + + + {ms(data.lockoutDurationSeconds * 1000, { long: true })} + + + {ms(data.lockoutCounterResetSeconds * 1000, { long: true })} + + + ); +}; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx index f732fd5ed..9c20a1bbe 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx @@ -2,11 +2,12 @@ import { faBan, faEye } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2"; -import { useGetIdentityLdapAuth } from "@app/hooks/api"; +import { useClearIdentityLdapAuthLockouts, useGetIdentityLdapAuth } from "@app/hooks/api"; import { IdentityLdapAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm"; import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; +import { LockoutFields } from "./IdentityAuthLockoutFields"; import { ViewAuthMethodProps } from "./types"; export const ViewIdentityLdapAuthContent = ({ @@ -14,9 +15,12 @@ export const ViewIdentityLdapAuthContent = ({ handlePopUpToggle, handlePopUpOpen, onDelete, - popUp + popUp, + lockedOut, + onResetAllLockouts }: ViewAuthMethodProps) => { const { data, isPending } = useGetIdentityLdapAuth(identityId); + const clearLockoutsResult = useClearIdentityLdapAuthLockouts(); if (isPending) { return ( @@ -98,6 +102,18 @@ export const ViewIdentityLdapAuthContent = ({ )} + + {data.lockoutEnabled ? "Enabled" : "Disabled"} + + {data.lockoutEnabled && ( + + )} ); }; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx index b05a873ce..4d7b95087 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx @@ -1,12 +1,7 @@ -import { useState } from "react"; import { faBan, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import ms from "ms"; -import { createNotification } from "@app/components/notifications"; -import { OrgPermissionCan } from "@app/components/permissions"; -import { Button, EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2"; -import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context"; +import { EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2"; import { useTimedReset } from "@app/hooks"; import { useClearIdentityUniversalAuthLockouts, @@ -16,6 +11,7 @@ import { import { IdentityUniversalAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; +import { LockoutFields } from "./IdentityAuthLockoutFields"; import { IdentityUniversalAuthClientSecretsTable } from "./IdentityUniversalAuthClientSecretsTable"; import { ViewAuthMethodProps } from "./types"; import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper"; @@ -32,33 +28,12 @@ export const ViewIdentityUniversalAuthContent = ({ const { data, isPending } = useGetIdentityUniversalAuth(identityId); const { data: clientSecrets = [], isPending: clientSecretsPending } = useGetIdentityUniversalAuthClientSecrets(identityId); - const { mutateAsync: clearLockoutsFn, isPending: isClearLockoutsPending } = - useClearIdentityUniversalAuthLockouts(); - - const [lockedOutState, setLockedOutState] = useState(lockedOut); + const clearLockoutsResult = useClearIdentityUniversalAuthLockouts(); const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset({ initialState: "Copy Client ID to clipboard" }); - async function clearLockouts() { - try { - const deleted = await clearLockoutsFn({ identityId }); - createNotification({ - text: `Successfully cleared ${deleted} lockout${deleted === 1 ? "" : "s"}`, - type: "success" - }); - setLockedOutState(false); - onResetAllLockouts(); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to clear lockouts. Please try again.", - type: "error" - }); - } - } - if (isPending || clientSecretsPending) { return (
@@ -119,36 +94,13 @@ export const ViewIdentityUniversalAuthContent = ({ {data.lockoutEnabled ? "Enabled" : "Disabled"} {data.lockoutEnabled && ( - <> -
- Lockout Options - - {(isAllowed) => ( - - )} - -
- - {data.lockoutThreshold} - - - {ms(data.lockoutDurationSeconds * 1000, { long: true })} - - - {ms(data.lockoutCounterResetSeconds * 1000, { long: true })} - - + )}
diff --git a/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx b/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx index 7b066a099..f7d20e2b7 100644 --- a/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx +++ b/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx @@ -1,5 +1,4 @@ import { useState } from "react"; -import { useForm } from "react-hook-form"; import { faArrowDownAZ, faBorderAll, @@ -16,6 +15,7 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; +import { RequestProjectAccessModal } from "@app/components/projects/RequestProjectAccessModal"; import { Badge, Button, @@ -24,12 +24,9 @@ import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, - FormControl, IconButton, Input, Lottie, - Modal, - ModalContent, Pagination, Skeleton, Tooltip @@ -43,12 +40,8 @@ import { setUserTablePreference } from "@app/helpers/userTablePreferences"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; -import { - useOrgAdminAccessProject, - useRequestProjectAccess, - useSearchProjects -} from "@app/hooks/api"; -import { ProjectType, Workspace, WorkspaceEnv } from "@app/hooks/api/workspace/types"; +import { useOrgAdminAccessProject, useSearchProjects } from "@app/hooks/api"; +import { Project, ProjectEnv, ProjectType } from "@app/hooks/api/projects/types"; import { ProjectListToggle, ProjectListView @@ -62,53 +55,6 @@ type Props = { onProjectListViewChange: (value: ProjectListView) => void; }; -type RequestAccessModalProps = { - projectId: string; - onPopUpToggle: () => void; -}; - -const RequestAccessModal = ({ projectId, onPopUpToggle }: RequestAccessModalProps) => { - const form = useForm<{ note: string }>(); - - const requestProjectAccess = useRequestProjectAccess(); - - const onFormSubmit = ({ note }: { note: string }) => { - if (requestProjectAccess.isPending) return; - requestProjectAccess.mutate( - { - comment: note, - projectId - }, - { - onSuccess: () => { - createNotification({ - type: "success", - title: "Project Access Request Sent", - text: "Project admins will receive an email of your request" - }); - onPopUpToggle(); - } - } - ); - }; - - return ( - - - - -
- - -
- - ); -}; - export const AllProjectView = ({ onAddNewProject, onUpgradePlan, @@ -155,7 +101,7 @@ export const AllProjectView = ({ const handleAccessProject = async ( type: ProjectType, projectId: string, - environments: WorkspaceEnv[] + environments: ProjectEnv[] ) => { try { await orgAdminAccessProject.mutateAsync({ @@ -180,7 +126,7 @@ export const AllProjectView = ({ offset, totalCount: searchedProjects?.totalCount || 0 }); - const requestedWorkspaceDetails = (popUp.requestAccessConfirmation.data || {}) as Workspace; + const requestedWorkspaceDetails = (popUp.requestAccessConfirmation.data || {}) as Project; const handleToggleFilterByProjectType = (el: ProjectType) => setProjectTypeFilter((state) => (state === el ? undefined : el)); @@ -278,22 +224,26 @@ export const AllProjectView = ({
- {(isAllowed) => ( - + {(isOldProjectPermissionAllowed) => ( + + {(isAllowed) => ( + + )} + )}
@@ -419,20 +369,11 @@ export const AllProjectView = ({
No Projects Found
)} - handlePopUpToggle("requestAccessConfirmation", isOpen)} - > - - handlePopUpToggle("requestAccessConfirmation")} - projectId={requestedWorkspaceDetails?.id} - /> - - + project={requestedWorkspaceDetails} + />
); }; diff --git a/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx b/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx index d63099273..46f520d41 100644 --- a/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx +++ b/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx @@ -39,11 +39,11 @@ import { setUserTablePreference } from "@app/helpers/userTablePreferences"; import { usePagination, useResetPageHelper } from "@app/hooks"; -import { useGetUserWorkspaces } from "@app/hooks/api"; +import { useGetUserProjects } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { Project, ProjectType } from "@app/hooks/api/projects/types"; import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; -import { ProjectType, Workspace } from "@app/hooks/api/workspace/types"; import { ProjectListToggle, ProjectListView @@ -79,7 +79,7 @@ export const MyProjectView = ({ {} ); - const { data: workspaces = [], isPending: isWorkspaceLoading } = useGetUserWorkspaces(); + const { data: workspaces = [], isPending: isWorkspaceLoading } = useGetUserProjects(); const { setPage, perPage, @@ -136,7 +136,7 @@ export const MyProjectView = ({ const { workspacesWithFaveProp } = useMemo(() => { const workspacesWithFav = filteredWorkspaces - .map((w): Workspace & { isFavorite: boolean } => ({ + .map((w): Project & { isFavorite: boolean } => ({ ...w, isFavorite: Boolean(projectFavorites?.includes(w.id)) })) @@ -188,7 +188,7 @@ export const MyProjectView = ({ } }; - const renderProjectGridItem = (workspace: Workspace, isFavorite: boolean) => ( + const renderProjectGridItem = (workspace: Project, isFavorite: boolean) => ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
{ @@ -242,7 +242,7 @@ export const MyProjectView = ({

); - const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => ( + const renderProjectListItem = (workspace: Project, isFavorite: boolean, index: number) => ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
{ @@ -480,22 +480,26 @@ export const MyProjectView = ({
- {(isAllowed) => ( - + {(isOldProjectV1Allowed) => ( + + {(isAllowed) => ( + + )} + )}
diff --git a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts index 03b86f2ab..9c374d398 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts +++ b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts @@ -122,12 +122,11 @@ export const formSchema = z.object({ .refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }), permissions: z .object({ - workspace: z + project: z .object({ create: z.boolean().optional() }) .optional(), - "audit-logs": auditLogsPermissionSchema, member: generalPermissionSchema, groups: groupPermissionSchema, @@ -162,7 +161,11 @@ export const rolePermission2Form = (permissions: TPermission[] = []) => { // i would have to write a if loop with both conditions same const formVal: Record = {}; permissions.forEach((permission) => { - const { subject, action } = permission; + const { action } = permission; + let { subject } = permission; + if (subject === OrgPermissionSubjects.Workspace) { + subject = OrgPermissionSubjects.Project; + } if (!formVal?.[subject]) formVal[subject] = {}; formVal[subject][action] = true; }); diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx index 2746e10bc..5f9041a29 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx @@ -28,7 +28,7 @@ export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) => const rule = useWatch({ control, - name: "permissions.workspace" + name: "permissions.project" }); const selectedPermissionCategory = useMemo(() => { @@ -60,7 +60,7 @@ export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) => setIsCustom.off(); if (val === Permission.NoAccess) { - setValue("permissions.workspace", { create: false }, { shouldDirty: true }); + setValue("permissions.project", { create: false }, { shouldDirty: true }); } }; @@ -95,8 +95,8 @@ export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) => {PERMISSION_ACTIONS.map(({ action, label }) => { return ( ( , - | "workspace" + | "project" | "organization-admin-console" | "kmip" | "gateway" diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm.tsx deleted file mode 100644 index 7e16afd60..000000000 --- a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { Controller, useFieldArray, useForm } from "react-hook-form"; -import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { z } from "zod"; - -import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, FormLabel, IconButton, Input, Spinner } from "@app/components/v2"; -import { useOrganization } from "@app/context"; -import { - useCreateAuditLogStream, - useGetAuditLogStreamDetails, - useUpdateAuditLogStream -} from "@app/hooks/api"; - -type Props = { - id?: string; - onClose: () => void; -}; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const formSchema = z.object({ - url: z.string().url().min(1), - headers: z - .object({ - key: z.string(), - value: z.string() - }) - .array() - .optional() -}); -type TForm = z.infer; - -export const AuditLogStreamForm = ({ id = "", onClose }: Props) => { - const isEdit = Boolean(id); - const { currentOrg } = useOrganization(); - const orgId = currentOrg?.id || ""; - - const auditLogStream = useGetAuditLogStreamDetails(id); - const createAuditLogStream = useCreateAuditLogStream(); - const updateAuditLogStream = useUpdateAuditLogStream(); - - const { - handleSubmit, - control, - setValue, - getValues, - formState: { isSubmitting } - } = useForm({ - values: auditLogStream?.data, - defaultValues: { - headers: [{ key: "", value: "" }] - } - }); - - const headerFields = useFieldArray({ - control, - name: "headers" - }); - - const handleAuditLogStreamEdit = async ({ headers, url }: TForm) => { - if (!id) return; - try { - await updateAuditLogStream.mutateAsync({ - id, - orgId, - headers, - url - }); - createNotification({ - type: "success", - text: "Successfully updated stream" - }); - onClose(); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to update stream" - }); - } - }; - - const handleFormSubmit = async ({ headers = [], url }: TForm) => { - if (isSubmitting) return; - const sanitizedHeaders = headers.filter(({ key, value }) => Boolean(key) && Boolean(value)); - const streamHeaders = sanitizedHeaders.length ? sanitizedHeaders : undefined; - if (isEdit) { - await handleAuditLogStreamEdit({ headers: streamHeaders, url }); - return; - } - try { - await createAuditLogStream.mutateAsync({ - orgId, - headers: streamHeaders, - url - }); - createNotification({ - type: "success", - text: "Successfully created stream" - }); - onClose(); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: (err as Error)?.message ?? "Failed to create stream" - }); - } - }; - - if (isEdit && auditLogStream.isPending) { - return ( -
- -
- ); - } - - return ( -
-
- ( - - - - )} - /> - - {headerFields.fields.map(({ id: headerFieldId }, i) => ( -
- ( - - - - )} - /> - ( - - - - )} - /> - { - const header = getValues("headers"); - if (header && header?.length > 1) { - headerFields.remove(i); - } else { - setValue("headers", [{ key: "", value: "" }]); - } - }} - > - - -
- ))} -
- -
-
-
- - -
-
- ); -}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AuditLogStreamForm.tsx new file mode 100644 index 000000000..f59ae23aa --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AuditLogStreamForm.tsx @@ -0,0 +1,139 @@ +import { createNotification } from "@app/components/notifications"; +import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams"; +import { useCreateAuditLogStream, useUpdateAuditLogStream } from "@app/hooks/api"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TAuditLogStream } from "@app/hooks/api/types"; +import { DiscriminativePick } from "@app/types"; + +import { AuditLogStreamHeader } from "../components/AuditLogStreamHeader"; +import { AzureProviderAuditLogStreamForm } from "./AzureProviderAuditLogStreamForm"; +import { CriblProviderAuditLogStreamForm } from "./CriblProviderAuditLogStreamForm"; +import { CustomProviderAuditLogStreamForm } from "./CustomProviderAuditLogStreamForm"; +import { DatadogProviderAuditLogStreamForm } from "./DatadogProviderAuditLogStreamForm"; +import { SplunkProviderAuditLogStreamForm } from "./SplunkProviderAuditLogStreamForm"; + +type FormProps = { + onComplete: (auditLogStream: TAuditLogStream) => void; +}; + +type CreateFormProps = FormProps & { provider: LogProvider }; +type UpdateFormProps = FormProps & { + auditLogStream: TAuditLogStream; +}; + +const CreateForm = ({ provider, onComplete }: CreateFormProps) => { + const createAuditLogStream = useCreateAuditLogStream(); + const { name: providerName } = AUDIT_LOG_STREAM_PROVIDER_MAP[provider]; + + const onSubmit = async ( + formData: DiscriminativePick + ) => { + try { + const logStream = await createAuditLogStream.mutateAsync(formData); + createNotification({ + text: `Successfully created ${providerName} Log Stream`, + type: "success" + }); + onComplete(logStream); + } catch (err: any) { + console.error(err); + createNotification({ + title: `Failed to create ${providerName} Log Stream`, + text: err.message, + type: "error" + }); + } + }; + + switch (provider) { + case LogProvider.Azure: + return ; + case LogProvider.Cribl: + return ; + case LogProvider.Custom: + return ; + case LogProvider.Datadog: + return ; + case LogProvider.Splunk: + return ; + default: + throw new Error(`Unhandled Provider: ${provider}`); + } +}; + +const UpdateForm = ({ auditLogStream, onComplete }: UpdateFormProps) => { + const updateAuditLogStream = useUpdateAuditLogStream(); + const { name: providerName } = AUDIT_LOG_STREAM_PROVIDER_MAP[auditLogStream.provider]; + + const onSubmit = async ( + formData: DiscriminativePick + ) => { + try { + const connection = await updateAuditLogStream.mutateAsync({ + auditLogStreamId: auditLogStream.id, + ...formData + }); + createNotification({ + text: `Successfully updated ${providerName} Log Stream`, + type: "success" + }); + onComplete(connection); + } catch (err: any) { + console.error(err); + createNotification({ + title: `Failed to update ${providerName} Log Stream`, + text: err.message, + type: "error" + }); + } + }; + + switch (auditLogStream.provider) { + case LogProvider.Azure: + return ( + + ); + case LogProvider.Cribl: + return ( + + ); + case LogProvider.Custom: + return ( + + ); + case LogProvider.Datadog: + return ( + + ); + case LogProvider.Splunk: + return ( + + ); + default: + throw new Error(`Unhandled Provider: ${(auditLogStream as TAuditLogStream).provider}`); + } +}; + +type Props = { onBack?: () => void } & Pick & + ( + | { provider: LogProvider; auditLogStream?: undefined } + | { provider?: undefined; auditLogStream: TAuditLogStream } + ); +export const AuditLogStreamForm = ({ onBack, ...props }: Props) => { + const { provider, auditLogStream } = props; + + return ( +
+ + {auditLogStream ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AzureProviderAuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AzureProviderAuditLogStreamForm.tsx new file mode 100644 index 000000000..131ea1c85 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AzureProviderAuditLogStreamForm.tsx @@ -0,0 +1,158 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, Input, ModalClose, SecretInput } from "@app/components/v2"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TAzureProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/azure-provider"; + +type Props = { + auditLogStream?: TAzureProviderLogStream; + onSubmit: (formData: FormData) => void; +}; + +const formSchema = z.object({ + provider: z.literal(LogProvider.Azure), + credentials: z.object({ + tenantId: z.string().trim().uuid(), + clientId: z.string().trim().uuid(), + clientSecret: z.string().trim().length(40), + dceUrl: z.string().trim().url().min(1).max(255), + dcrId: z + .string() + .trim() + .regex(/^dcr-[0-9a-f]{32}$/, "DCR ID must be in dcr-*** format"), + cltName: z.string().trim().min(1).max(255) + }) +}); + +type FormData = z.infer; + +export const AzureProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => { + const isUpdate = Boolean(auditLogStream); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: auditLogStream ?? { + provider: LogProvider.Azure + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CriblProviderAuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CriblProviderAuditLogStreamForm.tsx new file mode 100644 index 000000000..b839ddac8 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CriblProviderAuditLogStreamForm.tsx @@ -0,0 +1,108 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, Input, ModalClose, SecretInput } from "@app/components/v2"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TCriblProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/cribl-provider"; + +type Props = { + auditLogStream?: TCriblProviderLogStream; + onSubmit: (formData: FormData) => void; +}; + +const formSchema = z.object({ + provider: z.literal(LogProvider.Cribl), + credentials: z.object({ + url: z.string().url().trim().min(1).max(255), + token: z.string().trim().min(21).max(255) + }) +}); + +type FormData = z.infer; + +export const CriblProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => { + const isUpdate = Boolean(auditLogStream); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: auditLogStream ?? { + provider: LogProvider.Cribl + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ ( + + To derive your Stream URL: Obtain your Cribl hostname (e.g. cribl.example.com), + Infisical HTTP data source port (e.g. 20000), and HTTP event API path (e.g. + /infisical). +
+
+ If your Infisical Data Source has TLS enabled, then use the https protocol. + + } + > + +
+ )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx new file mode 100644 index 000000000..258af14b9 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx @@ -0,0 +1,176 @@ +import { Controller, FormProvider, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, FormLabel, IconButton, Input, ModalClose } from "@app/components/v2"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TCustomProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/custom-provider"; + +type Props = { + auditLogStream?: TCustomProviderLogStream; + onSubmit: (formData: FormData) => void; +}; + +const formSchema = z.object({ + provider: z.literal(LogProvider.Custom), + credentials: z.object({ + url: z.string().url().trim().min(1).max(255), + headers: z + .object({ + key: z.string().min(1), + value: z.string().min(1) + }) + .array() + }) +}); + +type FormData = z.infer; + +export const CustomProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => { + const isUpdate = Boolean(auditLogStream); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: auditLogStream ?? { + provider: LogProvider.Custom + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty }, + getValues, + setValue + } = form; + + const headerFields = useFieldArray({ + control, + name: "credentials.headers" + }); + + return ( + +
+ ( + + + + )} + /> + + + {headerFields.fields.map(({ id: headerFieldId }, i) => ( +
+ ( + + + + )} + /> + ( + + { + if ( + auditLogStream && + auditLogStream.credentials.headers[i] && + auditLogStream.credentials.headers[i].value === "******" && + field.value === "******" + ) { + field.onChange(""); + } + e.target.type = "text"; + }} + onBlur={(e) => { + if ( + auditLogStream && + auditLogStream.credentials.headers[i] && + auditLogStream.credentials.headers[i].value === "******" && + field.value === "" + ) { + field.onChange("******"); + } + e.target.type = "password"; + }} + /> + + )} + /> + { + const header = getValues("credentials.headers"); + if (header && header?.length > 1) { + headerFields.remove(i); + } else { + setValue("credentials.headers", [{ key: "", value: "" }]); + } + }} + > + + +
+ ))} +
+ +
+ +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/DatadogProviderAuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/DatadogProviderAuditLogStreamForm.tsx new file mode 100644 index 000000000..2db315416 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/DatadogProviderAuditLogStreamForm.tsx @@ -0,0 +1,132 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TDatadogProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/datadog-provider"; + +type Props = { + auditLogStream?: TDatadogProviderLogStream; + onSubmit: (formData: FormData) => void; +}; + +const formSchema = z.object({ + provider: z.literal(LogProvider.Datadog), + credentials: z.object({ + url: z.string().url().trim().min(1).max(255), + token: z + .string() + .trim() + .regex(/^[a-fA-F0-9]{32}$/, "Invalid Datadog API key format") + }) +}); + +type FormData = z.infer; + +const DATADOG_ENDPOINTS = { + "Datadog US1": "https://http-intake.logs.datadoghq.com/api/v2/logs", + "Datadog US3": "https://http-intake.logs.us3.datadoghq.com/api/v2/logs", + "Datadog US5": "https://http-intake.logs.us5.datadoghq.com/api/v2/logs", + "Datadog EU": "https://http-intake.logs.datadoghq.eu/api/v2/logs", + "Datadog AP1": "https://http-intake.logs.ap1.datadoghq.com/api/v2/logs", + "Datadog AP2": "https://http-intake.logs.ap2.datadoghq.com/api/v2/logs", + "Datadog GovCloud (US1-FED)": "https://http-intake.logs.ddog-gov.com/api/v2/logs" +}; + +export const DatadogProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => { + const isUpdate = Boolean(auditLogStream); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: auditLogStream ?? { + provider: LogProvider.Datadog, + credentials: { + url: DATADOG_ENDPOINTS["Datadog US1"] + } + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/SplunkProviderAuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/SplunkProviderAuditLogStreamForm.tsx new file mode 100644 index 000000000..c9d248d61 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/SplunkProviderAuditLogStreamForm.tsx @@ -0,0 +1,120 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, Input, ModalClose, SecretInput } from "@app/components/v2"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TSplunkProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/splunk-provider"; + +type Props = { + auditLogStream?: TSplunkProviderLogStream; + onSubmit: (formData: FormData) => void; +}; + +const formSchema = z.object({ + provider: z.literal(LogProvider.Splunk), + credentials: z.object({ + hostname: z + .string() + .trim() + .min(1) + .max(255) + .superRefine((val, ctx) => { + if (val.includes("://")) { + ctx.addIssue({ + code: "custom", + message: "Hostname should not include protocol" + }); + return; + } + + try { + const url = new URL(`https://${val}`); + if (url.hostname !== val) { + ctx.addIssue({ + code: "custom", + message: "Must be a valid hostname without port or path" + }); + } + } catch { + ctx.addIssue({ code: "custom", message: "Invalid hostname" }); + } + }), + token: z.string().uuid().trim().min(1) + }) +}); + +type FormData = z.infer; + +export const SplunkProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => { + const isUpdate = Boolean(auditLogStream); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: auditLogStream ?? { + provider: LogProvider.Splunk + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamTab.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamTab.tsx index beb0f63db..b58d1a6eb 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamTab.tsx @@ -1,72 +1,24 @@ -import { faPlug, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; -import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; -import { - Button, - DeleteActionModal, - EmptyState, - Modal, - ModalContent, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - THead, - Tr -} from "@app/components/v2"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - useOrganization, - useSubscription -} from "@app/context"; +import { Button } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { withPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks"; -import { useDeleteAuditLogStream, useGetAuditLogStreams } from "@app/hooks/api"; -import { AuditLogStreamForm } from "./AuditLogStreamForm"; +import { AuditLogStreamTable } from "./components/AuditLogStreamTable"; +import { AddAuditLogStreamModal } from "./components"; export const AuditLogStreamsTab = withPermission( () => { - const { currentOrg } = useOrganization(); - const orgId = currentOrg?.id || ""; - const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ - "auditLogStreamForm", - "deleteAuditLogStream", - "upgradePlan" - ] as const); const { subscription } = useSubscription(); - const { data: auditLogStreams, isPending: isAuditLogStreamsLoading } = - useGetAuditLogStreams(orgId); - - // mutation - const { mutateAsync: deleteAuditLogStream } = useDeleteAuditLogStream(); - - const handleAuditLogStreamDelete = async () => { - try { - const auditLogStreamId = popUp?.deleteAuditLogStream?.data as string; - await deleteAuditLogStream({ - id: auditLogStreamId, - orgId - }); - handlePopUpClose("deleteAuditLogStream"); - createNotification({ - type: "success", - text: "Successfully deleted stream" - }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to delete stream" - }); - } - }; + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "auditLogStreamForm", + "upgradePlan" + ] as const); return (
@@ -84,8 +36,10 @@ export const AuditLogStreamsTab = withPermission( }} leftIcon={} isDisabled={!isAllowed} + variant="outline_bg" + colorSchema="secondary" > - Create + Add Log Stream )} @@ -93,102 +47,15 @@ export const AuditLogStreamsTab = withPermission(

Send audit logs from Infisical to external logging providers via HTTP

-
- - - - - - - - - - {isAuditLogStreamsLoading && ( - - )} - {!isAuditLogStreamsLoading && auditLogStreams && auditLogStreams?.length === 0 && ( - - - - )} - {!isAuditLogStreamsLoading && - auditLogStreams?.map(({ id, url }) => ( - - - - - ))} - -
URLAction
- -
- {url} - -
- - {(isAllowed) => ( - - )} - - - {(isAllowed) => ( - - )} - -
-
-
-
- + { - handlePopUpToggle("auditLogStreamForm", isModalOpen); - }} - > - - handlePopUpToggle("auditLogStreamForm")} - /> - - + onOpenChange={(isOpen) => handlePopUpToggle("auditLogStreamForm", isOpen)} + /> handlePopUpToggle("upgradePlan", isOpen)} - text="You can add audit log streams if you switch to Infisical's Enterprise plan." - /> - handlePopUpToggle("deleteAuditLogStream", isOpen)} - onClose={() => handlePopUpClose("deleteAuditLogStream")} - onDeleteApproved={handleAuditLogStreamDelete} + text="You can add audit log streams if you switch to Infisical's Enterprise plan." />
); diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AddAuditLogStreamModal.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AddAuditLogStreamModal.tsx new file mode 100644 index 000000000..5a6892a49 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AddAuditLogStreamModal.tsx @@ -0,0 +1,69 @@ +import { Dispatch, SetStateAction, useState } from "react"; + +import { Modal, ModalContent } from "@app/components/v2"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TAuditLogStream } from "@app/hooks/api/types"; + +import { AuditLogStreamForm } from "../AuditLogStreamForm/AuditLogStreamForm"; +import { LogStreamProviderSelect } from "./LogStreamProviderSelect"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +type ContentProps = { + onComplete: (auditLogStream: TAuditLogStream) => void; + selectedProvider: LogProvider | null; + setSelectedProvider: Dispatch>; +}; + +const Content = ({ onComplete, selectedProvider, setSelectedProvider }: ContentProps) => { + if (selectedProvider) { + return ( + setSelectedProvider(null)} + provider={selectedProvider} + /> + ); + } + + return ; +}; + +export const AddAuditLogStreamModal = ({ isOpen, onOpenChange }: Props) => { + const [selectedProvider, setSelectedProvider] = useState(null); + + return ( + { + onOpenChange(e); + if (!e) setSelectedProvider(null); + }} + > + + Select a log provider or{" "} + {" "} + to stream logs to. + + > + onOpenChange(false)} + selectedProvider={selectedProvider} + setSelectedProvider={setSelectedProvider} + /> + + + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamHeader.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamHeader.tsx new file mode 100644 index 000000000..3175ffa46 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamHeader.tsx @@ -0,0 +1,69 @@ +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; + +type Props = { + provider: LogProvider; + logStreamExists: boolean; + onBack?: () => void; +}; + +export const AuditLogStreamHeader = ({ provider, logStreamExists, onBack }: Props) => { + const providerDetails = AUDIT_LOG_STREAM_PROVIDER_MAP[provider]; + + return ( +
+
+ {providerDetails.image ? ( + {providerDetails.name} + ) : ( + providerDetails.icon && ( +
+ +
+ ) + )} +
+
+
+ {providerDetails.name} + +
+ + Docs + +
+
+
+

+ {logStreamExists + ? `${providerDetails.name} Log Stream` + : `Create a ${providerDetails.name} Log Stream`} +

+
+ {onBack && ( + + )} +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamRow.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamRow.tsx new file mode 100644 index 000000000..037ba6f2a --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamRow.tsx @@ -0,0 +1,111 @@ +import { faAsterisk, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + IconButton, + Td, + Tooltip, + Tr +} from "@app/components/v2"; +import { OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionActions } from "@app/context/OrgPermissionContext/types"; +import { AUDIT_LOG_STREAM_PROVIDER_MAP, getProviderUrl } from "@app/helpers/auditLogStreams"; +import { TAuditLogStream } from "@app/hooks/api/types"; + +type Props = { + logStream: TAuditLogStream; + onDelete: (logStream: TAuditLogStream) => void; + onEditCredentials: (logStream: TAuditLogStream) => void; +}; + +export const AuditLogStreamRow = ({ logStream, onDelete, onEditCredentials }: Props) => { + const { id, provider } = logStream; + + const providerDetails = AUDIT_LOG_STREAM_PROVIDER_MAP[provider]; + const url = getProviderUrl(logStream); + + return ( + + +
+
+ {providerDetails.image ? ( + {providerDetails.name} + ) : ( + providerDetails.icon && ( + + ) + )} +
+ {providerDetails.name} +
+ + +
+

{url}

+
+ + +
+ + + + + + + + + + {(isAllowed: boolean) => ( + } + onClick={() => onEditCredentials(logStream)} + > + Edit Credentials + + )} + + + {(isAllowed: boolean) => ( + } + onClick={() => onDelete(logStream)} + > + Delete Stream + + )} + + + + +
+ + + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamTable.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamTable.tsx new file mode 100644 index 000000000..152f1d1c7 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamTable.tsx @@ -0,0 +1,304 @@ +import { useMemo, useState } from "react"; +import { + faArrowDown, + faArrowUp, + faFilter, + faMagnifyingGlass, + faPlug, + faSearch +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + EmptyState, + IconButton, + Input, + Pagination, + Table, + TableContainer, + TableSkeleton, + TBody, + Th, + THead, + Tr +} from "@app/components/v2"; +import { AUDIT_LOG_STREAM_PROVIDER_MAP, getProviderUrl } from "@app/helpers/auditLogStreams"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; +import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; +import { useListAuditLogStreams } from "@app/hooks/api"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { TAuditLogStream } from "@app/hooks/api/types"; + +import { AuditLogStreamRow } from "./AuditLogStreamRow"; +import { DeleteAuditLogStreamModal } from "./DeleteAuditLogStreamModal"; +import { EditAuditLogStreamCredentialsModal } from "./EditAuditLogStreamCredentialsModal"; + +enum LogStreamsOrderBy { + Provider = "provider", + Url = "url" +} + +type LogStreamFilters = { + providers: LogProvider[]; +}; + +export const AuditLogStreamTable = () => { + const { isPending, data: logStreams = [] } = useListAuditLogStreams(); + + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "delete", + "editCredentials" + ] as const); + + const [filters, setFilters] = useState({ + providers: [] + }); + + const { + search, + setSearch, + setPage, + page, + perPage, + setPerPage, + offset, + orderDirection, + toggleOrderDirection, + orderBy, + setOrderDirection, + setOrderBy + } = usePagination(LogStreamsOrderBy.Provider, { + initPerPage: getUserTablePreference("logStreamsTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("logStreamsTable", PreferenceKey.PerPage, newPerPage); + }; + + const filteredLogStreams = useMemo( + () => + logStreams + .filter((stream) => { + const { provider } = stream; + + if (filters.providers.length && !filters.providers.includes(provider)) return false; + + const searchValue = search.trim().toLowerCase(); + + return AUDIT_LOG_STREAM_PROVIDER_MAP[provider].name.toLowerCase().includes(searchValue); + }) + .sort((a, b) => { + const [one, two] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; + + switch (orderBy) { + case LogStreamsOrderBy.Url: + return getProviderUrl(one) + .toLowerCase() + .localeCompare(getProviderUrl(two).toLowerCase()); + case LogStreamsOrderBy.Provider: + default: + return AUDIT_LOG_STREAM_PROVIDER_MAP[one.provider].name + .toLowerCase() + .localeCompare(AUDIT_LOG_STREAM_PROVIDER_MAP[two.provider].name.toLowerCase()); + } + }), + [logStreams, orderDirection, search, orderBy, filters] + ); + + useResetPageHelper({ + totalCount: filteredLogStreams.length, + offset, + setPage + }); + + const handleSort = (column: LogStreamsOrderBy) => { + if (column === orderBy) { + toggleOrderDirection(); + return; + } + + setOrderBy(column); + setOrderDirection(OrderByDirection.ASC); + }; + + const getClassName = (col: LogStreamsOrderBy) => + twMerge("ml-2", orderBy === col ? "" : "opacity-30"); + + const getColSortIcon = (col: LogStreamsOrderBy) => + orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown; + + const isTableFiltered = Boolean(filters.providers.length); + + const handleDelete = (logStream: TAuditLogStream) => handlePopUpOpen("delete", logStream); + + const handleEditCredentials = (logStream: TAuditLogStream) => { + handlePopUpOpen("editCredentials", logStream); + }; + + return ( +
+
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search audit log streams..." + className="flex-1" + /> + + + + + + + + Filter by Provider + {logStreams.length ? ( + [...new Set(logStreams.map(({ provider }) => provider))].map((provider) => { + const providerDetails = AUDIT_LOG_STREAM_PROVIDER_MAP[provider]; + + return ( + { + e.preventDefault(); + setFilters((prev) => ({ + ...prev, + providers: prev.providers.includes(provider) + ? prev.providers.filter((a) => a !== provider) + : [...prev.providers, provider] + })); + }} + key={provider} + iconPos="right" + > +
+ {providerDetails.image ? ( + {providerDetails.name} + ) : ( + providerDetails.icon && ( + + ) + )} + {providerDetails.name} +
+
+ ); + }) + ) : ( + No Providers Configured + )} +
+
+
+ + + + + + + + + + + {isPending && ( + + )} + {filteredLogStreams.slice(offset, perPage * page).map((stream) => ( + + ))} + +
+
+ Provider + handleSort(LogStreamsOrderBy.Provider)} + > + + +
+
+
+ Endpoint URL + handleSort(LogStreamsOrderBy.Url)} + > + + +
+
+
+ {Boolean(filteredLogStreams.length) && ( + + )} + {!isPending && !filteredLogStreams?.length && ( + + )} +
+ handlePopUpToggle("delete", isOpen)} + auditLogStream={popUp.delete.data} + /> + handlePopUpToggle("editCredentials", isOpen)} + auditLogStream={popUp.editCredentials.data} + /> +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/DeleteAuditLogStreamModal.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/DeleteAuditLogStreamModal.tsx new file mode 100644 index 000000000..1a65e6c67 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/DeleteAuditLogStreamModal.tsx @@ -0,0 +1,54 @@ +import { createNotification } from "@app/components/notifications"; +import { DeleteActionModal } from "@app/components/v2"; +import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams"; +import { useDeleteAuditLogStream } from "@app/hooks/api"; +import { TAuditLogStream } from "@app/hooks/api/types"; + +type Props = { + auditLogStream?: TAuditLogStream; + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +export const DeleteAuditLogStreamModal = ({ isOpen, onOpenChange, auditLogStream }: Props) => { + const deleteAuditLogStream = useDeleteAuditLogStream(); + + if (!auditLogStream) return null; + + const { id: auditLogStreamId, provider } = auditLogStream; + + const providerDetails = AUDIT_LOG_STREAM_PROVIDER_MAP[provider]; + + const handleDelete = async () => { + try { + await deleteAuditLogStream.mutateAsync({ + auditLogStreamId, + provider + }); + + createNotification({ + text: `Successfully deleted ${providerDetails.name} stream`, + type: "success" + }); + + onOpenChange(false); + } catch (err) { + console.error(err); + + createNotification({ + text: `Failed to delete ${providerDetails.name} stream`, + type: "error" + }); + } + }; + + return ( + + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/EditAuditLogStreamCredentialsModal.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/EditAuditLogStreamCredentialsModal.tsx new file mode 100644 index 000000000..6e21b10fe --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/EditAuditLogStreamCredentialsModal.tsx @@ -0,0 +1,34 @@ +import { Modal, ModalContent } from "@app/components/v2"; +import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams"; +import { TAuditLogStream } from "@app/hooks/api/types"; + +import { AuditLogStreamForm } from "../AuditLogStreamForm/AuditLogStreamForm"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + auditLogStream?: TAuditLogStream; +}; + +export const EditAuditLogStreamCredentialsModal = ({ + isOpen, + onOpenChange, + auditLogStream +}: Props) => { + if (!auditLogStream) return null; + + return ( + + + onOpenChange(false)} + auditLogStream={auditLogStream} + /> + + + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/LogStreamProviderSelect.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/LogStreamProviderSelect.tsx new file mode 100644 index 000000000..26708e17b --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/LogStreamProviderSelect.tsx @@ -0,0 +1,155 @@ +import { useMemo } from "react"; +import { faSearch } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { EmptyState, Spinner } from "@app/components/v2"; +import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams"; +import { usePagination, useResetPageHelper } from "@app/hooks"; +import { useGetAuditLogStreamOptions } from "@app/hooks/api"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; + +type Props = { + onSelect: (provider: LogProvider) => void; +}; + +// TODO: When we have more than 1 page of providers, uncomment the search components + +export const LogStreamProviderSelect = ({ onSelect }: Props) => { + const { isPending, data: logStreamOptions } = useGetAuditLogStreamOptions(); + + const { search, setPage, page, perPage, offset } = usePagination("", { + initPerPage: 16 + }); + + const filteredOptions = useMemo( + () => + (logStreamOptions || []) + .filter( + ({ name, provider }) => + name.toLowerCase().includes(search.trim().toLowerCase()) || + provider.toLowerCase().includes(search.trim().toLowerCase()) + ) + .sort((a, b) => { + if (a.provider === LogProvider.Custom) return 1; + if (b.provider === LogProvider.Custom) return -1; + return 0; + }), + [logStreamOptions, search] + ); + + useResetPageHelper({ + totalCount: filteredOptions.length, + offset, + setPage + }); + + if (isPending) { + return ( +
+ +

Loading options...

+
+ ); + } + + return ( +
+ {/* setSearch(e.target.value)} + leftIcon={} + placeholder="Search options..." + className="bg-mineshaft-800 placeholder:text-mineshaft-400" + /> */} +
+ {filteredOptions.slice(offset, perPage * page)?.map((option) => { + const { image, icon, name, size = 50 } = AUDIT_LOG_STREAM_PROVIDER_MAP[option.provider]; + + return ( + + ); + })} + {!filteredOptions.length && ( + + )} +
+ {/* {Boolean(filteredOptions.length) && ( + +

Infisical is constantly adding support for more providers.

+

+ {`If you don't see the third-party + provider you're looking for,`}{" "} + + let us know on Slack + {" "} + or{" "} + + make a request on GitHub + + . +

+ + } + > +
+ + Don't see the third-party provider you're looking for? + + +
+ + } + count={filteredOptions.length} + page={page} + perPage={perPage} + onChangePage={setPage} + onChangePerPage={setPerPage} + perPageList={[16]} + /> + )} */} +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/index.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/index.tsx new file mode 100644 index 000000000..69febb4a9 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/index.tsx @@ -0,0 +1 @@ +export * from "./AddAuditLogStreamModal"; diff --git a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/ExternalMigrationsTab.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/ExternalMigrationsTab.tsx index 6ca1a5c28..8c6d511b1 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/ExternalMigrationsTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/ExternalMigrationsTab.tsx @@ -1,9 +1,8 @@ import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { OrgPermissionCan } from "@app/components/permissions"; import { Button } from "@app/components/v2"; -import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context"; +import { useOrgPermission } from "@app/context"; import { usePopUp } from "@app/hooks"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; @@ -15,52 +14,45 @@ export const ExternalMigrationsTab = () => { const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["selectImportPlatform"] as const); return ( - -
-
-
-

Import from external source

+
+
+
+

Import from external source

- + - -
-

Import data from another platform to Infisical.

- handlePopUpToggle("selectImportPlatform", state)} - /> +
- +

Import data from another platform to Infisical.

+ + handlePopUpToggle("selectImportPlatform", state)} + /> +
); }; diff --git a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultPlatformModal.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultPlatformModal.tsx index 137e765d2..e6f142644 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultPlatformModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultPlatformModal.tsx @@ -15,7 +15,9 @@ import { OrgPermissionSubjects } from "@app/context/OrgPermissionContext/types"; import { gatewaysQueryKeys } from "@app/hooks/api"; +import { useHasCustomMigrationAvailable } from "@app/hooks/api/migration"; import { useImportVault } from "@app/hooks/api/migration/mutations"; +import { ExternalMigrationProviders } from "@app/hooks/api/migration/types"; type Props = { id?: string; @@ -24,11 +26,13 @@ type Props = { enum VaultMappingType { KeyVault = "key-vault", - Namespace = "namespace" + Namespace = "namespace", + Custom = "custom" } const MAPPING_TYPE_MENU_ITEMS = [ { + isCustom: false, value: VaultMappingType.KeyVault, label: "Key Vaults", tooltip: ( @@ -48,6 +52,7 @@ const MAPPING_TYPE_MENU_ITEMS = [ ) }, { + isCustom: false, value: VaultMappingType.Namespace, label: "Namespaces", tooltip: ( @@ -63,10 +68,25 @@ const MAPPING_TYPE_MENU_ITEMS = [
) + }, + { + isCustom: true, + value: VaultMappingType.Custom, + label: "Custom Migration", + tooltip: ( +
+ Custom migrations allow you to shape your Vault migration to your specific needs. Please + contact our sales team to get started with custom migrations. +
+ ) } ]; export const VaultPlatformModal = ({ onClose }: Props) => { + const { data: isCustomMigrationAvailable } = useHasCustomMigrationAvailable( + ExternalMigrationProviders.Vault + ); + const formSchema = z.object({ vaultUrl: z.string().min(1), gatewayId: z.string().optional(), @@ -230,31 +250,44 @@ export const VaultPlatformModal = ({ onClose }: Props) => { errorText={error?.message} className="flex-1" > -
+
{MAPPING_TYPE_MENU_ITEMS.map((el) => (
field.onChange(el.value)} + onClick={() => { + if (el.isCustom && !isCustomMigrationAvailable?.data?.enabled) return; + + field.onChange(el.value); + }} + onKeyDown={(e) => { + if (e.key !== "Enter") return; + if (el.isCustom && !isCustomMigrationAvailable?.data?.enabled) return; + + field.onChange(el.value); + }} role="button" tabIndex={0} - onKeyDown={(e) => { - if (e.key === "Enter") { - field.onChange(el.value); - } - }} >
{el.label}
{el.tooltip && (
- +
)} @@ -272,7 +305,7 @@ export const VaultPlatformModal = ({ onClose }: Props) => { isLoading={isLoading} isDisabled={!isDirty || isSubmitting || isLoading || !isValid} > - Import data + Import Data - +
diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx index 1fa98628e..eb8653acb 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx @@ -23,7 +23,7 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { formatProjectRoleName } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; import { useUpdateIdentityWorkspaceRole } from "@app/hooks/api"; @@ -41,7 +41,7 @@ export const IdentityRoleDetailsSection = ({ identityMembershipDetails, isMembershipDetailsLoading }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ "deleteRole", "modifyRole" @@ -53,7 +53,7 @@ export const IdentityRoleDetailsSection = ({ try { const updatedRoles = identityMembershipDetails?.roles?.filter((el) => el.id !== id); await updateIdentityWorkspaceRole({ - workspaceId: currentWorkspace?.id || "", + projectId: currentProject?.id || "", identityId: identityMembershipDetails.identity.id, roles: updatedRoles.map( ({ diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleModify.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleModify.tsx index c5860ec11..d47f4a74e 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleModify.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleModify.tsx @@ -29,13 +29,13 @@ import { ProjectPermissionActions, ProjectPermissionIdentityActions, ProjectPermissionSub, - useProjectPermission, - useWorkspace + useProject, + useProjectPermission } from "@app/context"; import { useGetProjectRoles, useUpdateIdentityWorkspaceRole } from "@app/hooks/api"; import { IdentityMembership } from "@app/hooks/api/identities/types"; +import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/projects/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; -import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types"; const roleFormSchema = z.object({ roles: z @@ -62,9 +62,8 @@ type Props = { }; export const IdentityRoleModify = ({ identityProjectMembership }: Props) => { - const { currentWorkspace } = useWorkspace(); - const workspaceId = currentWorkspace?.id || ""; - const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles(workspaceId); + const { projectId } = useProject(); + const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles(projectId); const { permission } = useProjectPermission(); const isIdentityEditDisabled = permission.cannot( ProjectPermissionIdentityActions.Edit, @@ -117,7 +116,7 @@ export const IdentityRoleModify = ({ identityProjectMembership }: Props) => { try { await updateIdentityWorkspaceRole.mutateAsync({ - workspaceId, + projectId, identityId: identityProjectMembership.identity.id, roles: sanitizedRoles }); diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx index 7fadf2cb9..23b8299da 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx @@ -19,7 +19,7 @@ import { ProjectPermissionMemberActions, ProjectPermissionSub, useOrganization, - useWorkspace + useProject } from "@app/context"; import { getProjectBaseURL, getProjectHomePage } from "@app/helpers/project"; import { usePopUp } from "@app/hooks"; @@ -40,12 +40,10 @@ export const Page = () => { select: (el) => el.membershipId as string }); const { currentOrg } = useOrganization(); - const { currentWorkspace } = useWorkspace(); - - const workspaceId = currentWorkspace?.id || ""; + const { currentProject, projectId } = useProject(); const { data: membershipDetails, isPending: isMembershipDetailsLoading } = - useGetWorkspaceUserDetails(workspaceId, membershipId); + useGetWorkspaceUserDetails(projectId, membershipId); const { mutateAsync: removeUserFromWorkspace, isPending: isRemovingUserFromWorkspace } = useDeleteUserFromWorkspace(); @@ -63,7 +61,7 @@ export const Page = () => { { actorId: userId, actorType: ActorType.USER, - projectId: workspaceId + projectId }, { onSuccess: () => { @@ -72,19 +70,19 @@ export const Page = () => { text: "User privilege assumption has started" }); - const url = getProjectHomePage(currentWorkspace.type, currentWorkspace.environments); - window.location.href = url.replace("$projectId", currentWorkspace.id); + const url = getProjectHomePage(currentProject.type, currentProject.environments); + window.location.href = url.replace("$projectId", currentProject.id); } } ); }; const handleRemoveUser = async () => { - if (!currentOrg?.id || !currentWorkspace?.id || !membershipDetails?.user?.username) return; + if (!currentOrg?.id || !currentProject?.id || !membershipDetails?.user?.username) return; try { await removeUserFromWorkspace({ - workspaceId: currentWorkspace.id, + projectId, usernames: [membershipDetails?.user?.username], orgId: currentOrg.id }); @@ -93,9 +91,9 @@ export const Page = () => { type: "success" }); navigate({ - to: `${getProjectBaseURL(currentWorkspace.type)}/access-management` as const, + to: `${getProjectBaseURL(currentProject.type)}/access-management` as const, params: { - projectId: currentWorkspace.id + projectId: currentProject.id } }); } catch (error) { diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx index 9817aefdb..6fa54247f 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx @@ -23,8 +23,8 @@ import { import { ProjectPermissionMemberActions, ProjectPermissionSub, - useProjectPermission, - useWorkspace + useProject, + useProjectPermission } from "@app/context"; import { useCreateProjectUserAdditionalPrivilege, @@ -77,8 +77,8 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ isDisabled }: Props) => { const isCreate = !privilegeId; - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { data: privilegeDetails, isPending } = useGetProjectUserPrivilegeDetails( privilegeId || "" ); @@ -221,7 +221,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ > Save - +
diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx index 5ff78360d..012141c03 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx @@ -22,12 +22,7 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { - ProjectPermissionActions, - ProjectPermissionSub, - useUser, - useWorkspace -} from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject, useUser } from "@app/context"; import { formatProjectRoleName } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; import { useUpdateUserWorkspaceRole } from "@app/hooks/api"; @@ -49,7 +44,7 @@ export const MemberRoleDetailsSection = ({ }: Props) => { const { user } = useUser(); const userId = user?.id; - const { currentWorkspace } = useWorkspace(); + const { projectId } = useProject(); const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ "deleteRole", "modifyRole" @@ -63,7 +58,7 @@ export const MemberRoleDetailsSection = ({ try { const updatedRoles = membershipDetails?.roles?.filter((el) => el.id !== id); await updateUserWorkspaceRole({ - workspaceId: currentWorkspace?.id || "", + projectId, roles: updatedRoles.map( ({ role, diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleModify.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleModify.tsx index 739b22b7d..e7a05f70f 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleModify.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleModify.tsx @@ -29,14 +29,14 @@ import { ProjectPermissionActions, ProjectPermissionMemberActions, ProjectPermissionSub, + useProject, useProjectPermission, - useSubscription, - useWorkspace + useSubscription } from "@app/context"; import { useGetProjectRoles, useUpdateUserWorkspaceRole } from "@app/hooks/api"; +import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/projects/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { TWorkspaceUser } from "@app/hooks/api/types"; -import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types"; const roleFormSchema = z.object({ roles: z @@ -65,9 +65,8 @@ type Props = { export const MemberRoleModify = ({ projectMember, onOpenUpgradeModal }: Props) => { const { subscription } = useSubscription(); - const { currentWorkspace } = useWorkspace(); - const workspaceId = currentWorkspace?.id || ""; - const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles(workspaceId); + const { projectId } = useProject(); + const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles(projectId); const { permission } = useProjectPermission(); const isMemberEditDisabled = permission.cannot( ProjectPermissionMemberActions.Edit, @@ -131,7 +130,7 @@ export const MemberRoleModify = ({ projectMember, onOpenUpgradeModal }: Props) = try { await updateMembershipRole.mutateAsync({ - workspaceId, + projectId, membershipId: projectMember.id, roles: sanitizedRoles }); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx index e400767a1..775244b4d 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx @@ -16,7 +16,7 @@ import { DropdownMenuTrigger, PageHeader } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { getProjectBaseURL } from "@app/helpers/project"; import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; @@ -33,8 +33,8 @@ const Page = () => { strict: false, select: (el) => el.roleSlug as string }); - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { data } = useGetProjectRoleBySlug(projectId, roleSlug as string); @@ -48,7 +48,7 @@ const Page = () => { const onDeleteRoleSubmit = async () => { try { - if (!currentWorkspace?.slug || !data?.id) return; + if (!currentProject?.slug || !data?.id) return; await deleteProjectRole({ projectId, @@ -61,7 +61,7 @@ const Page = () => { }); handlePopUpClose("deleteRole"); navigate({ - to: `${getProjectBaseURL(currentWorkspace.type)}/access-management` as const, + to: `${getProjectBaseURL(currentProject.type)}/access-management` as const, params: { projectId }, diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx index abdf5c5d5..37f6b3868 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx @@ -9,7 +9,7 @@ import { IconButton } from "@app/components/v2"; import { usePopUp } from "@app/hooks"; -import { ProjectType } from "@app/hooks/api/workspace/types"; +import { ProjectType } from "@app/hooks/api/projects/types"; import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; import { PolicyTemplateModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal"; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/AppConnectionPermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/AppConnectionPermissionConditions.tsx new file mode 100644 index 000000000..2991d895c --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/AppConnectionPermissionConditions.tsx @@ -0,0 +1,19 @@ +import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; + +import { ConditionsFields } from "./ConditionsFields"; + +type Props = { + position?: number; + isDisabled?: boolean; +}; + +export const AppConnectionPermissionConditions = ({ position = 0, isDisabled }: Props) => { + return ( + + ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx index 24974225e..d36562089 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx @@ -5,7 +5,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalContent, Spinner } from "@app/components/v2"; -import { ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionSub, useProject } from "@app/context"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { getProjectBaseURL } from "@app/helpers/project"; import { useCreateProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; @@ -45,7 +45,7 @@ const Content = ({ role, onClose }: ContentProps) => { resolver: zodResolver(schema) }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const createRole = useCreateProjectRole(); const navigate = useNavigate(); @@ -70,7 +70,7 @@ const Content = ({ role, onClose }: ContentProps) => { }); const newRole = await createRole.mutateAsync({ - projectId: currentWorkspace.id, + projectId: currentProject.id, permissions: sanitizedPermission, ...form }); @@ -81,10 +81,10 @@ const Content = ({ role, onClose }: ContentProps) => { }); navigate({ - to: `${getProjectBaseURL(currentWorkspace.type)}/roles/$roleSlug` as const, + to: `${getProjectBaseURL(currentProject.type)}/roles/$roleSlug` as const, params: { roleSlug: newRole.slug, - projectId: currentWorkspace.id + projectId: currentProject.id } }); @@ -142,9 +142,9 @@ const Content = ({ role, onClose }: ContentProps) => { }; export const DuplicateProjectRoleModal = ({ isOpen, onOpenChange, roleSlug }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); - const { data: role, isPending } = useGetProjectRoleBySlug(currentWorkspace.id, roleSlug ?? ""); + const { data: role, isPending } = useGetProjectRoleBySlug(currentProject.id, roleSlug ?? ""); if (!roleSlug) return null; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx index a39ae8d64..f6c44b386 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx @@ -23,6 +23,7 @@ export const renderOperatorSelectItems = (type: string) => { case "secretTags": return Contains; case "identityId": + case "connectionId": return ( <> Equal diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx index 845c5f54e..22e64d48e 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx @@ -19,7 +19,7 @@ import { Tr } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; -import { ProjectType } from "@app/hooks/api/workspace/types"; +import { ProjectType } from "@app/hooks/api/projects/types"; import { EXCLUDED_PERMISSION_SUBS, diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal.tsx index d9cae6244..3064311bc 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal.tsx @@ -13,7 +13,7 @@ import { ModalContent } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; -import { ProjectType } from "@app/hooks/api/workspace/types"; +import { ProjectType } from "@app/hooks/api/projects/types"; import { PROJECT_PERMISSION_OBJECT, diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 856275374..89c1e5d86 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -12,6 +12,7 @@ import { } from "@app/context"; import { PermissionConditionOperators, + ProjectPermissionAppConnectionActions, ProjectPermissionAuditLogsActions, ProjectPermissionCommitsActions, ProjectPermissionDynamicSecretActions, @@ -32,8 +33,8 @@ import { TPermissionCondition, TPermissionConditionOperators } from "@app/context/ProjectPermissionContext/types"; +import { ProjectType } from "@app/hooks/api/projects/types"; import { TProjectPermission } from "@app/hooks/api/roles/types"; -import { ProjectType } from "@app/hooks/api/workspace/types"; const GeneralPolicyActionSchema = z.object({ read: z.boolean().optional(), @@ -134,6 +135,14 @@ const SecretScanningConfigPolicyActionSchema = z.object({ [ProjectPermissionSecretScanningConfigActions.Update]: z.boolean().optional() }); +const AppConnectionPolicyActionSchema = z.object({ + [ProjectPermissionAppConnectionActions.Create]: z.boolean().optional(), + [ProjectPermissionAppConnectionActions.Read]: z.boolean().optional(), + [ProjectPermissionAppConnectionActions.Edit]: z.boolean().optional(), + [ProjectPermissionAppConnectionActions.Delete]: z.boolean().optional(), + [ProjectPermissionAppConnectionActions.Connect]: z.boolean().optional() +}); + const KmipPolicyActionSchema = z.object({ [ProjectPermissionKmipActions.ReadClients]: z.boolean().optional(), [ProjectPermissionKmipActions.CreateClients]: z.boolean().optional(), @@ -311,6 +320,12 @@ export const projectRoleFormSchema = z.object({ }) .array() .default([]), + [ProjectPermissionSub.AppConnections]: AppConnectionPolicyActionSchema.extend({ + inverted: z.boolean().optional(), + conditions: ConditionSchema + }) + .array() + .default([]), [ProjectPermissionSub.Commits]: CommitPolicyActionSchema.array().default([]), [ProjectPermissionSub.Member]: MemberPolicyActionSchema.array().default([]), @@ -393,7 +408,8 @@ type TConditionalFields = | ProjectPermissionSub.SecretRotation | ProjectPermissionSub.Identity | ProjectPermissionSub.SecretSyncs - | ProjectPermissionSub.SecretEvents; + | ProjectPermissionSub.SecretEvents + | ProjectPermissionSub.AppConnections; export const isConditionalSubjects = ( subject: ProjectPermissionSub @@ -408,7 +424,8 @@ export const isConditionalSubjects = ( subject === ProjectPermissionSub.PkiSubscribers || subject === ProjectPermissionSub.CertificateTemplates || subject === ProjectPermissionSub.SecretSyncs || - subject === ProjectPermissionSub.SecretEvents; + subject === ProjectPermissionSub.SecretEvents || + subject === ProjectPermissionSub.AppConnections; const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => { const formConditions: z.infer = []; @@ -515,7 +532,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { ProjectPermissionSub.SshCertificates, ProjectPermissionSub.SshHostGroups, ProjectPermissionSub.SecretSyncs, - ProjectPermissionSub.SecretEvents + ProjectPermissionSub.SecretEvents, + ProjectPermissionSub.AppConnections ].includes(subject) ) { // from above statement we are sure it won't be undefined @@ -654,6 +672,27 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { return; } + if (subject === ProjectPermissionSub.AppConnections) { + const canCreate = action.includes(ProjectPermissionAppConnectionActions.Create); + const canRead = action.includes(ProjectPermissionAppConnectionActions.Read); + const canEdit = action.includes(ProjectPermissionAppConnectionActions.Edit); + const canDelete = action.includes(ProjectPermissionAppConnectionActions.Delete); + const canConnect = action.includes(ProjectPermissionAppConnectionActions.Connect); + + // from above statement we are sure it won't be undefined + formVal[subject]!.push({ + [ProjectPermissionAppConnectionActions.Read]: canRead, + [ProjectPermissionAppConnectionActions.Create]: canCreate, + [ProjectPermissionAppConnectionActions.Edit]: canEdit, + [ProjectPermissionAppConnectionActions.Delete]: canDelete, + [ProjectPermissionAppConnectionActions.Connect]: canConnect, + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted + }); + + return; + } + // for other subjects const canRead = action.includes(ProjectPermissionActions.Read); const canEdit = action.includes(ProjectPermissionActions.Edit); @@ -1597,6 +1636,31 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { value: ProjectPermissionSecretEventActions.SubscribeImportMutations } ] + }, + [ProjectPermissionSub.AppConnections]: { + title: "App Connections", + actions: [ + { + label: "Read", + value: ProjectPermissionAppConnectionActions.Read + }, + { + label: "Create", + value: ProjectPermissionAppConnectionActions.Create + }, + { + label: "Update", + value: ProjectPermissionAppConnectionActions.Edit + }, + { + label: "Delete", + value: ProjectPermissionAppConnectionActions.Delete + }, + { + label: "Connect", + value: ProjectPermissionAppConnectionActions.Connect + } + ] } }; @@ -1669,7 +1733,8 @@ export const ProjectTypePermissionSubjects: Record< ...KmsPermissionSubjects(), ...CertificateManagerPermissionSubjects(), ...SshPermissionSubjects(), - ...SecretScanningSubject() + ...SecretScanningSubject(), + [ProjectPermissionSub.AppConnections]: true }, [ProjectType.KMS]: { ...SharedPermissionSubjects, @@ -1677,7 +1742,8 @@ export const ProjectTypePermissionSubjects: Record< ...SecretsManagerPermissionSubjects(), ...CertificateManagerPermissionSubjects(), ...SshPermissionSubjects(), - ...SecretScanningSubject() + ...SecretScanningSubject(), + [ProjectPermissionSub.AppConnections]: false }, [ProjectType.CertificateManager]: { ...SharedPermissionSubjects, @@ -1685,7 +1751,8 @@ export const ProjectTypePermissionSubjects: Record< ...KmsPermissionSubjects(), ...SecretsManagerPermissionSubjects(), ...SshPermissionSubjects(), - ...SecretScanningSubject() + ...SecretScanningSubject(), + [ProjectPermissionSub.AppConnections]: true }, [ProjectType.SSH]: { ...SharedPermissionSubjects, @@ -1693,7 +1760,8 @@ export const ProjectTypePermissionSubjects: Record< ...CertificateManagerPermissionSubjects(), ...KmsPermissionSubjects(), ...SecretsManagerPermissionSubjects(), - ...SecretScanningSubject() + ...SecretScanningSubject(), + [ProjectPermissionSub.AppConnections]: false }, [ProjectType.SecretScanning]: { ...SharedPermissionSubjects, @@ -1701,7 +1769,8 @@ export const ProjectTypePermissionSubjects: Record< ...SshPermissionSubjects(), ...CertificateManagerPermissionSubjects(), ...KmsPermissionSubjects(), - ...SecretsManagerPermissionSubjects() + ...SecretsManagerPermissionSubjects(), + [ProjectPermissionSub.AppConnections]: true } }; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx index eb7a2ba83..e34f86892 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { getProjectBaseURL } from "@app/helpers/project"; import { useCreateProjectRole, @@ -38,8 +38,8 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { roleSlug: string; }; - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { data: role } = useGetProjectRoleBySlug(projectId, popupData?.roleSlug ?? ""); @@ -101,7 +101,7 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { }); navigate({ - to: `${getProjectBaseURL(currentWorkspace.type)}/roles/$roleSlug` as const, + to: `${getProjectBaseURL(currentProject.type)}/roles/$roleSlug` as const, params: { roleSlug: newRole.slug, projectId diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index f50446a8e..347ebaf99 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -8,14 +8,15 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { createNotification } from "@app/components/notifications"; import { AccessTree } from "@app/components/permissions"; import { Button } from "@app/components/v2"; -import { ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionSub, useProject } from "@app/context"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { evaluatePermissionsAbility } from "@app/helpers/permissions"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; +import { ProjectType } from "@app/hooks/api/projects/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { AddPoliciesButton } from "./AddPoliciesButton"; +import { AppConnectionPermissionConditions } from "./AppConnectionPermissionConditions"; import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions"; import { GeneralPermissionConditions } from "./GeneralPermissionConditions"; import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies"; @@ -77,6 +78,10 @@ export const renderConditionalComponents = ( return ; } + if (subject === ProjectPermissionSub.AppConnections) { + return ; + } + return ; } @@ -84,10 +89,10 @@ export const renderConditionalComponents = ( }; export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { data: role, isPending } = useGetProjectRoleBySlug( - currentWorkspace?.id ?? "", + currentProject?.id ?? "", roleSlug as string ); @@ -126,7 +131,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { (role?.slug ?? "") as ProjectMembershipRole ); - const isSecretManagerProject = currentWorkspace.type === ProjectType.SecretManager; + const isSecretManagerProject = currentProject.type === ProjectType.SecretManager; const permissions = form.watch("permissions"); @@ -178,7 +183,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { Save
- +
)} diff --git a/frontend/src/pages/project/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx b/frontend/src/pages/project/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx index 7c65b1857..11f4f0270 100644 --- a/frontend/src/pages/project/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx +++ b/frontend/src/pages/project/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx @@ -5,10 +5,10 @@ import { z } from "zod"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input } from "@app/components/v2"; -import { useProjectPermission, useSubscription, useWorkspace } from "@app/context"; +import { useProject, useProjectPermission, useSubscription } from "@app/context"; import { usePopUp } from "@app/hooks"; +import { useUpdateWorkspaceAuditLogsRetention } from "@app/hooks/api/projects/queries"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; -import { useUpdateWorkspaceAuditLogsRetention } from "@app/hooks/api/workspace/queries"; const formSchema = z.object({ auditLogsRetentionDays: z.coerce.number().min(0) @@ -19,7 +19,7 @@ type TForm = z.infer; export const AuditLogsRetentionSection = () => { const { mutateAsync: updateAuditLogsRetention } = useUpdateWorkspaceAuditLogsRetention(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { membership } = useProjectPermission(); const { subscription } = useSubscription(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); @@ -32,11 +32,11 @@ export const AuditLogsRetentionSection = () => { resolver: zodResolver(formSchema), values: { auditLogsRetentionDays: - currentWorkspace?.auditLogsRetentionDays ?? subscription?.auditLogsRetentionDays ?? 0 + currentProject?.auditLogsRetentionDays ?? subscription?.auditLogsRetentionDays ?? 0 } }); - if (!currentWorkspace) return null; + if (!currentProject) return null; const handleAuditLogsRetentionSubmit = async ({ auditLogsRetentionDays }: TForm) => { try { @@ -59,7 +59,7 @@ export const AuditLogsRetentionSection = () => { await updateAuditLogsRetention({ auditLogsRetentionDays, - projectSlug: currentWorkspace.slug + projectSlug: currentProject.slug }); createNotification({ diff --git a/frontend/src/pages/project/SettingsPage/components/DeleteProjectProtection/DeleteProjectProtection.tsx b/frontend/src/pages/project/SettingsPage/components/DeleteProjectProtection/DeleteProjectProtection.tsx index 2ac123607..efc69831d 100644 --- a/frontend/src/pages/project/SettingsPage/components/DeleteProjectProtection/DeleteProjectProtection.tsx +++ b/frontend/src/pages/project/SettingsPage/components/DeleteProjectProtection/DeleteProjectProtection.tsx @@ -1,20 +1,19 @@ import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Checkbox } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; -import { useToggleDeleteProjectProtection } from "@app/hooks/api/workspace/queries"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; +import { useUpdateProject } from "@app/hooks/api"; export const DeleteProjectProtection = () => { - const { currentWorkspace } = useWorkspace(); - const { mutateAsync } = useToggleDeleteProjectProtection(); + const { projectId, currentProject } = useProject(); + + const { mutateAsync } = useUpdateProject(); const handleToggleDeleteProjectProtection = async (state: boolean) => { try { - if (!currentWorkspace?.id) return; - await mutateAsync({ - workspaceID: currentWorkspace.id, - state + projectId, + hasDeleteProtection: state }); const text = `Successfully ${state ? "enabled" : "disabled"} delete protection`; @@ -40,7 +39,7 @@ export const DeleteProjectProtection = () => { { handleToggleDeleteProjectProtection(state as boolean); }} diff --git a/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx b/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx index ac589ba59..4771e0c88 100644 --- a/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx +++ b/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx @@ -9,8 +9,8 @@ import { ProjectPermissionActions, ProjectPermissionSub, useOrganization, - useProjectPermission, - useWorkspace + useProject, + useProjectPermission } from "@app/context"; import { useToggle } from "@app/hooks"; import { useDeleteWorkspace, useGetWorkspaceUsers, useLeaveProject } from "@app/hooks/api"; @@ -26,13 +26,13 @@ export const DeleteProjectSection = () => { const { currentOrg } = useOrganization(); const { hasProjectRole, membership } = useProjectPermission(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [isDeleting, setIsDeleting] = useToggle(); const [isLeaving, setIsLeaving] = useToggle(); const deleteWorkspace = useDeleteWorkspace(); const leaveProject = useLeaveProject(); const { data: members, isPending: isMembersLoading } = useGetWorkspaceUsers( - currentWorkspace?.id || "" + currentProject?.id || "" ); // If isNoAccessMember is true, then the user can't read the workspace members. So we need to handle this case separately. @@ -51,10 +51,10 @@ export const DeleteProjectSection = () => { const handleDeleteWorkspaceSubmit = async () => { setIsDeleting.on(); try { - if (!currentWorkspace?.id) return; + if (!currentProject?.id) return; await deleteWorkspace.mutateAsync({ - workspaceID: currentWorkspace?.id + projectID: currentProject?.id }); createNotification({ @@ -81,7 +81,7 @@ export const DeleteProjectSection = () => { try { setIsLeaving.on(); - if (!currentWorkspace?.id || !currentOrg?.id) return; + if (!currentProject?.id || !currentOrg?.id) return; // If there's no members, and the user has access to read members, something went wrong. if (!members && !isNoAccessMember) return; @@ -110,7 +110,7 @@ export const DeleteProjectSection = () => { // If it's actually a no-access member, then we don't really care about the members. await leaveProject.mutateAsync({ - workspaceId: currentWorkspace.id + projectId: currentProject.id }); navigate({ @@ -141,7 +141,7 @@ export const DeleteProjectSection = () => { type="submit" onClick={() => handlePopUpOpen("deleteWorkspace")} > - {`Delete ${currentWorkspace?.name}`} + {`Delete ${currentProject?.name}`} )} @@ -154,7 +154,7 @@ export const DeleteProjectSection = () => { type="submit" onClick={() => handlePopUpOpen("leaveWorkspace")} > - {`Leave ${currentWorkspace?.name}`} + {`Leave ${currentProject?.name}`} )}
@@ -162,7 +162,7 @@ export const DeleteProjectSection = () => { handlePopUpToggle("deleteWorkspace", isOpen)} deleteKey="confirm" buttonText="Delete Project" @@ -172,7 +172,7 @@ export const DeleteProjectSection = () => { handlePopUpToggle("leaveWorkspace", isOpen)} deleteKey="confirm" buttonText="Leave Project" diff --git a/frontend/src/pages/public/ErrorPage/ErrorPage.tsx b/frontend/src/pages/public/ErrorPage/ErrorPage.tsx index 1910eb891..ef6a837ae 100644 --- a/frontend/src/pages/public/ErrorPage/ErrorPage.tsx +++ b/frontend/src/pages/public/ErrorPage/ErrorPage.tsx @@ -5,7 +5,17 @@ import { AxiosError } from "axios"; import { Button } from "@app/components/v2"; +import { ProjectAccessError } from "./components"; + export const ErrorPage = ({ error }: ErrorComponentProps) => { + if ( + error instanceof AxiosError && + error.status === 403 && + error.response?.data?.error === "User not a part of the specified project" + ) { + return ; + } + return (
diff --git a/frontend/src/pages/public/ErrorPage/components/ProjectAccessError.tsx b/frontend/src/pages/public/ErrorPage/components/ProjectAccessError.tsx new file mode 100644 index 000000000..d6fd57d56 --- /dev/null +++ b/frontend/src/pages/public/ErrorPage/components/ProjectAccessError.tsx @@ -0,0 +1,109 @@ +import { faHome } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Link, useNavigate, useParams } from "@tanstack/react-router"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { RequestProjectAccessModal } from "@app/components/projects"; +import { AccessRestrictedBanner, Button } from "@app/components/v2"; +import { OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionAdminConsoleAction } from "@app/context/OrgPermissionContext/types"; +import { usePopUp } from "@app/hooks"; +import { useOrgAdminAccessProject, useSearchProjects } from "@app/hooks/api"; + +export const ProjectAccessError = () => { + const orgAdminAccessProject = useOrgAdminAccessProject(); + + const navigate = useNavigate(); + + const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([ + "requestAccessConfirmation" + ] as const); + + const { projectId } = useParams({ + strict: false + }); + + const { data, isPending: isProjectLoading } = useSearchProjects({ + projectIds: projectId ? [projectId] : [], + options: { + enabled: Boolean(projectId) + } + }); + + const [project] = data?.projects ?? []; + + const handleAccessProject = async () => { + if (!project) return; + try { + await orgAdminAccessProject.mutateAsync({ + projectId: project.id + }); + await navigate({ + to: "." + }); + } catch { + createNotification({ + text: "Failed to access project", + type: "error" + }); + } + }; + + return ( +
+ + You are not currently a member of this project. Request access to join project. +
+ + + + + {(isAllowed) => + isAllowed ? ( + + ) : ( + + ) + } + +
+ handlePopUpToggle("requestAccessConfirmation", isOpen)} + project={project} + onComplete={() => { + navigate({ + to: "/organization/projects" + }); + }} + /> + + } + /> +
+ ); +}; diff --git a/frontend/src/pages/public/ErrorPage/components/index.ts b/frontend/src/pages/public/ErrorPage/components/index.ts new file mode 100644 index 000000000..1940497e9 --- /dev/null +++ b/frontend/src/pages/public/ErrorPage/components/index.ts @@ -0,0 +1 @@ +export * from "./ProjectAccessError"; diff --git a/frontend/src/pages/secret-manager/CommitDetailsPage/CommitDetailsPage.tsx b/frontend/src/pages/secret-manager/CommitDetailsPage/CommitDetailsPage.tsx index 06f79e9f0..facbd782b 100644 --- a/frontend/src/pages/secret-manager/CommitDetailsPage/CommitDetailsPage.tsx +++ b/frontend/src/pages/secret-manager/CommitDetailsPage/CommitDetailsPage.tsx @@ -2,7 +2,7 @@ import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; import { ProjectPermissionCan } from "@app/components/permissions"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { ProjectPermissionCommitsActions, ProjectPermissionSub @@ -23,7 +23,7 @@ export const CommitDetailsPage = () => { from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id, select: (el) => el.folderId }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const navigate = useNavigate(); const routerQueryParams: { secretPath?: string } = useSearch({ @@ -36,7 +36,7 @@ export const CommitDetailsPage = () => { navigate({ to: "/projects/secret-management/$projectId/commits/$environment/$folderId", params: { - projectId: currentWorkspace.id, + projectId: currentProject.id, folderId, environment: envSlug }, @@ -51,7 +51,7 @@ export const CommitDetailsPage = () => { navigate({ to: "/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/restore", params: { - projectId: currentWorkspace.id, + projectId: currentProject.id, folderId, environment: envSlug, commitId: selectedCommitId @@ -73,7 +73,7 @@ export const CommitDetailsPage = () => { > { export const CommitDetailsTab = ({ selectedCommitId, - workspaceId, + projectId, envSlug, goBackToHistory, goToRollbackPreview }: { selectedCommitId: string; - workspaceId: string; + projectId: string; envSlug: string; goBackToHistory: () => void; goToRollbackPreview: () => void; @@ -71,7 +71,7 @@ export const CommitDetailsTab = ({ "revertChanges" ] as const); - const { data: commitDetails, isLoading } = useGetCommitDetails(workspaceId, selectedCommitId); + const { data: commitDetails, isLoading } = useGetCommitDetails(projectId, selectedCommitId); const routerQueryParams: { secretPath?: string } = useSearch({ from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id @@ -80,7 +80,7 @@ export const CommitDetailsTab = ({ const { mutateAsync: revert } = useCommitRevert({ commitId: selectedCommitId, - projectId: workspaceId, + projectId, environment: envSlug, directory: secretPath }); diff --git a/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/RollbackPreviewTab.tsx b/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/RollbackPreviewTab.tsx index e06daea38..ae082d8ea 100644 --- a/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/RollbackPreviewTab.tsx +++ b/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/RollbackPreviewTab.tsx @@ -17,7 +17,7 @@ import { Tooltip } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { ProjectPermissionCommitsActions, ProjectPermissionSub @@ -79,7 +79,7 @@ export const RollbackPreviewTab = (): JSX.Element => { const [deepRollback, setDeepRollback] = useState(false); const [message, setMessage] = useState(""); const [selectedFolderId, setSelectedFolderId] = useState(null); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const envSlug = useParams({ from: ROUTE_PATHS.SecretManager.RollbackPreviewPage.id, select: (el) => el.environment @@ -104,7 +104,7 @@ export const RollbackPreviewTab = (): JSX.Element => { navigate({ to: "/projects/secret-management/$projectId/commits/$environment/$folderId", params: { - projectId: currentWorkspace.id, + projectId: currentProject.id, folderId, environment: envSlug }, @@ -120,7 +120,7 @@ export const RollbackPreviewTab = (): JSX.Element => { ] as const); const { mutateAsync: rollback } = useCommitRollback({ - workspaceId: currentWorkspace.id, + projectId: currentProject.id, commitId: selectedCommitId, folderId, deepRollback, @@ -133,7 +133,7 @@ export const RollbackPreviewTab = (): JSX.Element => { folderId, selectedCommitId, envSlug, - currentWorkspace.id, + currentProject.id, deepRollback, secretPath ); diff --git a/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx b/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx index 1acb76446..2dba8ad55 100644 --- a/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx +++ b/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx @@ -4,7 +4,7 @@ import { ProjectPermissionCan } from "@app/components/permissions"; import { PageHeader } from "@app/components/v2"; import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { ProjectPermissionCommitsActions, ProjectPermissionSub @@ -17,7 +17,7 @@ export const CommitsPage = () => { from: ROUTE_PATHS.SecretManager.CommitsPage.id, select: (el) => el.environment }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const navigate = useNavigate(); const folderId = useParams({ from: ROUTE_PATHS.SecretManager.CommitsPage.id, @@ -33,7 +33,7 @@ export const CommitsPage = () => { navigate({ to: "/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId", params: { - projectId: currentWorkspace.id, + projectId: currentProject.id, folderId, environment: envSlug, commitId @@ -67,7 +67,7 @@ export const CommitsPage = () => { > diff --git a/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx b/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx index 58191f389..d5f982529 100644 --- a/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx +++ b/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx @@ -136,7 +136,7 @@ export const CommitHistoryTab = ({ isFetchingNextPage, hasNextPage } = useGetFolderCommitHistory({ - workspaceId: projectId, + projectId, environment, directory: secretPath, limit, diff --git a/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistModal.tsx b/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistModal.tsx index 4d95b6388..36f585850 100644 --- a/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistModal.tsx +++ b/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistModal.tsx @@ -5,7 +5,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useAddTrustedIp, useGetMyIp, useUpdateTrustedIp } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -27,7 +27,7 @@ type Props = { export const IPAllowlistModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => { const { data, isPending } = useGetMyIp(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const addTrustedIp = useAddTrustedIp(); const updateTrustedIp = useUpdateTrustedIp(); @@ -65,11 +65,11 @@ export const IPAllowlistModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: const onIPAllowlistModalSubmit = async ({ ipAddress, comment }: FormData) => { try { - if (!currentWorkspace?.id) return; + if (!currentProject?.id) return; if (popUp?.trustedIp?.data) { await updateTrustedIp.mutateAsync({ - workspaceId: currentWorkspace.id, + projectId: currentProject.id, trustedIpId: (popUp?.trustedIp?.data as { trustedIpId: string })?.trustedIpId, ipAddress, comment, @@ -77,7 +77,7 @@ export const IPAllowlistModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: }); } else { await addTrustedIp.mutateAsync({ - workspaceId: currentWorkspace.id, + projectId: currentProject.id, ipAddress, comment, isActive: true diff --git a/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistSection.tsx b/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistSection.tsx index 63b410ee2..fc2890b7f 100644 --- a/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistSection.tsx +++ b/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistSection.tsx @@ -8,8 +8,8 @@ import { Button, DeleteActionModal } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, - useSubscription, - useWorkspace + useProject, + useSubscription } from "@app/context"; import { useDeleteTrustedIp } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -20,7 +20,7 @@ import { IPAllowlistTable } from "./IPAllowlistTable"; export const IPAllowlistSection = () => { const { mutateAsync } = useDeleteTrustedIp(); const { subscription } = useSubscription(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "trustedIp", @@ -30,10 +30,10 @@ export const IPAllowlistSection = () => { const onDeleteTrustedIpSubmit = async (trustedIpId: string) => { try { - if (!currentWorkspace?.id) return; + if (!currentProject?.id) return; await mutateAsync({ - workspaceId: currentWorkspace.id, + projectId: currentProject.id, trustedIpId }); diff --git a/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistTable.tsx b/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistTable.tsx index 42ecc8d8a..8f3336be8 100644 --- a/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistTable.tsx +++ b/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistTable.tsx @@ -18,8 +18,8 @@ import { import { ProjectPermissionActions, ProjectPermissionSub, - useSubscription, - useWorkspace + useProject, + useSubscription } from "@app/context"; import { useGetTrustedIps } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -41,8 +41,8 @@ type Props = { export const IPAllowlistTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { const { subscription } = useSubscription(); - const { currentWorkspace } = useWorkspace(); - const { data, isPending } = useGetTrustedIps(currentWorkspace?.id ?? ""); + const { currentProject } = useProject(); + const { data, isPending } = useGetTrustedIps(currentProject?.id ?? ""); const formatType = (type: string, prefix?: number) => { return `${type.slice(0, 2).toUpperCase() + type.slice(2)} ${ @@ -77,10 +77,10 @@ export const IPAllowlistTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: {comment} {/*
- + />

Active

*/} diff --git a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx index c1ba83ae6..3788d955e 100644 --- a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx @@ -20,11 +20,11 @@ import { Tooltip } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { OrgPermissionActions, OrgPermissionSubjects, useWorkspace } from "@app/context"; +import { OrgPermissionActions, OrgPermissionSubjects, useProject } from "@app/context"; import { usePopUp, useToggle } from "@app/hooks"; import { useGetIntegration } from "@app/hooks/api"; import { useDeleteIntegration, useSyncIntegration } from "@app/hooks/api/integrations/queries"; -import { ProjectType } from "@app/hooks/api/workspace/types"; +import { ProjectType } from "@app/hooks/api/projects/types"; import { IntegrationAuditLogsSection } from "./components/IntegrationAuditLogsSection"; import { IntegrationConnectionSection } from "./components/IntegrationConnectionSection"; @@ -45,8 +45,8 @@ export const IntegrationDetailsByIDPage = () => { const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false); - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace.id; + const { currentProject } = useProject(); + const projectId = currentProject.id; const { mutateAsync: syncIntegration } = useSyncIntegration(); const { mutateAsync: deleteIntegration } = useDeleteIntegration(); @@ -56,7 +56,7 @@ export const IntegrationDetailsByIDPage = () => { try { await deleteIntegration({ id: integrationId, - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, shouldDeleteIntegrationSecrets }); diff --git a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationAuditLogsSection.tsx b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationAuditLogsSection.tsx index c27b1bb08..6a808814e 100644 --- a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationAuditLogsSection.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationAuditLogsSection.tsx @@ -1,7 +1,7 @@ import { Link } from "@tanstack/react-router"; import { EmptyState } from "@app/components/v2"; -import { useSubscription, useWorkspace } from "@app/context"; +import { useProject, useSubscription } from "@app/context"; import { EventType } from "@app/hooks/api/auditLogs/enums"; import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types"; import { LogsSection } from "@app/pages/organization/AuditLogsPage/components/LogsSection"; @@ -15,7 +15,7 @@ type Props = { export const IntegrationAuditLogsSection = ({ integration }: Props) => { const { subscription } = useSubscription(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const auditLogsRetentionDays = subscription?.auditLogsRetentionDays ?? 30; @@ -31,7 +31,7 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => { { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { t } = useTranslation(); const { selectedTab } = useSearch({ @@ -30,7 +30,7 @@ export const IntegrationsListPage = () => { to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path, search: (prev) => ({ ...prev, selectedTab: tab as IntegrationsListPageTabs }), params: { - projectId: currentWorkspace.id + projectId: currentProject.id } }); }; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx index 935ad1a8c..42f661606 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx @@ -24,8 +24,8 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionActions, ProjectPermissionSub, - useProjectPermission, - useWorkspace + useProject, + useProjectPermission } from "@app/context"; import { usePopUp } from "@app/hooks"; import { SecretSync } from "@app/hooks/api/secretSyncs"; @@ -60,7 +60,7 @@ export const CloudIntegrationSection = ({ "deleteConfirmation" ] as const); const { permission } = useProjectPermission(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const navigate = useNavigate(); const isEmpty = !isLoading && !cloudIntegrations?.length; @@ -68,12 +68,12 @@ export const CloudIntegrationSection = ({ const sortedCloudIntegrations = useMemo(() => { const sortedIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name)); - if (currentWorkspace?.environments.length === 0) { + if (currentProject?.environments.length === 0) { return sortedIntegrations.map((integration) => ({ ...integration, isAvailable: false })); } return sortedIntegrations; - }, [cloudIntegrations, currentWorkspace?.environments]); + }, [cloudIntegrations, currentProject?.environments]); const [search, setSearch] = useState(""); @@ -83,9 +83,9 @@ export const CloudIntegrationSection = ({ return (
- {currentWorkspace?.environments.length === 0 && ( + {currentProject?.environments.length === 0 && (
- +
)}
@@ -138,7 +138,7 @@ export const CloudIntegrationSection = ({ navigate({ to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path, params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.SecretSyncs, diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationRow.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationRow.tsx index df862b8ba..c83e24254 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationRow.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationRow.tsx @@ -15,7 +15,7 @@ import { twMerge } from "tailwind-merge"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Badge, IconButton, Td, Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { TCloudIntegration } from "@app/hooks/api/integrations/types"; import { TIntegration } from "@app/hooks/api/types"; @@ -37,7 +37,7 @@ export const IntegrationRow = ({ cloudIntegration }: IProps) => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { id, secretPath, syncMessage, isSynced } = integration; @@ -62,7 +62,7 @@ export const IntegrationRow = ({ to: "/projects/secret-management/$projectId/integrations/$integrationId", params: { integrationId: integration.id, - projectId: currentWorkspace.id + projectId: currentProject.id } }) } diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/NativeIntegrationsTab.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/NativeIntegrationsTab.tsx index f3706940c..b4f790075 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/NativeIntegrationsTab.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/NativeIntegrationsTab.tsx @@ -5,7 +5,7 @@ import { useNavigate } from "@tanstack/react-router"; import { createNotification } from "@app/components/notifications"; import { Button, Checkbox, DeleteActionModal, Spinner } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { usePopUp, useToggle } from "@app/hooks"; import { useDeleteIntegration, @@ -27,8 +27,8 @@ enum IntegrationView { } export const NativeIntegrationsTab = () => { - const { currentWorkspace } = useWorkspace(); - const { environments, id: workspaceId } = currentWorkspace; + const { currentProject } = useProject(); + const { environments, id: workspaceId } = currentProject; const navigate = useNavigate(); const { data: cloudIntegrations, isPending: isCloudIntegrationsLoading } = @@ -91,7 +91,7 @@ export const NativeIntegrationsTab = () => { if (!selectedCloudIntegration) return; try { - redirectForProviderAuth(currentWorkspace.id, navigate, selectedCloudIntegration); + redirectForProviderAuth(currentProject.id, navigate, selectedCloudIntegration); } catch (error) { console.error(error); } diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx index fcd42a10f..ed96dcd45 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx @@ -36,7 +36,7 @@ import { THead, Tr } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { getUserTablePreference, @@ -109,7 +109,7 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => { environmentIds: [] }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { search, @@ -372,7 +372,7 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => { No Secret Syncs Configured )} Environment - {currentWorkspace.environments.map((env) => ( + {currentProject.environments.map((env) => ( { e.preventDefault(); diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncsTab.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncsTab.tsx index 20a3c74df..f1866cda0 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncsTab.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncsTab.tsx @@ -1,13 +1,14 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate, useSearch } from "@tanstack/react-router"; import { ProjectPermissionCan } from "@app/components/permissions"; import { CreateSecretSyncModal } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; import { Button, Spinner } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionSub, useProject } from "@app/context"; import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp } from "@app/hooks"; import { useListSecretSyncs } from "@app/hooks/api/secretSyncs"; @@ -16,14 +17,15 @@ import { SecretSyncsTable } from "./SecretSyncTable"; export const SecretSyncsTab = () => { const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addSync"] as const); + const [initialSyncFormData, setInitialSyncFormData] = useState>(); - const { addSync, ...search } = useSearch({ + const { addSync, connectionId, connectionName, ...search } = useSearch({ from: ROUTE_PATHS.SecretManager.IntegrationsListPage.id }); const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); useEffect(() => { if (!addSync) return; @@ -32,14 +34,46 @@ export const SecretSyncsTab = () => { navigate({ to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path, params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search }); }, [addSync]); + useEffect(() => { + if (connectionId && connectionName) { + const storedFormData = localStorage.getItem("secretSyncFormData"); + + if (!storedFormData) return; + + let form: Partial = {}; + try { + form = JSON.parse(storedFormData) as TSecretSyncForm; + } catch { + return; + } finally { + localStorage.removeItem("secretSyncFormData"); + } + + handlePopUpOpen("addSync", form.destination); + + setInitialSyncFormData({ + ...form, + connection: { id: connectionId, name: connectionName } + }); + + navigate({ + to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path, + params: { + projectId: currentProject.id + }, + search + }); + } + }, [connectionId, connectionName]); + const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs( - currentWorkspace.id, + currentProject.id, { refetchInterval: 30000 } @@ -100,7 +134,11 @@ export const SecretSyncsTab = () => { handlePopUpToggle("addSync", isOpen)} + initialFormData={initialSyncFormData} + onOpenChange={(isOpen) => { + if (!isOpen) setInitialSyncFormData(undefined); + handlePopUpToggle("addSync", isOpen); + }} /> ); diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx index b7f8236da..405aec3e9 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx @@ -2,22 +2,24 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; -import { workspaceKeys } from "@app/hooks/api"; +import { projectKeys } from "@app/hooks/api"; import { TIntegration } from "@app/hooks/api/integrations/types"; +import { fetchWorkspaceIntegrations } from "@app/hooks/api/projects/queries"; import { fetchSecretSyncsByProjectId, SecretSync, secretSyncKeys, TSecretSync } from "@app/hooks/api/secretSyncs"; -import { fetchWorkspaceIntegrations } from "@app/hooks/api/workspace/queries"; import { IntegrationsListPageTabs } from "@app/types/integrations"; import { IntegrationsListPage } from "./IntegrationsListPage"; const IntegrationsListPageQuerySchema = z.object({ selectedTab: z.nativeEnum(IntegrationsListPageTabs).optional(), - addSync: z.nativeEnum(SecretSync).optional() + addSync: z.nativeEnum(SecretSync).optional(), + connectionId: z.string().optional(), + connectionName: z.string().optional() }); export const Route = createFileRoute( @@ -57,7 +59,7 @@ export const Route = createFileRoute( let integrations: TIntegration[]; try { integrations = await context.queryClient.ensureQueryData({ - queryKey: workspaceKeys.getWorkspaceIntegrations(projectId), + queryKey: projectKeys.getProjectIntegrations(projectId), queryFn: () => fetchWorkspaceIntegrations(projectId) }); } catch { diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 910e07eac..ab9b332b0 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -63,9 +63,9 @@ import { ProjectPermissionActions, ProjectPermissionDynamicSecretActions, ProjectPermissionSub, + useProject, useProjectPermission, - useSubscription, - useWorkspace + useSubscription } from "@app/context"; import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types"; import { @@ -92,16 +92,11 @@ import { import { useGetProjectSecretsOverview } from "@app/hooks/api/dashboard/queries"; import { DashboardSecretsOrderBy, ProjectSecretsImportedBy } from "@app/hooks/api/dashboard/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { ProjectVersion } from "@app/hooks/api/projects/types"; import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries"; import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types"; import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2"; -import { - SecretType, - SecretV3RawSanitized, - TSecretFolder, - WorkspaceEnv -} from "@app/hooks/api/types"; -import { ProjectVersion } from "@app/hooks/api/workspace/types"; +import { ProjectEnv, SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types"; import { useDynamicSecretOverview, useFolderOverview, @@ -173,10 +168,9 @@ export const OverviewPage = () => { const [debouncedScrollOffset] = useDebounce(scrollOffset); const { permission } = useProjectPermission(); const tableRef = useRef(null); - const { currentWorkspace } = useWorkspace(); - const isProjectV3 = currentWorkspace?.version === ProjectVersion.V3; - const workspaceId = currentWorkspace?.id as string; - const projectSlug = currentWorkspace?.slug as string; + const { currentProject, projectId } = useProject(); + const isProjectV3 = currentProject?.version === ProjectVersion.V3; + const projectSlug = currentProject?.slug as string; const [searchFilter, setSearchFilter] = useState(""); const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter); const secretPath = (routerSearch?.secretPath as string) || "/"; @@ -246,7 +240,7 @@ export const OverviewPage = () => { }; }, []); - const userAvailableEnvs = currentWorkspace?.environments || []; + const userAvailableEnvs = currentProject?.environments || []; const userAvailableDynamicSecretEnvs = userAvailableEnvs.filter((env) => permission.can( ProjectPermissionDynamicSecretActions.CreateRootCredential, @@ -267,7 +261,7 @@ export const OverviewPage = () => { ) ); - const [filteredEnvs, setFilteredEnvs] = useState([]); + const [filteredEnvs, setFilteredEnvs] = useState([]); const visibleEnvs = filteredEnvs.length ? filteredEnvs : userAvailableEnvs; const { @@ -276,7 +270,7 @@ export const OverviewPage = () => { getImportedSecretByKey, getEnvImportedSecretKeyCount } = useGetImportedSecretsAllEnvs({ - projectId: workspaceId, + projectId, path: secretPath, environments: (userAvailableEnvs || []).map(({ slug }) => slug) }); @@ -284,7 +278,7 @@ export const OverviewPage = () => { const isFilteredByResources = Object.values(filter).some(Boolean); const { isPending: isOverviewLoading, data: overview } = useGetProjectSecretsOverview( { - projectId: workspaceId, + projectId, environments: visibleEnvs.map((env) => env.slug), secretPath, orderDirection, @@ -368,7 +362,7 @@ export const OverviewPage = () => { ); const { data: tags } = useGetWsTags( - permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags) ? workspaceId : "" + permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags) ? projectId : "" ); const { mutateAsync: createSecretV3 } = useCreateSecretV3(); @@ -398,7 +392,7 @@ export const OverviewPage = () => { name: folderName, path: secretPath, environment, - projectId: workspaceId, + projectId, description }); }); @@ -456,9 +450,8 @@ export const OverviewPage = () => { try { await updateFolderBatch({ - projectSlug, folders: updatedFolders, - projectId: workspaceId + projectId }); createNotification({ type: "success", @@ -491,7 +484,7 @@ export const OverviewPage = () => { ); if (folderName && parentPath && canCreateFolder) { await createFolder({ - projectId: workspaceId, + projectId, path: parentPath, environment: env, name: folderName @@ -500,7 +493,7 @@ export const OverviewPage = () => { } const result = await createSecretV3({ environment: env, - workspaceId, + projectId, secretPath, secretKey: key, secretValue: value, @@ -555,7 +548,7 @@ export const OverviewPage = () => { try { const result = await updateSecretV3({ environment: env, - workspaceId, + projectId, secretPath, secretKey: key, secretValue, @@ -586,7 +579,7 @@ export const OverviewPage = () => { try { const result = await deleteSecretV3({ environment: env, - workspaceId, + projectId, secretPath, secretKey: key, secretId, @@ -655,7 +648,7 @@ export const OverviewPage = () => { ); if (folderName && parentPath && canCreateFolder) { await createFolder({ - projectId: workspaceId, + projectId, environment: slug, path: parentPath, name: folderName @@ -669,7 +662,7 @@ export const OverviewPage = () => { navigate({ to: "/projects/secret-management/$projectId/secrets/$envSlug", params: { - projectId: workspaceId, + projectId, envSlug: slug }, search: query @@ -1099,7 +1092,7 @@ export const OverviewPage = () => { tags={tags} onChange={setSearchFilter} environments={userAvailableEnvs} - projectId={currentWorkspace?.id} + projectId={currentProject?.id} /> {userAvailableEnvs.length > 0 && (
@@ -1419,7 +1412,7 @@ export const OverviewPage = () => { diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx index 7087faf72..d4f8bbafe 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -19,8 +19,8 @@ import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { ProjectPermissionActions, ProjectPermissionSub, - useProjectPermission, - useWorkspace + useProject, + useProjectPermission } from "@app/context"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { getKeyValue } from "@app/helpers/parseEnvVar"; @@ -57,16 +57,15 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => { formState: { isSubmitting, errors } } = useForm({ resolver: zodResolver(typeSchema) }); - const { currentWorkspace } = useWorkspace(); + const { currentProject, projectId } = useProject(); const { permission } = useProjectPermission(); const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); - const workspaceId = currentWorkspace?.id || ""; - const environments = currentWorkspace?.environments || []; + const environments = currentProject?.environments || []; const { mutateAsync: createSecretV3 } = useCreateSecretV3(); const { mutateAsync: createFolder } = useCreateFolder(); const { data: projectTags, isPending: isTagsLoading } = useGetWsTags( - canReadTags ? workspaceId : "" + canReadTags ? projectId : "" ); const secretKeyInputRef = useRef(null); @@ -93,7 +92,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => { if (folderName && parentPath && canCreateFolder) { await createFolder({ - projectId: workspaceId, + projectId, path: parentPath, environment, name: folderName @@ -106,7 +105,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => { return { ...(await createSecretV3({ environment, - workspaceId, + projectId, secretPath, secretKey: key, secretValue: value || "", @@ -176,7 +175,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => { if (!secretKey || isWholeKeyHighlighted) { e.preventDefault(); - const keyStr = currentWorkspace.autoCapitalization ? key.toUpperCase() : key; + const keyStr = currentProject.autoCapitalization ? key.toUpperCase() : key; setValue("key", keyStr); if (value) { setValue("value", value); @@ -191,7 +190,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => { try { const parsedSlug = slugSchema.parse(slug); await createWsTag.mutateAsync({ - workspaceID: workspaceId, + projectId, tagSlug: parsedSlug, tagColor: "" }); @@ -220,7 +219,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => { }} placeholder="Type your secret name" onPaste={handlePaste} - autoCapitalization={currentWorkspace?.autoCapitalization} + autoCapitalization={currentProject?.autoCapitalization} /> { secret?: SecretV3RawSanitized; environmentInfo?: WorkspaceEnv } | undefined; + ) => { secret?: SecretV3RawSanitized; environmentInfo?: ProjectEnv } | undefined; scrollOffset: number; importedBy?: { environment: { name: string; slug: string }; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx index d15e96a91..5e1f27f57 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -10,7 +10,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2"; -import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context"; +import { ProjectPermissionSub, useProject, useProjectPermission } from "@app/context"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { useToggle } from "@app/hooks"; import { useUpdateSecretV3 } from "@app/hooks/api"; @@ -37,7 +37,7 @@ export const formSchema = z.object({ type TFormSchema = z.infer; function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }: Props) { - const { currentWorkspace } = useWorkspace(); + const { currentProject, projectId } = useProject(); const { permission } = useProjectPermission(); const secrets = environments.map((env) => getSecretByKey(env.slug, secretKey)); @@ -68,7 +68,6 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } secret?.overrideAction === SecretActionType.Created || secret?.overrideAction === SecretActionType.Modified ); - const workspaceId = currentWorkspace?.id || ""; const [isSecNameCopied, setIsSecNameCopied] = useToggle(false); @@ -111,7 +110,7 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } return updateSecretV3({ environment: secret?.env, - workspaceId, + projectId, secretPath, secretKey: secret.key, secretValue: secret.value || "", @@ -163,7 +162,7 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } void; isSingleEnv?: boolean; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx index 14faa7e9e..048589827 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx @@ -8,11 +8,11 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, Checkbox, Modal, ModalContent, Spinner } from "@app/components/v2"; -import { useProjectPermission, useWorkspace } from "@app/context"; +import { useProject, useProjectPermission } from "@app/context"; import { usePopUp } from "@app/hooks"; -import { useGetWorkspaceById, useMigrateProjectToV3, workspaceKeys } from "@app/hooks/api"; +import { projectKeys, useGetWorkspaceById, useMigrateProjectToV3 } from "@app/hooks/api"; +import { ProjectVersion } from "@app/hooks/api/projects/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; -import { ProjectVersion } from "@app/hooks/api/workspace/types"; enum ProjectUpgradeStatus { InProgress = "IN_PROGRESS", @@ -28,14 +28,14 @@ const formSchema = z.object({ export const SecretV2MigrationSection = () => { const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["migrationInfo"] as const); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const queryClient = useQueryClient(); const { data: workspaceDetails, refetch } = useGetWorkspaceById( // if v3 no need to fetch - currentWorkspace?.version === ProjectVersion.V3 ? "" : currentWorkspace?.id || "", + currentProject?.version === ProjectVersion.V3 ? "" : currentProject?.id || "", { refetchInterval: - currentWorkspace?.upgradeStatus === ProjectUpgradeStatus.InProgress ? 2000 : false + currentProject?.upgradeStatus === ProjectUpgradeStatus.InProgress ? 2000 : false } ); const { membership } = useProjectPermission(); @@ -54,12 +54,12 @@ export const SecretV2MigrationSection = () => { createNotification({ type: "success", text: "Project upgrade completed successfully" }); migrateProjectToV3.reset(); queryClient.invalidateQueries({ - queryKey: workspaceKeys.getAllUserWorkspace() + queryKey: projectKeys.getAllUserProjects() }); } }, [isProjectUpgraded, Boolean(migrateProjectToV3.data)]); - if (isProjectUpgraded || currentWorkspace?.version === ProjectVersion.V3) return null; + if (isProjectUpgraded || currentProject?.version === ProjectVersion.V3) return null; const isUpgrading = workspaceDetails?.upgradeStatus === ProjectUpgradeStatus.InProgress; const didProjectUpgradeFailed = workspaceDetails?.upgradeStatus === ProjectUpgradeStatus.Failed; @@ -67,7 +67,7 @@ export const SecretV2MigrationSection = () => { const handleMigrationSecretV2 = async () => { try { handlePopUpToggle("migrationInfo"); - await migrateProjectToV3.mutateAsync({ workspaceId: currentWorkspace?.id || "" }); + await migrateProjectToV3.mutateAsync({ projectId: currentProject?.id || "" }); refetch(); createNotification({ text: "Project upgrade started", diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx index 11ee3def8..8f84cd612 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx @@ -9,9 +9,9 @@ import { Button, DeleteActionModal, Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, + useProject, useProjectPermission, - useSubscription, - useWorkspace + useSubscription } from "@app/context"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp } from "@app/hooks"; @@ -66,9 +66,8 @@ export const SelectionPanel = ({ ); const selectedCount = selectedFolderCount + selectedKeysCount; - const { currentWorkspace } = useWorkspace(); - const workspaceId = currentWorkspace?.id || ""; - const userAvailableEnvs = currentWorkspace?.environments || []; + const { currentProject, projectId } = useProject(); + const userAvailableEnvs = currentProject?.environments || []; const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch(); const { mutateAsync: deleteFolder } = useDeleteFolder(); @@ -134,7 +133,7 @@ export const SelectionPanel = ({ folderId: folder?.id, path: secretPath, environment: env.slug, - projectId: workspaceId + projectId }); } }) @@ -173,7 +172,7 @@ export const SelectionPanel = ({ processedEntries += secretsToDelete.length; await deleteBatchSecretV3({ secretPath, - workspaceId, + projectId, environment: env.slug, secrets: secretsToDelete }); @@ -281,8 +280,8 @@ export const SelectionPanel = ({ isOpen={popUp.bulkMoveSecrets.isOpen} onOpenChange={(isOpen) => handlePopUpToggle("bulkMoveSecrets", isOpen)} environments={userAvailableEnvs} - projectId={workspaceId} - projectSlug={currentWorkspace.slug} + projectId={projectId} + projectSlug={currentProject.slug} sourceSecretPath={secretPath} secrets={selectedEntries[EntryType.SECRET]} onComplete={resetSelectedEntries} diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx index bb7862f09..30ea57957 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx @@ -30,13 +30,13 @@ import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionCo import { useDebounce } from "@app/hooks"; import { useMoveSecrets } from "@app/hooks/api"; import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard"; +import { ProjectEnv } from "@app/hooks/api/projects/types"; import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; -import { WorkspaceEnv } from "@app/hooks/api/workspace/types"; type Props = { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; projectId: string; projectSlug: string; sourceSecretPath: string; @@ -64,7 +64,6 @@ type MoveResults = { const Content = ({ onComplete, secrets, - projectSlug, environments, projectId, sourceSecretPath @@ -189,7 +188,6 @@ const Content = ({ try { const { isDestinationUpdated, isSourceUpdated } = await moveSecrets.mutateAsync({ - projectSlug, shouldOverwrite, sourceEnvironment: environment.slug, sourceSecretPath, diff --git a/frontend/src/pages/secret-manager/OverviewPage/route.tsx b/frontend/src/pages/secret-manager/OverviewPage/route.tsx index 968286e13..2be6c6f25 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/route.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/route.tsx @@ -6,7 +6,9 @@ import { OverviewPage } from "./OverviewPage"; const SecretOverviewPageQuerySchema = z.object({ search: z.string().catch(""), - secretPath: z.string().catch("/") + secretPath: z.string().catch("/"), + connectionId: z.string().optional(), + connectionName: z.string().optional() }); export const Route = createFileRoute( diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/SecretApprovalsPage.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/SecretApprovalsPage.tsx index bc82e9b90..77106a57c 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/SecretApprovalsPage.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/SecretApprovalsPage.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { Badge } from "@app/components/v2/Badge"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api"; import { AccessApprovalRequest } from "./components/AccessApprovalRequest"; @@ -20,11 +20,10 @@ enum TabSection { export const SecretApprovalsPage = () => { const { t } = useTranslation(); - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; - const projectSlug = currentWorkspace?.slug || ""; + const { currentProject, projectId } = useProject(); + const projectSlug = currentProject?.slug || ""; const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ - workspaceId: projectId + projectId }); const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug }); const defaultTab = @@ -67,7 +66,7 @@ export const SecretApprovalsPage = () => { - +
diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 957091fe5..ca04665a5 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -39,10 +39,10 @@ import { Badge } from "@app/components/v2/Badge"; import { ProjectPermissionMemberActions, ProjectPermissionSub, + useProject, useProjectPermission, useSubscription, - useUser, - useWorkspace + useUser } from "@app/context"; import { getUserTablePreference, @@ -93,7 +93,7 @@ export const AccessApprovalRequest = ({ }) => { const [selectedRequest, setSelectedRequest] = useState< | (TAccessApprovalRequest & { - user: { firstName?: string; lastName?: string; email?: string } | null; + user: { firstName?: string | null; lastName?: string | null; email?: string | null } | null; isRequestedByCurrentUser: boolean; isSelfApproveAllowed: boolean; isApprover: boolean; @@ -110,7 +110,7 @@ export const AccessApprovalRequest = ({ const { permission } = useProjectPermission(); const { user } = useUser(); const { subscription } = useSubscription(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: members } = useGetWorkspaceUsers(projectId, true); const membersGroupById = members?.reduce>( @@ -410,7 +410,7 @@ export const AccessApprovalRequest = ({ Select an Environment - {currentWorkspace?.environments.map(({ slug, name }) => ( + {currentProject?.environments.map(({ slug, name }) => ( setEnvFilter((state) => (state === slug ? undefined : slug))} key={`request-filter-${slug}`} diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx index b1cd906bb..a9691724d 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx @@ -25,7 +25,7 @@ import { Tooltip } from "@app/components/v2"; import { Badge } from "@app/components/v2/Badge"; -import { ProjectPermissionActions, useUser, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, useProject, useUser } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useListWorkspaceGroups, useReviewAccessRequest } from "@app/hooks/api"; import { @@ -85,7 +85,7 @@ export const ReviewAccessRequestModal = ({ isOpen: boolean; onOpenChange: (isOpen: boolean) => void; request: TAccessApprovalRequest & { - user: { firstName?: string; lastName?: string; email?: string } | null; + user: { firstName?: string | null; lastName?: string | null; email?: string | null } | null; isRequestedByCurrentUser: boolean; isSelfApproveAllowed: boolean; isApprover: boolean; @@ -102,8 +102,8 @@ export const ReviewAccessRequestModal = ({ const [bypassApproval, setBypassApproval] = useState(false); const [bypassReason, setBypassReason] = useState(""); - const { currentWorkspace } = useWorkspace(); - const { data: groupMemberships = [] } = useListWorkspaceGroups(currentWorkspace?.id || ""); + const { currentProject } = useProject(); + const { data: groupMemberships = [] } = useListWorkspaceGroups(currentProject?.id || ""); const { user } = useUser(); const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["editRequest"] as const); diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx index 3d292f9a2..bb20643e8 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx @@ -40,9 +40,9 @@ import { import { ProjectPermissionSub, TProjectPermission, + useProject, useProjectPermission, - useSubscription, - useWorkspace + useSubscription } from "@app/context"; import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types"; import { @@ -59,14 +59,14 @@ import { import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { PolicyType } from "@app/hooks/api/policies/enums"; -import { TAccessApprovalPolicy, Workspace } from "@app/hooks/api/types"; +import { Project, TAccessApprovalPolicy } from "@app/hooks/api/types"; import { AccessPolicyForm } from "./components/AccessPolicyModal"; import { ApprovalPolicyRow } from "./components/ApprovalPolicyRow"; import { RemoveApprovalPolicyModal } from "./components/RemoveApprovalPolicyModal"; interface IProps { - workspaceId: string; + projectId: string; } enum PolicyOrderBy { @@ -81,24 +81,24 @@ type PolicyFilters = { environmentIds: string[]; }; -const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?: Workspace) => { +const useApprovalPolicies = (permission: TProjectPermission, currentProject?: Project) => { const { data: accessPolicies, isPending: isAccessPoliciesLoading } = useGetAccessApprovalPolicies( { - projectSlug: currentWorkspace?.slug as string, + projectSlug: currentProject?.slug as string, options: { enabled: permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) && - !!currentWorkspace?.slug + !!currentProject?.slug } } ); const { data: secretPolicies, isPending: isSecretPoliciesLoading } = useGetSecretApprovalPolicies( { - workspaceId: currentWorkspace?.id as string, + projectId: currentProject?.id as string, options: { enabled: permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) && - !!currentWorkspace?.id + !!currentProject?.id } } ); @@ -118,7 +118,7 @@ const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?: }; }; -export const ApprovalPolicyList = ({ workspaceId }: IProps) => { +export const ApprovalPolicyList = ({ projectId }: IProps) => { const { handlePopUpToggle, handlePopUpOpen, popUp } = usePopUp([ "policyForm", "deletePolicy", @@ -126,14 +126,14 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => { ] as const); const { permission } = useProjectPermission(); const { subscription } = useSubscription(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); - const { data: members } = useGetWorkspaceUsers(workspaceId, true); - const { data: groups } = useListWorkspaceGroups(currentWorkspace?.id || ""); + const { data: members } = useGetWorkspaceUsers(projectId, true); + const { data: groups } = useListWorkspaceGroups(currentProject?.id || ""); const { policies, isLoading: isPoliciesLoading } = useApprovalPolicies( permission, - currentWorkspace + currentProject ); const [filters, setFilters] = useState({ @@ -367,7 +367,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => { Change Policy Environment - {currentWorkspace.environments.map((env) => ( + {currentProject.environments.map((env) => ( { e.preventDefault(); @@ -466,7 +466,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => { )} - {!!currentWorkspace && + {!!currentProject && filteredPolicies ?.slice(offset, perPage * page) .map((policy) => ( @@ -497,8 +497,8 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
handlePopUpToggle("policyForm", isOpen)} members={members} diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx index 856c16ee1..b78b8aa0c 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx @@ -22,7 +22,7 @@ import { Tooltip } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { getMemberLabel } from "@app/helpers/members"; import { policyDetails } from "@app/helpers/policies"; import { @@ -207,10 +207,10 @@ const Form = ({ name: "sequenceApprovers" }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: groups } = useListWorkspaceGroups(projectId); - const availableEnvironments = currentWorkspace?.environments || []; + const availableEnvironments = currentProject?.environments || []; const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy; const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); @@ -246,7 +246,7 @@ const Form = ({ approvers: [...userApprovers, ...groupApprovers], bypassers: bypassers.length > 0 ? bypassers : undefined, environments: environments.map((env) => env.slug), - workspaceId: currentWorkspace?.id || "" + projectId: currentProject?.id || "" }); } else { await createAccessApprovalPolicy({ @@ -302,7 +302,7 @@ const Form = ({ ...data, approvers: [...userApprovers, ...groupApprovers], bypassers: bypassers.length > 0 ? bypassers : undefined, - workspaceId: currentWorkspace?.id || "", + projectId: currentProject?.id || "", environments: environments.map((env) => env.slug) }); } else { diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx index 89a97003e..13147b37b 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx @@ -33,13 +33,13 @@ import { Approver } from "@app/hooks/api/accessApproval/types"; import { TGroupMembership } from "@app/hooks/api/groups/types"; import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums"; import { ApproverType } from "@app/hooks/api/secretApproval/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { TWorkspaceUser } from "@app/hooks/api/users/types"; interface IPolicy { id: string; name: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; projectId?: string; secretPath?: string; approvals: number; diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/RemoveApprovalPolicyModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/RemoveApprovalPolicyModal.tsx index d267ffc78..c9f92f6ed 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/RemoveApprovalPolicyModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/RemoveApprovalPolicyModal.tsx @@ -4,7 +4,7 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { DeleteActionModal, Spinner } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useDeleteAccessApprovalPolicy, useDeleteSecretApprovalPolicy, @@ -29,18 +29,18 @@ export const RemoveApprovalPolicyModal = ({ const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy(); const { mutateAsync: deleteAccessApprovalPolicy } = useDeleteAccessApprovalPolicy(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const handleDeletePolicy = async () => { try { if (policyType === PolicyType.ChangePolicy) { await deleteSecretApprovalPolicy({ - workspaceId: currentWorkspace.id, + projectId: currentProject.id, id: policyId }); } else { await deleteAccessApprovalPolicy({ - projectSlug: currentWorkspace.slug, + projectSlug: currentProject.slug, id: policyId }); } @@ -59,14 +59,14 @@ export const RemoveApprovalPolicyModal = ({ const deleteSecretApprovalData = useGetSecretApprovalRequestCount({ policyId, - workspaceId: currentWorkspace.id, + projectId: currentProject.id, options: { enabled: Boolean(policyId) && policyType === PolicyType.ChangePolicy } }); const deleteAccessApprovalData = useGetAccessRequestsCount({ - projectSlug: currentWorkspace.slug, + projectSlug: currentProject.slug, policyId, options: { enabled: Boolean(policyId) && policyType === PolicyType.AccessPolicy diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx index 46b611328..827d14f1e 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx @@ -35,9 +35,9 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionMemberActions, ProjectPermissionSub, + useProject, useProjectPermission, - useUser, - useWorkspace + useUser } from "@app/context"; import { getUserTablePreference, @@ -58,8 +58,7 @@ import { } from "./components/SecretApprovalRequestChanges"; export const SecretApprovalRequest = () => { - const { currentWorkspace } = useWorkspace(); - const workspaceId = currentWorkspace?.id || ""; + const { currentProject, projectId } = useProject(); const [selectedApprovalId, setSelectedApprovalId] = useState(null); // filters @@ -92,7 +91,7 @@ export const SecretApprovalRequest = () => { isPending: isApprovalRequestLoading, refetch } = useGetSecretApprovalRequests({ - workspaceId, + projectId, status: statusFilter, environment: envFilter, committer: committerFilter, @@ -105,14 +104,14 @@ export const SecretApprovalRequest = () => { const secretApprovalRequests = data?.approvals ?? []; const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } = - useGetSecretApprovalRequestCount({ workspaceId }); + useGetSecretApprovalRequestCount({ projectId }); const { user: userSession } = useUser(); const search = useSearch({ from: ROUTE_PATHS.SecretManager.ApprovalPage.id }); const { permission } = useProjectPermission(); - const { data: members } = useGetWorkspaceUsers(workspaceId); + const { data: members } = useGetWorkspaceUsers(projectId); const isSecretApprovalScreen = Boolean(selectedApprovalId); const { requestId } = search; @@ -143,7 +142,6 @@ export const SecretApprovalRequest = () => { exit={{ opacity: 0, translateX: 30 }} > @@ -241,7 +239,7 @@ export const SecretApprovalRequest = () => { Select an Environment - {currentWorkspace?.environments.map(({ slug, name }) => ( + {currentProject?.environments.map(({ slug, name }) => ( setEnvFilter((state) => (state === slug ? undefined : slug))} key={`request-filter-${slug}`} diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx index ee45a442b..9bafb36d4 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx @@ -13,6 +13,7 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { Button, Checkbox, FormControl, Input } from "@app/components/v2"; +import { useProject } from "@app/context"; import { usePerformSecretApprovalRequestMerge, useUpdateSecretApprovalRequestStatus @@ -28,7 +29,6 @@ type Props = { canApprove?: boolean; isBypasser: boolean; statusChangeByEmail?: string; - workspaceId: string; enforcementLevel: EnforcementLevel; }; @@ -39,11 +39,11 @@ export const SecretApprovalRequestAction = ({ isMergable, approvals, statusChangeByEmail, - workspaceId, enforcementLevel, canApprove, isBypasser }: Props) => { + const { projectId } = useProject(); const { mutateAsync: performSecretApprovalMerge, isPending: isMerging } = usePerformSecretApprovalRequestMerge(); @@ -62,7 +62,7 @@ export const SecretApprovalRequestAction = ({ try { await performSecretApprovalMerge({ id: approvalRequestId, - workspaceId, + projectId, bypassReason: byPassApproval ? bypassReason : undefined }); createNotification({ @@ -83,7 +83,7 @@ export const SecretApprovalRequestAction = ({ await updateSecretStatusChange({ id: approvalRequestId, status: reqState, - workspaceId + projectId }); createNotification({ type: "success", diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx index 636952c33..0cce12c47 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx @@ -33,7 +33,7 @@ import { TextArea, Tooltip } from "@app/components/v2"; -import { useUser, useWorkspace } from "@app/context"; +import { useProject, useUser } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useGetSecretApprovalRequestDetails, @@ -92,7 +92,6 @@ const getReviewedStatusSymbol = (status?: ApprovalStatus) => { }; type Props = { - workspaceId: string; approvalRequestId: string; onGoBack: () => void; }; @@ -104,13 +103,9 @@ const reviewFormSchema = z.object({ type TReviewFormSchema = z.infer; -export const SecretApprovalRequestChanges = ({ - approvalRequestId, - onGoBack, - workspaceId -}: Props) => { +export const SecretApprovalRequestChanges = ({ approvalRequestId, onGoBack }: Props) => { const { user: userSession } = useUser(); - const { currentWorkspace } = useWorkspace(); + const { projectId } = useProject(); const { data: secretApprovalRequestDetails, isSuccess: isSecretApprovalRequestSuccess, @@ -123,7 +118,7 @@ export const SecretApprovalRequestChanges = ({ ); const { data: secretImports } = useGetSecretImports({ environment: secretApprovalRequestDetails?.environment || "", - projectId: currentWorkspace.id, + projectId, path: approvalSecretPath }); @@ -526,7 +521,6 @@ export const SecretApprovalRequestChanges = ({ isMergable={isMergable} statusChangeByEmail={secretApprovalRequestDetails.statusChangedByUser?.email} enforcementLevel={secretApprovalRequestDetails.policy.enforcementLevel} - workspaceId={workspaceId} />
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index d79a107db..c926450eb 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -25,10 +25,11 @@ import { ProjectPermissionActions, ProjectPermissionDynamicSecretActions, ProjectPermissionSub, - useProjectPermission, - useWorkspace + useProject, + useProjectPermission } from "@app/context"; import { + ProjectPermissionCommitsActions, ProjectPermissionSecretActions, ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types"; @@ -49,10 +50,10 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard"; import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; import { useGetFolderCommitsCount } from "@app/hooks/api/folderCommits"; import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { ProjectVersion } from "@app/hooks/api/projects/types"; import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { useCreateCommit } from "@app/hooks/api/secrets/mutations"; import { SecretV3RawSanitized } from "@app/hooks/api/types"; -import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies"; import { useResizableColWidth } from "@app/hooks/useResizableColWidth"; import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; @@ -93,7 +94,7 @@ const LOADER_TEXT = [ ]; const Page = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const navigate = useNavigate({ from: ROUTE_PATHS.SecretManager.SecretDashboardPage.path }); @@ -106,7 +107,7 @@ const Page = () => { }); const { permission } = useProjectPermission(); - const { mutateAsync: createCommit } = useCreateCommit(); + const { mutateAsync: createCommit, isPending: isCommitPending } = useCreateCommit(); const tableRef = useRef(null); @@ -141,15 +142,15 @@ const Page = () => { ] as const); // env slug - const workspaceId = currentWorkspace?.id || ""; - const projectSlug = currentWorkspace?.slug || ""; + const projectId = currentProject?.id || ""; + const projectSlug = currentProject?.slug || ""; const secretPath = (routerQueryParams.secretPath as string) || "/"; useEffect(() => { - if (isBatchMode && workspaceId && environment && secretPath) { - loadPendingChanges({ workspaceId, environment, secretPath }); + if (isBatchMode && projectId && environment && secretPath) { + loadPendingChanges({ projectId, environment, secretPath }); } - }, [isBatchMode, workspaceId, environment, secretPath, loadPendingChanges]); + }, [isBatchMode, projectId, environment, secretPath, loadPendingChanges]); const canReadSecret = hasSecretReadValueOrDescribePermission( permission, @@ -213,6 +214,11 @@ const Page = () => { ProjectPermissionSub.SecretRollback ); + const canReadCommits = permission.can( + ProjectPermissionCommitsActions.Read, + ProjectPermissionSub.Commits + ); + const defaultFilterState = { tags: {}, searchFilter: (routerQueryParams.search as string) || "", @@ -234,7 +240,7 @@ const Page = () => { const { togglePopUp } = usePopUpAction(); useEffect(() => { - if (!currentWorkspace?.environments.find((env) => env.slug === environment)) { + if (!currentProject?.environments.find((env) => env.slug === environment)) { createNotification({ text: "No environment found with given slug", type: "error" @@ -242,11 +248,11 @@ const Page = () => { navigate({ to: "/projects/secret-management/$projectId/overview", params: { - projectId: workspaceId + projectId } }); } - }, [currentWorkspace, environment]); + }, [currentProject, environment]); const isResourceTypeFiltered = Object.values(filter.include).some(Boolean); const { @@ -256,7 +262,7 @@ const Page = () => { isFetched } = useGetProjectSecretsDetails({ environment, - projectId: workspaceId, + projectId, secretPath, offset, limit, @@ -310,7 +316,7 @@ const Page = () => { // fetch imported secrets to show user the overriden ones const { data: importedSecrets } = useGetImportedSecretsSingleEnv({ - projectId: workspaceId, + projectId, environment, path: secretPath, options: { @@ -320,13 +326,13 @@ const Page = () => { // fetch tags const { data: tags } = useGetWsTags( - permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags) ? workspaceId : "" + permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags) ? projectId : "" ); const { pathPolicies, hasPathPolicies } = usePathAccessPolicies({ secretPath, environment }); const { data: boardPolicy } = useGetSecretApprovalPolicyOfABoard({ - workspaceId, + projectId, environment, secretPath }); @@ -335,7 +341,7 @@ const Page = () => { const handleCreateCommit = async (changes: PendingChanges, message: string) => { try { await createCommit({ - workspaceId, + projectId, environment, secretPath, pendingChanges: changes, @@ -362,7 +368,7 @@ const Page = () => { fetchNextPage: fetchNextSnapshotList, hasNextPage: hasNextSnapshotListPage } = useGetWorkspaceSnapshotList({ - workspaceId, + projectId, directory: secretPath, environment, isPaused: !popUp.snapshots.isOpen || !canDoReadRollback, @@ -375,9 +381,9 @@ const Page = () => { isFetching: isFolderCommitsCountFetching } = useGetFolderCommitsCount({ directory: secretPath, - workspaceId, + projectId, environment, - isPaused: !canDoReadRollback + isPaused: !canReadCommits }); const { @@ -385,13 +391,13 @@ const Page = () => { isPending: isSnapshotCountLoading, isFetching: isSnapshotCountFetching } = useGetWsSnapshotCount({ - workspaceId, + projectId, environment, directory: secretPath, isPaused: !canDoReadRollback }); - const isPITEnabled = !currentWorkspace?.showSnapshotsLegacy; + const isPITEnabled = !currentProject?.showSnapshotsLegacy; const changesCount = useMemo(() => { return isPITEnabled ? folderCommitsCount : snapshotCount; @@ -410,7 +416,7 @@ const Page = () => { navigate({ to: "/projects/secret-management/$projectId/commits/$environment/$folderId", params: { - projectId: workspaceId, + projectId, folderId, environment }, @@ -641,7 +647,7 @@ const Page = () => { ? change.tags?.map((tag) => ({ id: tag.id, slug: tag.slug, - projectId: workspaceId, + projectId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), __v: 0 @@ -728,7 +734,7 @@ const Page = () => { const mergedSecrets = getMergedSecretsWithPending(); const mergedFolders = getMergedFoldersWithPending(); - if (!(currentWorkspace?.version === ProjectVersion.V3)) + if (!(currentProject?.version === ProjectVersion.V3)) return (
@@ -788,8 +794,6 @@ const Page = () => { <> { secretImports={imports} isFetching={isDetailsFetching} environment={environment} - workspaceId={workspaceId} + projectId={projectId} secretPath={secretPath} importedSecrets={importedSecrets} /> @@ -960,7 +964,7 @@ const Page = () => { { tags={tags} isVisible={isVisible} environment={environment} - workspaceId={workspaceId} + projectId={projectId} secretPath={secretPath} isProtectedBranch={isProtectedBranch} importedBy={importedBy} @@ -995,9 +999,9 @@ const Page = () => { )} {noAccessSecretCount > 0 && } @@ -1046,9 +1050,9 @@ const Page = () => { > @@ -1067,10 +1071,10 @@ const Page = () => { )} { { - return `${workspaceId}_${environment}_${secretPath}`; +const generateContextKey = (projectId: string, environment: string, secretPath: string) => { + return `${projectId}_${environment}_${secretPath}`; }; const createBatchModeStore: StateCreator = (set, get) => ({ @@ -272,7 +272,7 @@ const createBatchModeStore: StateCreator addPendingChange: (change: PendingChange, context: BatchContext) => set((state) => { const contextKey = generateContextKey( - context.workspaceId, + context.projectId, context.environment, context.secretPath ); @@ -551,7 +551,7 @@ const createBatchModeStore: StateCreator const currentChanges = contextKey === generateContextKey( - state.currentContext?.workspaceId || context.workspaceId, + state.currentContext?.projectId || context.projectId, state.currentContext?.environment || context.environment, state.currentContext?.secretPath || context.secretPath ) @@ -568,7 +568,7 @@ const createBatchModeStore: StateCreator removePendingChange: (changeId: string, resourceType: string, context: BatchContext) => set((state) => { const contextKey = generateContextKey( - context.workspaceId, + context.projectId, context.environment, context.secretPath ); @@ -593,7 +593,7 @@ const createBatchModeStore: StateCreator state.currentContext && contextKey === generateContextKey( - state.currentContext.workspaceId, + state.currentContext.projectId, state.currentContext.environment, state.currentContext.secretPath ); @@ -606,7 +606,7 @@ const createBatchModeStore: StateCreator loadPendingChanges: (context) => { const contextKey = generateContextKey( - context.workspaceId, + context.projectId, context.environment, context.secretPath ); @@ -626,7 +626,7 @@ const createBatchModeStore: StateCreator clearAllPendingChanges: (context) => { const contextKey = generateContextKey( - context.workspaceId, + context.projectId, context.environment, context.secretPath ); @@ -641,7 +641,7 @@ const createBatchModeStore: StateCreator state.currentContext && contextKey === generateContextKey( - state.currentContext.workspaceId, + state.currentContext.projectId, state.currentContext.environment, state.currentContext.secretPath ); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index 9252d71a1..226230c1a 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -55,9 +55,9 @@ import { ProjectPermissionActions, ProjectPermissionDynamicSecretActions, ProjectPermissionSub, + useProject, useProjectPermission, - useSubscription, - useWorkspace + useSubscription } from "@app/context"; import { ProjectPermissionCommitsActions, @@ -109,9 +109,6 @@ type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv }; type Props = { // switch the secrets type as it gets decrypted after api call environment: string; - // @depreciated will be moving all these details to zustand - workspaceId: string; - projectSlug: string; secretPath?: string; filter: Filter; tags?: WsTag[]; @@ -142,8 +139,6 @@ type Props = { export const ActionBar = ({ environment, - workspaceId, - projectSlug, secretPath = "/", filter, tags = [], @@ -164,6 +159,7 @@ export const ActionBar = ({ hasPathPolicies, onClearFilters }: Props) => { + const { projectId, currentProject } = useProject(); const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ "addFolder", "addDynamicSecret", @@ -196,7 +192,6 @@ export const ActionBar = ({ const { reset: resetSelectedSecret } = useSelectedSecretActions(); const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length); - const { currentWorkspace } = useWorkspace(); const { permission } = useProjectPermission(); const handleFolderCreate = async (folderName: string, description: string | null) => { @@ -214,7 +209,7 @@ export const ActionBar = ({ }; addPendingChange(pendingFolderCreate, { - workspaceId, + projectId, environment, secretPath }); @@ -227,7 +222,7 @@ export const ActionBar = ({ name: folderName, path: secretPath, environment, - projectId: workspaceId, + projectId, description }); handlePopUpClose("addFolder"); @@ -247,7 +242,7 @@ export const ActionBar = ({ const handleSecretDownload = async () => { try { const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({ - workspaceId, + projectId, expandSecretReferences: true, includeImports: true, environment, @@ -317,7 +312,7 @@ export const ActionBar = ({ try { await deleteBatchSecretV3({ secretPath, - workspaceId, + projectId, environment, secrets: bulkDeletedSecrets.map(({ key }) => ({ secretKey: key, type: SecretType.Shared })) }); @@ -348,13 +343,12 @@ export const ActionBar = ({ try { const secretsToMove = Object.values(selectedSecrets); const { isDestinationUpdated, isSourceUpdated } = await moveSecrets({ - projectSlug, shouldOverwrite, sourceEnvironment: environment, sourceSecretPath: secretPath, destinationEnvironment, destinationSecretPath, - projectId: workspaceId, + projectId, secretIds: secretsToMove.map((sec) => sec.id) }); @@ -448,7 +442,7 @@ export const ActionBar = ({ const { secrets: batchSecrets } = await fetchDashboardProjectSecretsByKeys({ secretPath: normalizedPath, environment, - projectId: workspaceId, + projectId, keys: batch }); @@ -566,7 +560,7 @@ export const ActionBar = ({ name: folderName, path: parentPath, environment, - projectId: workspaceId + projectId }); createdFolders.add(fullPath); @@ -598,7 +592,7 @@ export const ActionBar = ({ Object.entries(groupedCreateSecrets).map(([path, secrets]) => createSecretBatch({ secretPath: path, - workspaceId, + projectId, environment, secrets }) @@ -628,7 +622,7 @@ export const ActionBar = ({ Object.entries(groupedUpdateSecrets).map(([path, secrets]) => updateSecretBatch({ secretPath: path, - workspaceId, + projectId, environment, secrets }) @@ -638,13 +632,12 @@ export const ActionBar = ({ // Invalidate appropriate queries to refresh UI queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); + queryClient.invalidateQueries({}); + dashboardKeys.getDashboardSecrets({ projectId, secretPath }); queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) - }); - queryClient.invalidateQueries({ - queryKey: secretApprovalRequestKeys.count({ workspaceId }) + queryKey: secretApprovalRequestKeys.count({ projectId }) }); // Close the modal and show notification @@ -677,8 +670,8 @@ export const ActionBar = ({ className="w-2/5" value={filter.searchFilter} onChange={onSearchChange} - environments={[currentWorkspace.environments.find((env) => env.slug === environment)!]} - projectId={workspaceId} + environments={[currentProject.environments.find((env) => env.slug === environment)!]} + projectId={projectId} tags={tags} />
@@ -1134,8 +1127,8 @@ export const ActionBar = ({ {/* all the side triggers from actions like modals etc */} handlePopUpOpen("upgradePlan")} isOpen={popUp.addSecretImport.isOpen} onClose={() => handlePopUpClose("addSecretImport")} @@ -1144,7 +1137,7 @@ export const ActionBar = ({ handlePopUpToggle("addDynamicSecret", isOpen)} - projectSlug={projectSlug} + projectSlug={currentProject.slug} environments={[{ slug: environment, name: environment, id: "not-used" }]} secretPath={secretPath} isSingleEnvironmentMode @@ -1190,8 +1183,8 @@ export const ActionBar = ({ onToggle={(isOpen) => handlePopUpToggle("replicateFolder", isOpen)} onParsedEnv={handleParsedEnvMultiFolder} environment={environment} - environments={currentWorkspace.environments} - workspaceId={workspaceId} + environments={currentProject.environments} + projectId={projectId} secretPath={secretPath} /> {subscription && ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx index 84cf7f7b5..e458f347c 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx @@ -19,7 +19,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -63,7 +63,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx index 616010f93..a2cc036ec 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx @@ -21,7 +21,7 @@ import { DynamicSecretAwsIamCredentialType, DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { MetadataForm } from "../../DynamicSecretListView/MetadataForm"; @@ -122,7 +122,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx index 771523828..6c4ccd65b 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx @@ -18,7 +18,7 @@ import { Tooltip } from "@app/components/v2/Tooltip"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; const formSchema = z.object({ selectedUsers: z.array( @@ -66,7 +66,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx index 8f99d368c..0ef4d092d 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx @@ -19,7 +19,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; const formSchema = z.object({ @@ -66,7 +66,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CouchbaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CouchbaseInputForm.tsx index fd2eecaa6..1b42dc7e0 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CouchbaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CouchbaseInputForm.tsx @@ -24,7 +24,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; // Component for managing scopes and collections within a bucket @@ -265,7 +265,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx index abe14d82d..dd169727d 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -24,7 +24,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { Modal, ModalContent } from "@app/components/v2"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm"; import { AwsIamInputForm } from "./AwsIamInputForm"; @@ -51,7 +51,7 @@ type Props = { isOpen?: boolean; onToggle: (isOpen: boolean) => void; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; secretPath: string; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx index 51dcea68b..390909e81 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx @@ -20,7 +20,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; const authMethods = [ @@ -87,7 +87,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GcpIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GcpIamInputForm.tsx index f47273aa1..e7518830d 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GcpIamInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GcpIamInputForm.tsx @@ -8,7 +8,7 @@ import { createNotification } from "@app/components/notifications"; import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; const validateTTL = (val: string, ctx: z.RefinementCtx) => { if (!val) return; @@ -49,7 +49,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GithubInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GithubInputForm.tsx index 800bc677a..acac4e0aa 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GithubInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GithubInputForm.tsx @@ -16,7 +16,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -44,7 +44,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/KubernetesInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/KubernetesInputForm.tsx index 2b0838246..945e46777 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/KubernetesInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/KubernetesInputForm.tsx @@ -28,7 +28,7 @@ import { DynamicSecretProviders, KubernetesDynamicSecretCredentialType } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; enum RoleType { @@ -150,7 +150,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx index f2f5c9233..62f8cdd96 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx @@ -16,7 +16,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; enum CredentialType { @@ -89,7 +89,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx index 821730b08..05109e5a3 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx @@ -23,7 +23,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; const formSchema = z.object({ @@ -77,7 +77,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx index 5a89f4649..646755a8f 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx @@ -18,7 +18,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; const formSchema = z.object({ @@ -67,7 +67,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx index f80c374d8..1c2db7d2a 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx @@ -18,7 +18,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; const formSchema = z.object({ @@ -71,7 +71,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx index 4d0c51194..06bf4e41a 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx @@ -19,7 +19,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; const formSchema = z.object({ @@ -64,7 +64,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx index 413bbe0b6..507e67820 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx @@ -18,7 +18,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; const formSchema = z.object({ @@ -62,7 +62,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx index deea1ada1..38ddbc7f0 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx @@ -19,7 +19,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; const formSchema = z.object({ @@ -64,7 +64,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx index 3120d7cef..30f01c13f 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx @@ -18,7 +18,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; const formSchema = z.object({ @@ -62,7 +62,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx index 8bedbbaeb..b0c18fbd3 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx @@ -19,6 +19,7 @@ import { SecretInput, Select, SelectItem, + Switch, TextArea, Tooltip } from "@app/components/v2"; @@ -28,7 +29,7 @@ import { } from "@app/context/OrgPermissionContext/types"; import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; import { MetadataForm } from "../../DynamicSecretListView/MetadataForm"; @@ -66,6 +67,7 @@ const formSchema = z.object({ creationStatement: z.string().min(1), revocationStatement: z.string().min(1), renewStatement: z.string().optional(), + sslEnabled: z.boolean().optional(), ca: z.string().optional(), gatewayId: z.string().optional() }), @@ -108,7 +110,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; @@ -200,6 +202,7 @@ export const SqlDatabaseInputForm = ({ const createDynamicSecret = useCreateDynamicSecret(); const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); + const selectedClient = watch("provider.client"); const handleCreateDynamicSecret = async ({ name, @@ -458,13 +461,34 @@ export const SqlDatabaseInputForm = ({ />
+ {selectedClient === SqlProviders.MsSQL && ( +
+ ( + + + Encrypt Connection (SSL) + + + )} + /> +
+ )} ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx index 1ec9f8a0c..63680f524 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx @@ -13,7 +13,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; import { slugSchema } from "@app/lib/schemas"; enum ConfigType { @@ -66,7 +66,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/VerticaInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/VerticaInputForm.tsx index d5443b1ed..ca417adb6 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/VerticaInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/VerticaInputForm.tsx @@ -27,7 +27,7 @@ import { } from "@app/context/OrgPermissionContext/types"; import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv } from "@app/hooks/api/types"; const passwordRequirementsSchema = z .object({ @@ -91,7 +91,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environments: WorkspaceEnv[]; + environments: ProjectEnv[]; isSingleEnvironmentMode?: boolean; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateSecretImportForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateSecretImportForm.tsx index 9bdace648..68d2d8cd0 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateSecretImportForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateSecretImportForm.tsx @@ -14,7 +14,7 @@ import { SelectItem } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; -import { useSubscription, useWorkspace } from "@app/context"; +import { useProject, useSubscription } from "@app/context"; import { useCreateSecretImport } from "@app/hooks/api"; const typeSchema = z.object({ @@ -32,7 +32,7 @@ type TFormSchema = z.infer; type Props = { environment: string; - workspaceId: string; + projectId: string; secretPath?: string; // modal props isOpen?: boolean; @@ -43,7 +43,7 @@ type Props = { export const CreateSecretImportForm = ({ environment, - workspaceId, + projectId, secretPath = "/", isOpen, onClose, @@ -57,8 +57,8 @@ export const CreateSecretImportForm = ({ watch, formState: { isSubmitting } } = useForm({ resolver: zodResolver(typeSchema) }); - const { currentWorkspace } = useWorkspace(); - const environments = currentWorkspace?.environments || []; + const { currentProject } = useProject(); + const environments = currentProject?.environments || []; const selectedEnvironment = watch("environment"); const { subscription } = useSubscription(); @@ -77,7 +77,7 @@ export const CreateSecretImportForm = ({ await createSecretImport({ environment, - projectId: workspaceId, + projectId, path: secretPath, isReplication, import: { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/MoveSecretsModal.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/MoveSecretsModal.tsx index 606f23fac..05f00315d 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/MoveSecretsModal.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/MoveSecretsModal.tsx @@ -12,7 +12,7 @@ import { SelectItem } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { @@ -47,8 +47,8 @@ export const MoveSecretsModal = ({ popUp, handlePopUpToggle, onMoveApproved }: P formState: { isSubmitting } } = useForm({ resolver: zodResolver(formSchema) }); - const { currentWorkspace } = useWorkspace(); - const environments = currentWorkspace?.environments || []; + const { currentProject } = useProject(); + const environments = currentProject?.environments || []; const selectedEnvironment = watch("environment"); const handleFormSubmit = (data: TFormSchema) => { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx index 339612a89..5e4cee953 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx @@ -48,7 +48,7 @@ type Props = { env: Record> ) => void; environments?: { name: string; slug: string }[]; - workspaceId: string; + projectId: string; environment: string; secretPath: string; }; @@ -64,7 +64,7 @@ type SecretStructure = { export const ReplicateFolderFromBoard = ({ environments = [], - workspaceId, + projectId, isOpen, onToggle, onParsedEnv @@ -82,14 +82,14 @@ export const ReplicateFolderFromBoard = ({ const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath); const { data: accessibleSecrets } = useGetAccessibleSecrets({ - projectId: workspaceId, + projectId, secretPath: "/", environment: selectedEnvSlug.slug, recursive: true, filterByAction: shouldIncludeValues ? ProjectPermissionSecretActions.ReadValue : ProjectPermissionSecretActions.DescribeSecret, - options: { enabled: Boolean(workspaceId) && Boolean(selectedEnvSlug) && isOpen } + options: { enabled: Boolean(projectId) && Boolean(selectedEnvSlug) && isOpen } }); const restructureSecrets = useMemo(() => { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx index c3f5e74ee..f81cd7b2d 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx @@ -25,14 +25,14 @@ interface CommitFormProps { onCommit: (changes: PendingChanges, commitMessage: string) => Promise; isCommitting?: boolean; environment: string; - workspaceId: string; + projectId: string; secretPath: string; } interface ResourceChangeProps { change: PendingChange; environment: string; - workspaceId: string; + projectId: string; secretPath: string; } @@ -237,7 +237,7 @@ const RenderFolderChanges = ({ onDiscard, change }: RenderResourceProps) => { const ResourceChange: React.FC = ({ change, environment, - workspaceId, + projectId, secretPath }) => { const { removePendingChange } = useBatchModeActions(); @@ -245,7 +245,7 @@ const ResourceChange: React.FC = ({ const handleDeletePending = useCallback( (changeType: string, id: string) => { removePendingChange(id, changeType, { - workspaceId, + projectId, environment, secretPath }); @@ -272,7 +272,7 @@ export const CommitForm: React.FC = ({ onCommit, isCommitting = false, environment, - workspaceId, + projectId, secretPath }) => { const { isBatchMode, pendingChanges, totalChangesCount } = useBatchMode(); @@ -291,7 +291,7 @@ export const CommitForm: React.FC = ({ } await onCommit(pendingChanges, commitMessage); clearAllPendingChanges({ - workspaceId, + projectId, environment, secretPath }); @@ -335,9 +335,7 @@ export const CommitForm: React.FC = ({
+ {selectedClient === SqlProviders.MsSQL && ( +
+ ( + + + Encrypt Connection (SSL) + + + )} + /> +
+ )} ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx index 66fd3a878..c9a2c1b62 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx @@ -24,12 +24,12 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionActions, ProjectPermissionSub, - useSubscription, - useWorkspace + useProject, + useSubscription } from "@app/context"; import { usePopUp } from "@app/hooks"; -import { workspaceKeys } from "@app/hooks/api"; -import { WorkspaceEnv } from "@app/hooks/api/workspace/types"; +import { projectKeys } from "@app/hooks/api"; +import { ProjectEnv } from "@app/hooks/api/projects/types"; import { AddEnvironmentModal } from "@app/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal"; import { CompareEnvironments } from "../CompareEnvironments"; @@ -45,7 +45,7 @@ type Props = { const TABS_TO_SHOW = 5; export const EnvironmentTabs = ({ secretPath }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const currentEnv = useParams({ from: ROUTE_PATHS.SecretManager.SecretDashboardPage.id, select: (el) => el.envSlug @@ -54,8 +54,8 @@ export const EnvironmentTabs = ({ secretPath }: Props) => { const { subscription } = useSubscription(); const isMoreEnvironmentsAllowed = - subscription?.environmentLimit && currentWorkspace?.environments - ? currentWorkspace.environments.length < subscription.environmentLimit + subscription?.environmentLimit && currentProject?.environments + ? currentProject.environments.length < subscription.environmentLimit : true; const [isNavigating, setIsNavigating] = useState(false); @@ -68,20 +68,20 @@ export const EnvironmentTabs = ({ secretPath }: Props) => { "upgradePlan" ] as const); - const selectedIndex = currentWorkspace.environments.findIndex((env) => env.slug === currentEnv); + const selectedIndex = currentProject.environments.findIndex((env) => env.slug === currentEnv); - let tabEnvironments: WorkspaceEnv[]; - let dropdownEnvironments: WorkspaceEnv[]; + let tabEnvironments: ProjectEnv[]; + let dropdownEnvironments: ProjectEnv[]; if (selectedIndex < TABS_TO_SHOW) { - tabEnvironments = currentWorkspace.environments.slice(0, TABS_TO_SHOW); - dropdownEnvironments = currentWorkspace.environments.slice(TABS_TO_SHOW); + tabEnvironments = currentProject.environments.slice(0, TABS_TO_SHOW); + dropdownEnvironments = currentProject.environments.slice(TABS_TO_SHOW); } else { tabEnvironments = [ - ...currentWorkspace.environments.slice(0, TABS_TO_SHOW - 1), - currentWorkspace.environments[selectedIndex] + ...currentProject.environments.slice(0, TABS_TO_SHOW - 1), + currentProject.environments[selectedIndex] ]; - dropdownEnvironments = currentWorkspace.environments + dropdownEnvironments = currentProject.environments .slice(TABS_TO_SHOW - 1) .filter((env) => env.slug !== currentEnv); } @@ -96,7 +96,7 @@ export const EnvironmentTabs = ({ secretPath }: Props) => { to: ROUTE_PATHS.SecretManager.SecretDashboardPage.path, params: { envSlug, - projectId: currentWorkspace.id + projectId: currentProject.id }, search: (prev) => prev }); @@ -199,7 +199,7 @@ export const EnvironmentTabs = ({ secretPath }: Props) => { )} - {currentWorkspace.environments.length > 1 && ( + {currentProject.environments.length > 1 && (
@@ -232,7 +232,7 @@ export const EnvironmentTabs = ({ secretPath }: Props) => { onOpenChange={(isOpen) => handlePopUpToggle("createEnvironment", isOpen)} onComplete={async (env) => { await queryClient.refetchQueries({ - queryKey: workspaceKeys.getWorkspaceById(currentWorkspace.id) + queryKey: projectKeys.getProjectById(currentProject.id) }); handleSelect(env.slug); }} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx index fd65bc639..e35f62bf7 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx @@ -33,7 +33,7 @@ import { FolderForm } from "../ActionBar/FolderForm"; type Props = { folders?: TSecretFolder[]; environment: string; - workspaceId: string; + projectId: string; secretPath?: string; onNavigateToFolder: (path: string) => void; canNavigate: boolean; @@ -42,7 +42,7 @@ type Props = { export const FolderListView = ({ folders = [], environment, - workspaceId, + projectId, secretPath = "/", onNavigateToFolder, canNavigate @@ -89,7 +89,7 @@ export const FolderListView = ({ }; addPendingChange(updatedCreate, { - workspaceId, + projectId, environment, secretPath }); @@ -106,7 +106,7 @@ export const FolderListView = ({ }; addPendingChange(updateChange, { - workspaceId, + projectId, environment, secretPath }); @@ -121,7 +121,7 @@ export const FolderListView = ({ name: newFolderName, path: secretPath, environment, - projectId: workspaceId, + projectId, description: newFolderDescription }); handlePopUpClose("updateFolder"); @@ -140,7 +140,7 @@ export const FolderListView = ({ const handleDeletePending = (id: string) => { removePendingChange(id, "folder", { - workspaceId, + projectId, environment, secretPath }); @@ -161,7 +161,7 @@ export const FolderListView = ({ }; addPendingChange(pendingFolderDelete, { - workspaceId, + projectId, environment, secretPath }); @@ -174,7 +174,7 @@ export const FolderListView = ({ folderId: folderData.id, path: secretPath, environment, - projectId: workspaceId + projectId }); handlePopUpClose("deleteFolder"); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/CopySecretsFromBoard.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/CopySecretsFromBoard.tsx index 3e09a3857..de62d5856 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/CopySecretsFromBoard.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/CopySecretsFromBoard.tsx @@ -46,14 +46,14 @@ type Props = { onToggle: (isOpen: boolean) => void; onParsedEnv: (env: Record) => void; environments?: { name: string; slug: string }[]; - workspaceId: string; + projectId: string; environment: string; secretPath: string; }; export const CopySecretsFromBoard = ({ environments = [], - workspaceId, + projectId, environment, secretPath, isOpen, @@ -81,7 +81,7 @@ export const CopySecretsFromBoard = ({ const { data: accessibleSecrets, isPending: isAccessibleSecretsLoading } = useGetAccessibleSecrets({ - projectId: workspaceId, + projectId, secretPath: debouncedEnvCopySecretPath, environment: selectedEnvSlug.slug, filterByAction: shouldIncludeValues @@ -89,7 +89,7 @@ export const CopySecretsFromBoard = ({ : ProjectPermissionSecretActions.DescribeSecret, options: { enabled: - Boolean(workspaceId) && + Boolean(projectId) && Boolean(selectedEnvSlug) && Boolean(debouncedEnvCopySecretPath) && isOpen diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx index d7cb87da7..9799d17ad 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx @@ -53,7 +53,7 @@ type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv }; type Props = { isSmaller: boolean; environments?: { name: string; slug: string }[]; - workspaceId: string; + projectId: string; environment: string; secretPath: string; isProtectedBranch?: boolean; @@ -140,7 +140,7 @@ const MatrixImportModalTableRow = ({ export const SecretDropzone = ({ isSmaller, environments = [], - workspaceId, + projectId, environment, secretPath, isProtectedBranch = false @@ -196,7 +196,7 @@ export const SecretDropzone = ({ const { secrets: existingSecrets } = await fetchDashboardProjectSecretsByKeys({ secretPath, environment, - projectId: workspaceId, + projectId, keys: envSecretKeys }); @@ -334,7 +334,7 @@ export const SecretDropzone = ({ if (Object.keys(create || {}).length) { await createSecretBatch({ secretPath, - workspaceId, + projectId, environment, secrets: Object.entries(create).map(([secretKey, secData]) => ({ type: SecretType.Shared, @@ -347,7 +347,7 @@ export const SecretDropzone = ({ if (Object.keys(update || {}).length) { await updateSecretBatch({ secretPath, - workspaceId, + projectId, environment, secrets: Object.entries(update).map(([secretKey, secData]) => ({ type: SecretType.Shared, @@ -358,13 +358,13 @@ export const SecretDropzone = ({ }); } queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretApprovalRequestKeys.count({ workspaceId }) + queryKey: secretApprovalRequestKeys.count({ projectId }) }); handlePopUpClose("confirmUpload"); createNotification({ @@ -464,7 +464,7 @@ export const SecretDropzone = ({ onParsedEnv={handleParsedEnv} environment={environment} environments={environments} - workspaceId={workspaceId} + projectId={projectId} secretPath={secretPath} isSmaller={isSmaller} /> diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem.tsx index 44886c21f..9fc8cc98f 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem.tsx @@ -21,7 +21,7 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { useToggle } from "@app/hooks"; import { useResyncSecretReplication } from "@app/hooks/api"; import { TSecretImport } from "@app/hooks/api/types"; @@ -82,7 +82,7 @@ export const SecretImportItem = ({ lastReplicated, importEnv } = secretImport as TSecretImport; - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [isExpanded, setIsExpanded] = useToggle(); const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({ id @@ -118,7 +118,7 @@ export const SecretImportItem = ({ id, environment, path: secretPath, - projectId: currentWorkspace?.id || "" + projectId: currentProject?.id || "" }); createNotification({ text: "Please refresh the dashboard to view changes", diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView.tsx index e212d6da9..d99295286 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView.tsx @@ -18,7 +18,7 @@ import { usePopUp } from "@app/hooks"; import { useDeleteSecretImport, useUpdateSecretImport } from "@app/hooks/api"; import { ReservedFolders } from "@app/hooks/api/secretFolders/types"; import { TSecretImport } from "@app/hooks/api/secretImports/types"; -import { SecretV3RawSanitized, WorkspaceEnv } from "@app/hooks/api/types"; +import { ProjectEnv, SecretV3RawSanitized } from "@app/hooks/api/types"; import { formatReservedPaths } from "@app/lib/fn/string"; import { SecretImportItem } from "./SecretImportItem"; @@ -26,7 +26,7 @@ import { SecretImportItem } from "./SecretImportItem"; const SECRET_IN_DASHBOARD = "Present In Dashboard"; type TImportedSecrets = Array<{ - environmentInfo: WorkspaceEnv; + environmentInfo: ProjectEnv; secretPath: string; folderId: string; secrets: SecretV3RawSanitized[]; @@ -86,7 +86,7 @@ export const computeImportedSecretRows = ( type Props = { environment: string; - workspaceId: string; + projectId: string; secretPath?: string; secretImports?: TSecretImport[]; isFetching?: boolean; @@ -98,7 +98,7 @@ type Props = { export const SecretImportListView = ({ secretImports, environment, - workspaceId, + projectId, secretPath, importedSecrets, // secrets = [], @@ -132,7 +132,7 @@ export const SecretImportListView = ({ const { id: secretImportId } = popUp.deleteSecretImport?.data as { id: string }; try { await deleteSecretImport({ - projectId: workspaceId, + projectId, environment, path: secretPath, id: secretImportId @@ -158,7 +158,7 @@ export const SecretImportListView = ({ const newImportOrder = arrayMove(items, oldIndex, newIndex); setItems(newImportOrder); updateSecretImport({ - projectId: workspaceId, + projectId, environment, path: secretPath, id: active.id as string, diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx index 70010fe81..a1ed019f1 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx @@ -4,7 +4,7 @@ import { faFileImport, faKey, faSync, faWarning } from "@fortawesome/free-solid- import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types"; enum ItemType { @@ -44,7 +44,7 @@ export const CollapsibleSecretImports: React.FC = secretsToDelete, onlyReferences }) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const truncatePath = (path: string, maxLength = 24): string => { if (path.length <= maxLength) return path; @@ -62,7 +62,7 @@ export const CollapsibleSecretImports: React.FC = const handlePathClick = (item: FlatItem) => { if (item.type === ItemType.SecretSync) { window.open( - `/secret-manager/${currentWorkspace.id}/integrations/secret-syncs/${item.destination}/${item.id}`, + `/secret-manager/${currentProject.id}/integrations/secret-syncs/${item.destination}/${item.id}`, "_blank", "noopener,noreferrer" ); @@ -78,7 +78,7 @@ export const CollapsibleSecretImports: React.FC = } const encodedPath = encodeURIComponent(pathToNavigate); window.open( - `/secret-manager/${currentWorkspace.id}/secrets/${item.environment.slug}?secretPath=${encodedPath}`, + `/secret-manager/${currentProject.id}/secrets/${item.environment.slug}?secretPath=${encodedPath}`, "_blank" ); }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm.tsx index 78e79d373..9dc12a1a1 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm.tsx @@ -21,7 +21,7 @@ import { SelectItem, TextArea } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useGetWorkspaceUsers } from "@app/hooks/api"; import { dashboardKeys } from "@app/hooks/api/dashboard/queries"; import { useCreateReminder, useDeleteReminder } from "@app/hooks/api/reminders"; @@ -52,7 +52,7 @@ interface ReminderFormProps { isOpen: boolean; reminderId?: string; onOpenChange: () => void; - workspaceId: string; + projectId: string; environment: string; secretPath: string; secretId: string; @@ -118,8 +118,8 @@ const useReminderForm = (reminderData?: Reminder) => { // Custom hook for workspace members const useWorkspaceMembers = () => { - const { currentWorkspace } = useWorkspace(); - const { data: members = [] } = useGetWorkspaceUsers(currentWorkspace?.id); + const { currentProject } = useProject(); + const { data: members = [] } = useGetWorkspaceUsers(currentProject?.id); const memberOptions = useMemo( (): RecipientOption[] => @@ -137,7 +137,7 @@ const useWorkspaceMembers = () => { export const CreateReminderForm = ({ isOpen, onOpenChange, - workspaceId, + projectId, environment, secretPath, secretId, @@ -185,12 +185,12 @@ export const CreateReminderForm = ({ const invalidateQueries = () => { queryClient.invalidateQueries({ queryKey: dashboardKeys.getDashboardSecrets({ - projectId: workspaceId, + projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ queryKey: reminderKeys.getReminder(secretId) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 953176ff5..f00e1b72b 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -55,8 +55,8 @@ import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { ProjectPermissionActions, ProjectPermissionSub, - useProjectPermission, - useWorkspace + useProject, + useProjectPermission } from "@app/context"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { getProjectBaseURL } from "@app/helpers/project"; @@ -120,7 +120,7 @@ export const SecretDetailSidebar = ({ ] as const); const { permission } = useProjectPermission(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const tagFields = useFieldArray({ control, @@ -187,7 +187,7 @@ export const SecretDetailSidebar = ({ }); const { data: secretAccessList, isPending } = useGetSecretAccessList({ - workspaceId: currentWorkspace.id, + projectId: currentProject.id, environment, secretPath, secretKey @@ -252,9 +252,9 @@ export const SecretDetailSidebar = ({ ) => { switch (actorType) { case ActorType.USER: - return `/projects/secret-management/${currentWorkspace.id}/members/${membershipId}`; + return `/projects/secret-management/${currentProject.id}/members/${membershipId}`; case ActorType.IDENTITY: - return `/projects/secret-management/${currentWorkspace.id}/identities/${actorId}`; + return `/projects/secret-management/${currentProject.id}/identities/${actorId}`; default: return null; } @@ -367,16 +367,14 @@ export const SecretDetailSidebar = ({ />
); diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal.tsx index 0832dfe9d..611504a14 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal.tsx @@ -4,15 +4,15 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateWsEnvironment } from "@app/hooks/api"; -import { WorkspaceEnv } from "@app/hooks/api/workspace/types"; +import { ProjectEnv } from "@app/hooks/api/projects/types"; import { slugSchema } from "@app/lib/schemas"; type Props = { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; - onComplete?: (environment: WorkspaceEnv) => void; + onComplete?: (environment: ProjectEnv) => void; }; const schema = z.object({ @@ -25,11 +25,11 @@ const schema = z.object({ export type FormData = z.infer; type ContentProps = { - onComplete: (environment: WorkspaceEnv) => void; + onComplete: (environment: ProjectEnv) => void; }; const Content = ({ onComplete }: ContentProps) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync, isPending } = useCreateWsEnvironment(); const { control, handleSubmit } = useForm({ resolver: zodResolver(schema) @@ -37,10 +37,10 @@ const Content = ({ onComplete }: ContentProps) => { const onFormSubmit = async ({ environmentName, environmentSlug }: FormData) => { try { - if (!currentWorkspace?.id) return; + if (!currentProject?.id) return; const env = await mutateAsync({ - workspaceId: currentWorkspace.id, + projectId: currentProject.id, name: environmentName, slug: environmentSlug }); diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentSection.tsx index 34acf0c7b..016266e69 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentSection.tsx @@ -8,9 +8,9 @@ import { Button, DeleteActionModal } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, + useProject, useProjectPermission, - useSubscription, - useWorkspace + useSubscription } from "@app/context"; import { useDeleteWsEnvironment } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -21,14 +21,14 @@ import { UpdateEnvironmentModal } from "./UpdateEnvironmentModal"; export const EnvironmentSection = () => { const { subscription } = useSubscription(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { permission } = useProjectPermission(); const deleteWsEnvironment = useDeleteWsEnvironment(); const isMoreEnvironmentsAllowed = - subscription?.environmentLimit && currentWorkspace?.environments - ? currentWorkspace.environments.length < subscription.environmentLimit + subscription?.environmentLimit && currentProject?.environments + ? currentProject.environments.length < subscription.environmentLimit : true; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -40,10 +40,10 @@ export const EnvironmentSection = () => { const onEnvDeleteSubmit = async (id: string) => { try { - if (!currentWorkspace?.id) return; + if (!currentProject?.id) return; await deleteWsEnvironment.mutateAsync({ - workspaceId: currentWorkspace.id, + projectId: currentProject.id, id }); diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentTable.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentTable.tsx index 0626db281..0908d8377 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentTable.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentTable.tsx @@ -18,8 +18,8 @@ import { import { ProjectPermissionActions, ProjectPermissionSub, - useSubscription, - useWorkspace + useProject, + useSubscription } from "@app/context"; import { useUpdateWsEnvironment } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -40,17 +40,17 @@ type Props = { }; export const EnvironmentTable = ({ handlePopUpOpen }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { subscription } = useSubscription(); const updateEnvironment = useUpdateWsEnvironment(); const handleReorderEnv = async (id: string, position: number) => { try { - if (!currentWorkspace?.id) return; + if (!currentProject?.id) return; await updateEnvironment.mutateAsync({ - workspaceId: currentWorkspace.id, + projectId: currentProject.id, id, position }); @@ -69,13 +69,13 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => { }; const isMoreEnvironmentsAllowed = - subscription?.environmentLimit && currentWorkspace?.environments - ? currentWorkspace.environments.length <= subscription.environmentLimit + subscription?.environmentLimit && currentProject?.environments + ? currentProject.environments.length <= subscription.environmentLimit : true; const environmentsOverPlanLimit = - subscription?.environmentLimit && currentWorkspace?.environments - ? Math.max(0, currentWorkspace.environments.length - subscription.environmentLimit) + subscription?.environmentLimit && currentProject?.environments + ? Math.max(0, currentProject.environments.length - subscription.environmentLimit) : 0; return ( @@ -89,7 +89,7 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => { - {currentWorkspace.environments.map(({ name, slug, id }, pos) => ( + {currentProject.environments.map(({ name, slug, id }, pos) => ( {name} {slug} @@ -102,15 +102,12 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => { - handleReorderEnv( - id, - Math.min(currentWorkspace.environments.length, pos + 2) - ) + handleReorderEnv(id, Math.min(currentProject.environments.length, pos + 2)) } colorSchema="primary" variant="plain" ariaLabel="update" - isDisabled={pos === currentWorkspace.environments.length - 1 || !isAllowed} + isDisabled={pos === currentProject.environments.length - 1 || !isAllowed} > @@ -183,7 +180,7 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => { ))} - {currentWorkspace.environments?.length === 0 && ( + {currentProject.environments?.length === 0 && ( diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/UpdateEnvironmentModal.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/UpdateEnvironmentModal.tsx index f40091303..e2d64ee72 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/UpdateEnvironmentModal.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/UpdateEnvironmentModal.tsx @@ -4,7 +4,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useUpdateWsEnvironment } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { slugSchema } from "@app/lib/schemas"; @@ -23,7 +23,7 @@ const schema = z.object({ export type FormData = z.infer; export const UpdateEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync, isPending } = useUpdateWsEnvironment(); const { control, handleSubmit, reset } = useForm({ resolver: zodResolver(schema), @@ -34,10 +34,10 @@ export const UpdateEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpTog const onFormSubmit = async ({ name, slug }: FormData) => { try { - if (!currentWorkspace?.id) return; + if (!currentProject?.id) return; await mutateAsync({ - workspaceId: currentWorkspace.id, + projectId: currentProject.id, name, slug, id: oldEnvId diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx index 288500d26..bd8d15887 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx @@ -4,9 +4,9 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input } from "@app/components/v2"; -import { useProjectPermission, useWorkspace } from "@app/context"; +import { useProject, useProjectPermission } from "@app/context"; +import { useUpdateProject } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; -import { useUpdateWorkspaceVersionLimit } from "@app/hooks/api/workspace/queries"; const formSchema = z.object({ pitVersionLimit: z.coerce.number().min(1).max(100) @@ -15,9 +15,9 @@ const formSchema = z.object({ type TForm = z.infer; export const PointInTimeVersionLimitSection = () => { - const { mutateAsync: updatePitVersion } = useUpdateWorkspaceVersionLimit(); + const { mutateAsync: updateProject } = useUpdateProject(); - const { currentWorkspace } = useWorkspace(); + const { currentProject, projectId } = useProject(); const { membership } = useProjectPermission(); const { @@ -27,17 +27,17 @@ export const PointInTimeVersionLimitSection = () => { } = useForm({ resolver: zodResolver(formSchema), values: { - pitVersionLimit: currentWorkspace?.pitVersionLimit || 10 + pitVersionLimit: currentProject?.pitVersionLimit || 10 } }); - if (!currentWorkspace) return null; + if (!currentProject) return null; const handleVersionLimitSubmit = async ({ pitVersionLimit }: TForm) => { try { - await updatePitVersion({ + await updateProject({ pitVersionLimit, - projectSlug: currentWorkspace.slug + projectId }); createNotification({ diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx index 5869aa127..a7c6bbc36 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx @@ -7,7 +7,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, IconButton, Input } from "@app/components/v2"; -import { useProjectPermission, useWorkspace } from "@app/context"; +import { useProject, useProjectPermission } from "@app/context"; import { useUpdateProject } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; @@ -23,7 +23,7 @@ const formSchema = z.object({ type TForm = z.infer; export const SecretDetectionIgnoreValuesSection = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { membership } = useProjectPermission(); const { mutateAsync: updateProject } = useUpdateProject(); @@ -45,19 +45,19 @@ export const SecretDetectionIgnoreValuesSection = () => { }); useEffect(() => { - const existingIgnoreValues = currentWorkspace?.secretDetectionIgnoreValues || []; + const existingIgnoreValues = currentProject?.secretDetectionIgnoreValues || []; reset({ ignoreValues: existingIgnoreValues.length > 0 ? existingIgnoreValues.map((value) => ({ value })) : [{ value: "" }] // Show one empty field by default }); - }, [currentWorkspace?.secretDetectionIgnoreValues, reset]); + }, [currentProject?.secretDetectionIgnoreValues, reset]); const handleIgnoreValuesSubmit = async ({ ignoreValues }: TForm) => { try { await updateProject({ - projectID: currentWorkspace.id, + projectId: currentProject.id, secretDetectionIgnoreValues: ignoreValues.map((item) => item.value) }); @@ -75,7 +75,7 @@ export const SecretDetectionIgnoreValuesSection = () => { const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin); - if (!currentWorkspace) return null; + if (!currentProject) return null; return (
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx index 0dd145e41..83639d204 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx @@ -3,11 +3,11 @@ import { useState } from "react"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Checkbox } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; -import { useUpdateProject } from "@app/hooks/api/workspace/queries"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; +import { useUpdateProject } from "@app/hooks/api/projects/queries"; export const SecretSharingSection = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync: updateProject } = useUpdateProject(); const [isLoading, setIsLoading] = useState(false); @@ -16,13 +16,13 @@ export const SecretSharingSection = () => { setIsLoading(true); try { - if (!currentWorkspace?.id) { + if (!currentProject?.id) { setIsLoading(false); return; } await updateProject({ - projectID: currentWorkspace.id, + projectId: currentProject.id, secretSharing: state }); @@ -50,7 +50,7 @@ export const SecretSharingSection = () => { handleToggle(state as boolean)} > This feature enables your project members to securely share secrets. diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/SecretSnapshotsLegacySection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/SecretSnapshotsLegacySection.tsx index 69c2bcbfd..90b866e96 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/SecretSnapshotsLegacySection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/SecretSnapshotsLegacySection.tsx @@ -3,11 +3,11 @@ import { useState } from "react"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Checkbox } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; -import { useUpdateProject } from "@app/hooks/api/workspace/queries"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; +import { useUpdateProject } from "@app/hooks/api/projects/queries"; export const SecretSnapshotsLegacySection = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync: updateProject } = useUpdateProject(); const [isLoading, setIsLoading] = useState(false); @@ -16,13 +16,13 @@ export const SecretSnapshotsLegacySection = () => { setIsLoading(true); try { - if (!currentWorkspace?.id) { + if (!currentProject?.id) { setIsLoading(false); return; } await updateProject({ - projectID: currentWorkspace.id, + projectId: currentProject.id, showSnapshotsLegacy: state }); @@ -50,7 +50,7 @@ export const SecretSnapshotsLegacySection = () => { handleToggle(state as boolean)} > This feature enables your project members to view secret snapshots in the legacy diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/AddSecretTagModal.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/AddSecretTagModal.tsx index 96c667050..1d4c8e492 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/AddSecretTagModal.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/AddSecretTagModal.tsx @@ -4,7 +4,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateWsTag } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { slugSchema } from "@app/lib/schemas"; @@ -27,7 +27,7 @@ type Props = { }; export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const createWsTag = useCreateWsTag(); const { control, @@ -40,10 +40,10 @@ export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle } const onFormSubmit = async ({ slug }: FormData) => { try { - if (!currentWorkspace?.id) return; + if (!currentProject?.id) return; await createWsTag.mutateAsync({ - workspaceID: currentWorkspace?.id, + projectId: currentProject?.id, tagSlug: slug, tagColor: "" }); @@ -73,7 +73,7 @@ export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle } }} >
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/SecretTagsSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/SecretTagsSection.tsx index 26be8eb17..55da0f03b 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/SecretTagsSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/SecretTagsSection.tsx @@ -7,8 +7,8 @@ import { Button, DeleteActionModal } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, - useProjectPermission, - useWorkspace + useProject, + useProjectPermission } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteWsTag } from "@app/hooks/api"; @@ -23,7 +23,7 @@ export const SecretTagsSection = (): JSX.Element => { "CreateSecretTag", "deleteTagConfirmation" ] as const); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { permission } = useProjectPermission(); const deleteWsTag = useDeleteWsTag(); @@ -31,7 +31,7 @@ export const SecretTagsSection = (): JSX.Element => { const onDeleteApproved = async () => { try { await deleteWsTag.mutateAsync({ - projectId: currentWorkspace?.id || "", + projectId: currentProject?.id || "", tagID: (popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id }); diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/SecretTagsTable.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/SecretTagsTable.tsx index d0633eb12..15d8ef7cb 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/SecretTagsTable.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/SecretTagsTable.tsx @@ -24,7 +24,7 @@ import { THead, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { getUserTablePreference, PreferenceKey, @@ -53,8 +53,8 @@ enum TagsOrderBy { } export const SecretTagsTable = ({ handlePopUpOpen }: Props) => { - const { currentWorkspace } = useWorkspace(); - const { data: tags = [], isPending } = useGetWsTags(currentWorkspace?.id ?? ""); + const { currentProject } = useProject(); + const { data: tags = [], isPending } = useGetWsTags(currentProject?.id ?? ""); const { search, diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WebhooksTab/WebhooksTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WebhooksTab/WebhooksTab.tsx index 8c8d75f47..79fc7aafc 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WebhooksTab/WebhooksTab.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WebhooksTab/WebhooksTab.tsx @@ -18,7 +18,7 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { withProjectPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks"; import { @@ -35,14 +35,14 @@ export const WebhooksTab = withProjectPermission( () => { const { t } = useTranslation(); - const { currentWorkspace } = useWorkspace(); - const workspaceId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ "addWebhook", "deleteWebhook" ] as const); - const { data: webhooks, isPending: isWebhooksLoading } = useGetWebhooks(workspaceId); + const { data: webhooks, isPending: isWebhooksLoading } = useGetWebhooks(projectId); // mutation const { mutateAsync: createWebhook } = useCreateWebhook(); @@ -62,7 +62,7 @@ export const WebhooksTab = withProjectPermission( try { await createWebhook({ ...data, - workspaceId + projectId }); handlePopUpClose("addWebhook"); createNotification({ @@ -82,7 +82,7 @@ export const WebhooksTab = withProjectPermission( try { await updateWebhook({ webhookId, - workspaceId, + projectId, isDisabled }); createNotification({ @@ -103,7 +103,7 @@ export const WebhooksTab = withProjectPermission( const webhookId = popUp?.deleteWebhook?.data as string; await deleteWebhook({ webhookId, - workspaceId + projectId }); handlePopUpClose("deleteWebhook"); createNotification({ @@ -123,7 +123,7 @@ export const WebhooksTab = withProjectPermission( try { await testWebhook({ webhookId, - workspaceId + projectId }); createNotification({ type: "success", @@ -302,7 +302,7 @@ export const WebhooksTab = withProjectPermission(
handlePopUpToggle("addWebhook", isOpen)} onCreateWebhook={handleWebhookCreate} diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx index 5b5f92bbb..ce8a3a70c 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx @@ -15,7 +15,7 @@ import { THead, Tr } from "@app/components/v2"; -import { OrgPermissionActions, OrgPermissionSubjects, useWorkspace } from "@app/context"; +import { OrgPermissionActions, OrgPermissionSubjects, useProject } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteProjectWorkflowIntegration, @@ -47,16 +47,16 @@ export const WorkflowIntegrationTab = () => { "editIntegration" ] as const); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: slackConfig, isPending: isSlackConfigLoading } = useGetWorkspaceWorkflowIntegrationConfig({ - workspaceId: currentWorkspace?.id ?? "", + projectId: currentProject?.id ?? "", integration: WorkflowIntegrationPlatform.SLACK }); const { data: microsoftTeamsConfig, isPending: isMicrosoftTeamsConfigLoading } = useGetWorkspaceWorkflowIntegrationConfig({ - workspaceId: currentWorkspace?.id ?? "", + projectId: currentProject?.id ?? "", integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS }); @@ -66,12 +66,12 @@ export const WorkflowIntegrationTab = () => { integrationType: WorkflowIntegrationPlatform, integrationId: string ) => { - if (!currentWorkspace.id) { + if (!currentProject.id) { return; } await deleteIntegration({ - projectId: currentWorkspace?.id ?? "", + projectId: currentProject?.id ?? "", integration: integrationType, integrationId }); diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/AddWorkflowIntegrationModal.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/AddWorkflowIntegrationModal.tsx index 8601c1c02..28a3c83d3 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/AddWorkflowIntegrationModal.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/AddWorkflowIntegrationModal.tsx @@ -6,7 +6,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { twMerge } from "tailwind-merge"; import { Modal, ModalContent } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useGetWorkspaceWorkflowIntegrationConfig } from "@app/hooks/api"; import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types"; @@ -40,14 +40,14 @@ export const AddWorkflowIntegrationModal = ({ isOpen, onToggle }: Props) => { const [wizardStep, setWizardStep] = useState(WizardSteps.SelectPlatform); const [selectedPlatform, setSelectedPlatform] = useState(null); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: microsoftTeamsConfig } = useGetWorkspaceWorkflowIntegrationConfig({ - workspaceId: currentWorkspace?.id ?? "", + projectId: currentProject?.id ?? "", integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS }); const { data: slackConfig } = useGetWorkspaceWorkflowIntegrationConfig({ - workspaceId: currentWorkspace?.id ?? "", + projectId: currentProject?.id ?? "", integration: WorkflowIntegrationPlatform.SLACK }); diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsIntegrationForm.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsIntegrationForm.tsx index aab932b8d..2bfe8cece 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsIntegrationForm.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsIntegrationForm.tsx @@ -19,7 +19,7 @@ import { SelectItem, Switch } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub @@ -95,13 +95,13 @@ type Props = { }; export const MicrosoftTeamsIntegrationForm = ({ onClose }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: microsoftTeamsConfig } = useGetWorkspaceWorkflowIntegrationConfig({ - workspaceId: currentWorkspace?.id ?? "", + projectId: currentProject?.id ?? "", integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS }); const { data: microsoftTeamsIntegrations } = useGetMicrosoftTeamsIntegrations( - currentWorkspace?.orgId + currentProject?.orgId ); const { mutateAsync: updateProjectMicrosoftTeamsConfig } = @@ -131,12 +131,12 @@ export const MicrosoftTeamsIntegrationForm = ({ onClose }: Props) => { const handleIntegrationSave = async (data: TMicrosoftTeamsConfigForm) => { try { - if (!currentWorkspace) { + if (!currentProject) { return; } await updateProjectMicrosoftTeamsConfig({ - workspaceId: currentWorkspace.id, + projectId: currentProject.id, isAccessRequestNotificationEnabled: data.isAccessRequestNotificationEnabled, isSecretRequestNotificationEnabled: data.isSecretRequestNotificationEnabled, ...(data.isAccessRequestNotificationEnabled && { diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx index 6bcaa6ac1..26cae7f61 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx @@ -19,7 +19,7 @@ import { SelectItem, Switch } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub @@ -47,12 +47,12 @@ type Props = { }; export const SlackIntegrationForm = ({ onClose }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: slackConfig } = useGetWorkspaceWorkflowIntegrationConfig({ - workspaceId: currentWorkspace?.id ?? "", + projectId: currentProject?.id ?? "", integration: WorkflowIntegrationPlatform.SLACK }); - const { data: workflowIntegrations } = useGetWorkflowIntegrations(currentWorkspace?.orgId); + const { data: workflowIntegrations } = useGetWorkflowIntegrations(currentProject?.orgId); const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectWorkflowIntegrationConfig(); const slackIntegrations = workflowIntegrations?.filter( @@ -77,13 +77,13 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { const handleIntegrationSave = async (data: TSlackConfigForm) => { try { - if (!currentWorkspace) { + if (!currentProject) { return; } await updateProjectSlackConfig({ ...data, - workspaceId: currentWorkspace.id, + projectId: currentProject.id, integration: WorkflowIntegrationPlatform.SLACK, integrationId: data.slackIntegrationId, accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "), diff --git a/frontend/src/pages/secret-manager/integrations/AwsParameterStoreAuthorizePage/AwsParameterStoreAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/AwsParameterStoreAuthorizePage/AwsParameterStoreAuthorizePage.tsx index e67e1e328..16493ad0f 100644 --- a/frontend/src/pages/secret-manager/integrations/AwsParameterStoreAuthorizePage/AwsParameterStoreAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AwsParameterStoreAuthorizePage/AwsParameterStoreAuthorizePage.tsx @@ -16,7 +16,7 @@ import { Select, SelectItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; enum AwsAuthType { @@ -40,7 +40,7 @@ type TForm = z.infer; export const AWSParameterStoreAuthorizeIntegrationPage = () => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync } = useSaveIntegrationAccessToken(); const { control, handleSubmit, formState, watch } = useForm({ @@ -55,7 +55,7 @@ export const AWSParameterStoreAuthorizeIntegrationPage = () => { const handleFormSubmit = async (data: TForm) => { try { const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "aws-parameter-store", ...(data.type === AwsAuthType.AssumeRole ? { @@ -70,7 +70,7 @@ export const AWSParameterStoreAuthorizeIntegrationPage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/aws-parameter-store/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/AwsParameterStoreConfigurePage/AwsParamterStoreConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/AwsParameterStoreConfigurePage/AwsParamterStoreConfigurePage.tsx index bbdd2d71b..5b5d97f6a 100644 --- a/frontend/src/pages/secret-manager/integrations/AwsParameterStoreConfigurePage/AwsParamterStoreConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AwsParameterStoreConfigurePage/AwsParamterStoreConfigurePage.tsx @@ -26,7 +26,7 @@ import { Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries"; @@ -77,7 +77,7 @@ export const AWSParameterStoreConfigurePage = () => { from: ROUTE_PATHS.SecretManager.Integratons.AwsParameterStoreConfigurePage.id, select: (el) => el.integrationAuthId }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: integrationAuth, isPending: isintegrationAuthLoading } = useGetIntegrationAuthById( (integrationAuthId as string) ?? "" @@ -97,11 +97,11 @@ export const AWSParameterStoreConfigurePage = () => { const [kmsKeyId, setKmsKeyId] = useState(""); useEffect(() => { - if (currentWorkspace) { - setSelectedSourceEnvironment(currentWorkspace.environments[0].slug); + if (currentProject) { + setSelectedSourceEnvironment(currentProject.environments[0].slug); setSelectedAWSRegion(awsRegions[0].slug); } - }, [currentWorkspace]); + }, [currentProject]); const { data: integrationAuthAwsKmsKeys, isPending: isIntegrationAuthAwsKmsKeysLoading } = useGetIntegrationAuthAwsKmsKeys({ @@ -158,7 +158,7 @@ export const AWSParameterStoreConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -226,7 +226,7 @@ export const AWSParameterStoreConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( {
; export const AWSSecretManagerAuthorizePage = () => { const navigate = useNavigate(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { control, handleSubmit, formState, watch } = useForm({ resolver: zodResolver(formSchema), @@ -54,7 +54,7 @@ export const AWSSecretManagerAuthorizePage = () => { const handleFormSubmit = async (data: TForm) => { try { const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "aws-secret-manager", ...(data.type === AwsAuthType.AssumeRole ? { @@ -68,7 +68,7 @@ export const AWSSecretManagerAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/aws-secret-manager/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/AwsSecretManagerConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/AwsSecretManagerConfigurePage.tsx index bd73fc596..6c82b7edd 100644 --- a/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/AwsSecretManagerConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/AwsSecretManagerConfigurePage.tsx @@ -30,7 +30,7 @@ import { } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries"; @@ -150,7 +150,7 @@ export const AwsSecretManagerConfigurePage = () => { select: (el) => el.integrationAuthId }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: integrationAuth, isPending: isintegrationAuthLoading } = useGetIntegrationAuthById( (integrationAuthId as string) ?? "" ); @@ -162,11 +162,11 @@ export const AwsSecretManagerConfigurePage = () => { }); useEffect(() => { - if (currentWorkspace) { - setValue("sourceEnvironment", currentWorkspace.environments[0].slug); + if (currentProject) { + setValue("sourceEnvironment", currentProject.environments[0].slug); setValue("awsRegion", awsRegions[0].slug); } - }, [currentWorkspace]); + }, [currentProject]); const handleButtonClick = async ({ secretName, @@ -206,7 +206,7 @@ export const AwsSecretManagerConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -286,7 +286,7 @@ export const AwsSecretManagerConfigurePage = () => { field.onChange(val); }} > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { isError={Boolean(error)} > { from: ROUTE_PATHS.SecretManager.Integratons.AzureAppConfigurationsConfigurePage.id, select: (el) => el.integrationAuthId }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); - const { data: workspace } = useGetWorkspaceById(currentWorkspace.id); + const { data: workspace } = useGetWorkspaceById(currentProject.id); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); useEffect(() => { @@ -133,7 +133,7 @@ export const AzureAppConfigurationConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations diff --git a/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationOauthCallbackPage/AzureAppConfigurationOauthCallbackPage.tsx b/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationOauthCallbackPage/AzureAppConfigurationOauthCallbackPage.tsx index d15fea903..046fac913 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationOauthCallbackPage/AzureAppConfigurationOauthCallbackPage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationOauthCallbackPage/AzureAppConfigurationOauthCallbackPage.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { useNavigate, useSearch } from "@tanstack/react-router"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useAuthorizeIntegration } from "@app/hooks/api"; export const AzureAppConfigurationOauthCallbackPage = () => { @@ -12,7 +12,7 @@ export const AzureAppConfigurationOauthCallbackPage = () => { const { code, state } = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.AzureAppConfigurationsOauthCallbackPage.id }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); useEffect(() => { (async () => { @@ -22,7 +22,7 @@ export const AzureAppConfigurationOauthCallbackPage = () => { localStorage.removeItem("latestCSRFToken"); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, code: code as string, integration: "azure-app-configuration" }); @@ -30,7 +30,7 @@ export const AzureAppConfigurationOauthCallbackPage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/azure-app-configuration/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/AzureDevopsAuthorizePage/AzureDevopsAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/AzureDevopsAuthorizePage/AzureDevopsAuthorizePage.tsx index 25cc9c617..4a03c9c3e 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureDevopsAuthorizePage/AzureDevopsAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureDevopsAuthorizePage/AzureDevopsAuthorizePage.tsx @@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; export const AzureDevopsAuthorizePage = () => { @@ -16,7 +16,7 @@ export const AzureDevopsAuthorizePage = () => { const [devopsOrgName, setDevopsOrgName] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const handleButtonClick = async () => { try { @@ -31,7 +31,7 @@ export const AzureDevopsAuthorizePage = () => { localStorage.setItem("azure-devops-org-name", devopsOrgName); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "azure-devops", accessToken: btoa(`:${apiKey}`) // This is a base64 encoding of the API key without any username }); @@ -41,7 +41,7 @@ export const AzureDevopsAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/azure-devops/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/AzureDevopsConfigurePage/AzureDevopsConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/AzureDevopsConfigurePage/AzureDevopsConfigurePage.tsx index b06859e1f..3d092fa26 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureDevopsConfigurePage/AzureDevopsConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureDevopsConfigurePage/AzureDevopsConfigurePage.tsx @@ -14,13 +14,13 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; -import { useGetWorkspaceById } from "@app/hooks/api/workspace"; +import { useGetWorkspaceById } from "@app/hooks/api/projects"; import { IntegrationsListPageTabs } from "@app/types/integrations"; export const AzureDevopsConfigurePage = () => { @@ -31,9 +31,9 @@ export const AzureDevopsConfigurePage = () => { from: ROUTE_PATHS.SecretManager.Integratons.AzureDevopsConfigurePage.id, select: (el) => el.integrationAuthId }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); - const { data: workspace } = useGetWorkspaceById(currentWorkspace.id); + const { data: workspace } = useGetWorkspaceById(currentProject.id); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); const { data: integrationAuthApps } = useGetIntegrationAuthApps({ integrationAuthId: (integrationAuthId as string) ?? "", @@ -82,7 +82,7 @@ export const AzureDevopsConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations diff --git a/frontend/src/pages/secret-manager/integrations/AzureKeyVaultConfigurePage/AzureKeyVaultConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/AzureKeyVaultConfigurePage/AzureKeyVaultConfigurePage.tsx index e5ccd3dfa..91f0d5440 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureKeyVaultConfigurePage/AzureKeyVaultConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureKeyVaultConfigurePage/AzureKeyVaultConfigurePage.tsx @@ -11,11 +11,11 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types"; -import { useGetWorkspaceById } from "@app/hooks/api/workspace"; +import { useGetWorkspaceById } from "@app/hooks/api/projects"; import { IntegrationsListPageTabs } from "@app/types/integrations"; const initialSyncBehaviors = [ @@ -38,9 +38,9 @@ export const AzureKeyVaultConfigurePage = () => { from: ROUTE_PATHS.SecretManager.Integratons.AzureKeyVaultConfigurePage.id, select: (el) => el.integrationAuthId }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); - const { data: workspace } = useGetWorkspaceById(currentWorkspace.id); + const { data: workspace } = useGetWorkspaceById(currentProject.id); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); @@ -90,7 +90,7 @@ export const AzureKeyVaultConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations diff --git a/frontend/src/pages/secret-manager/integrations/AzureKeyVaultOauthCallbackPage/AzureKeyVaultOauthCallback.tsx b/frontend/src/pages/secret-manager/integrations/AzureKeyVaultOauthCallbackPage/AzureKeyVaultOauthCallback.tsx index ef98d1cba..90a93edce 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureKeyVaultOauthCallbackPage/AzureKeyVaultOauthCallback.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureKeyVaultOauthCallbackPage/AzureKeyVaultOauthCallback.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { useNavigate, useSearch } from "@tanstack/react-router"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useAuthorizeIntegration } from "@app/hooks/api"; export const AzureKeyVaultOauthCallbackPage = () => { @@ -12,7 +12,7 @@ export const AzureKeyVaultOauthCallbackPage = () => { const { code, state } = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.AzureKeyVaultOauthCallbackPage.id }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); useEffect(() => { (async () => { @@ -22,7 +22,7 @@ export const AzureKeyVaultOauthCallbackPage = () => { localStorage.removeItem("latestCSRFToken"); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, code: code as string, integration: "azure-key-vault" }); @@ -30,7 +30,7 @@ export const AzureKeyVaultOauthCallbackPage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/azure-key-vault/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx index 10dafdb76..022a8e8d7 100644 --- a/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx @@ -16,7 +16,7 @@ import { Spinner } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration, useGetIntegrationAuthApps, @@ -96,7 +96,7 @@ export const BitbucketConfigurePage = () => { from: ROUTE_PATHS.SecretManager.Integratons.BitbucketConfigurePage.id, select: (el) => el.integrationAuthId }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: bitbucketWorkspaces, isPending: isBitbucketWorkspacesLoading } = useGetIntegrationAuthBitbucketWorkspaces((integrationAuthId as string) ?? ""); @@ -150,7 +150,7 @@ export const BitbucketConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -171,18 +171,18 @@ export const BitbucketConfigurePage = () => { bitbucketRepo || !bitbucketRepos || !bitbucketWorkspaces || - !currentWorkspace + !currentProject ) return; reset({ targetRepo: bitbucketRepos[0], targetWorkspace: bitbucketWorkspaces[0], - sourceEnvironment: currentWorkspace.environments[0], + sourceEnvironment: currentProject.environments[0], secretPath: "/", scope: ScopeOptions[0] }); - }, [bitbucketWorkspaces, bitbucketRepos, currentWorkspace]); + }, [bitbucketWorkspaces, bitbucketRepos, currentProject]); if (isBitbucketWorkspacesLoading || isBitbucketReposLoading) return ( @@ -217,9 +217,9 @@ export const BitbucketConfigurePage = () => { value={value} getOptionLabel={(option) => option.name} onChange={onChange} - options={currentWorkspace?.environments} + options={currentProject?.environments} placeholder="Select a project environment" - isDisabled={!currentWorkspace?.environments.length} + isDisabled={!currentProject?.environments.length} /> )} diff --git a/frontend/src/pages/secret-manager/integrations/BitbucketOauthCallbackPage/BitbucketOauthCallbackPage.tsx b/frontend/src/pages/secret-manager/integrations/BitbucketOauthCallbackPage/BitbucketOauthCallbackPage.tsx index 1e3d302f2..8b23f46ab 100644 --- a/frontend/src/pages/secret-manager/integrations/BitbucketOauthCallbackPage/BitbucketOauthCallbackPage.tsx +++ b/frontend/src/pages/secret-manager/integrations/BitbucketOauthCallbackPage/BitbucketOauthCallbackPage.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { useNavigate, useSearch } from "@tanstack/react-router"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useAuthorizeIntegration } from "@app/hooks/api"; export const BitbucketOauthCallbackPage = () => { @@ -11,7 +11,7 @@ export const BitbucketOauthCallbackPage = () => { const { code, state } = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.BitbucketOauthCallbackPage.id }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); useEffect(() => { (async () => { @@ -21,7 +21,7 @@ export const BitbucketOauthCallbackPage = () => { localStorage.removeItem("latestCSRFToken"); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, code: code as string, integration: "bitbucket" }); @@ -29,7 +29,7 @@ export const BitbucketOauthCallbackPage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/bitbucket/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/ChecklyAuthorizePage/ChecklyAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/ChecklyAuthorizePage/ChecklyAuthorizePage.tsx index 5dd711fc0..43521fd2e 100644 --- a/frontend/src/pages/secret-manager/integrations/ChecklyAuthorizePage/ChecklyAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/ChecklyAuthorizePage/ChecklyAuthorizePage.tsx @@ -5,13 +5,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; export const ChecklyAuthorizePage = () => { const navigate = useNavigate(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [accessToken, setAccessToken] = useState(""); const [accessTokenErrorText, setAccessTokenErrorText] = useState(""); @@ -28,7 +28,7 @@ export const ChecklyAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "checkly", accessToken }); @@ -38,7 +38,7 @@ export const ChecklyAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/checkly/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/ChecklyConfigurePage/ChecklyConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/ChecklyConfigurePage/ChecklyConfigurePage.tsx index 2e02f7cf2..e09a1361a 100644 --- a/frontend/src/pages/secret-manager/integrations/ChecklyConfigurePage/ChecklyConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/ChecklyConfigurePage/ChecklyConfigurePage.tsx @@ -19,14 +19,14 @@ import { Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, useGetIntegrationAuthById, useGetIntegrationAuthChecklyGroups } from "@app/hooks/api/integrationAuth"; -import { useGetWorkspaceById } from "@app/hooks/api/workspace"; +import { useGetWorkspaceById } from "@app/hooks/api/projects"; import { IntegrationsListPageTabs } from "@app/types/integrations"; enum TabSections { @@ -42,7 +42,7 @@ export const ChecklyConfigurePage = () => { from: ROUTE_PATHS.SecretManager.Integratons.ChecklyConfigurePage.id, select: (el) => el.integrationAuthId }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const [secretPath, setSecretPath] = useState("/"); @@ -53,7 +53,7 @@ export const ChecklyConfigurePage = () => { const [isLoading, setIsLoading] = useState(false); - const { data: workspace } = useGetWorkspaceById(currentWorkspace.id); + const { data: workspace } = useGetWorkspaceById(currentProject.id); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); const { data: integrationAuthApps, isPending: isIntegrationAuthAppsLoading } = useGetIntegrationAuthApps({ @@ -115,7 +115,7 @@ export const ChecklyConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations diff --git a/frontend/src/pages/secret-manager/integrations/CircleCIAuthorizePage/CircleCIAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/CircleCIAuthorizePage/CircleCIAuthorizePage.tsx index e64227fc3..088c22e1d 100644 --- a/frontend/src/pages/secret-manager/integrations/CircleCIAuthorizePage/CircleCIAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/CircleCIAuthorizePage/CircleCIAuthorizePage.tsx @@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; export const CircleCIAuthorizePage = () => { @@ -15,7 +15,7 @@ export const CircleCIAuthorizePage = () => { const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const handleButtonClick = async () => { try { @@ -28,7 +28,7 @@ export const CircleCIAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "circleci", accessToken: apiKey }); @@ -38,7 +38,7 @@ export const CircleCIAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/circleci/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/CircleCIConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/CircleCIConfigurePage.tsx index f03f35af8..36777af29 100644 --- a/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/CircleCIConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/CircleCIConfigurePage.tsx @@ -18,7 +18,7 @@ import { } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration, useGetIntegrationAuthCircleCIOrganizations } from "@app/hooks/api"; import { CircleCiScope } from "@app/hooks/api/integrationAuth/types"; import { IntegrationsListPageTabs } from "@app/types/integrations"; @@ -45,7 +45,7 @@ type TFormData = z.infer; export const CircleCIConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync, isPending: isCreatingIntegration } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.CircleConfigurePage.id, @@ -56,7 +56,7 @@ export const CircleCIConfigurePage = () => { resolver: zodResolver(formSchema), defaultValues: { secretPath: "/", - sourceEnvironment: currentWorkspace?.environments[0], + sourceEnvironment: currentProject?.environments[0], scope: CircleCiScope.Project } }); @@ -105,7 +105,7 @@ export const CircleCIConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -179,9 +179,9 @@ export const CircleCIConfigurePage = () => { value={value} getOptionLabel={(option) => option.name} onChange={onChange} - options={currentWorkspace?.environments} + options={currentProject?.environments} placeholder="Select a project environment" - isDisabled={!currentWorkspace?.environments.length} + isDisabled={!currentProject?.environments.length} /> )} diff --git a/frontend/src/pages/secret-manager/integrations/Cloud66AuthorizePage/Cloud66AuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/Cloud66AuthorizePage/Cloud66AuthorizePage.tsx index 8fc9dd5ca..1a464cadc 100644 --- a/frontend/src/pages/secret-manager/integrations/Cloud66AuthorizePage/Cloud66AuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/Cloud66AuthorizePage/Cloud66AuthorizePage.tsx @@ -2,14 +2,14 @@ import { useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; export const Cloud66AuthorizePage = () => { const navigate = useNavigate(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); @@ -26,7 +26,7 @@ export const Cloud66AuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "cloud-66", accessToken: apiKey }); @@ -36,7 +36,7 @@ export const Cloud66AuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/cloud-66/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/Cloud66ConfigurePage/Cloud66ConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/Cloud66ConfigurePage/Cloud66ConfigurePage.tsx index 6de5e3bc3..68535d70e 100644 --- a/frontend/src/pages/secret-manager/integrations/Cloud66ConfigurePage/Cloud66ConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/Cloud66ConfigurePage/Cloud66ConfigurePage.tsx @@ -11,7 +11,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -28,7 +28,7 @@ export const Cloud66ConfigurePage = () => { select: (el) => el.integrationAuthId }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); const { data: integrationAuthApps } = useGetIntegrationAuthApps({ integrationAuthId: (integrationAuthId as string) ?? "" @@ -40,10 +40,10 @@ export const Cloud66ConfigurePage = () => { const [isLoading, setIsLoading] = useState(false); useEffect(() => { - if (currentWorkspace) { - setSelectedSourceEnvironment(currentWorkspace.environments[0].slug); + if (currentProject) { + setSelectedSourceEnvironment(currentProject.environments[0].slug); } - }, [currentWorkspace]); + }, [currentProject]); useEffect(() => { if (integrationAuthApps) { @@ -77,7 +77,7 @@ export const Cloud66ConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -98,7 +98,7 @@ export const Cloud66ConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { @@ -14,7 +14,7 @@ export const CloudflarePagesAuthorizePage = () => { const [accountIdErrorText, setAccountIdErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const navigate = useNavigate(); const handleButtonClick = async () => { @@ -30,7 +30,7 @@ export const CloudflarePagesAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "cloudflare-pages", accessId: accountId, accessToken: accessKey @@ -43,7 +43,7 @@ export const CloudflarePagesAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/cloudflare-pages/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/CloudflarePagesConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/CloudflarePagesConfigurePage.tsx index 1ff6e75d5..69225d9ae 100644 --- a/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/CloudflarePagesConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/CloudflarePagesConfigurePage.tsx @@ -14,7 +14,7 @@ import { } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -31,7 +31,7 @@ export const CloudflarePagesConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.CloudflarePagesConfigurePage.id, @@ -39,7 +39,7 @@ export const CloudflarePagesConfigurePage = () => { }); const [secretPath, setSecretPath] = useState("/"); - const { data: workspace } = useGetWorkspaceById(currentWorkspace.id); + const { data: workspace } = useGetWorkspaceById(currentProject.id); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); const { data: integrationAuthApps } = useGetIntegrationAuthApps({ integrationAuthId: (integrationAuthId as string) ?? "" @@ -96,7 +96,7 @@ export const CloudflarePagesConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations diff --git a/frontend/src/pages/secret-manager/integrations/CloudflareWorkersAuthorizePage/CloudflareWorkersAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/CloudflareWorkersAuthorizePage/CloudflareWorkersAuthorizePage.tsx index e2829c76a..dd0b16a6b 100644 --- a/frontend/src/pages/secret-manager/integrations/CloudflareWorkersAuthorizePage/CloudflareWorkersAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/CloudflareWorkersAuthorizePage/CloudflareWorkersAuthorizePage.tsx @@ -2,13 +2,13 @@ import { useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; export const CloudflareWorkersAuthorizePage = () => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync } = useSaveIntegrationAccessToken(); const [accessKey, setAccessKey] = useState(""); @@ -30,7 +30,7 @@ export const CloudflareWorkersAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "cloudflare-workers", accessId: accountId, accessToken: accessKey @@ -43,7 +43,7 @@ export const CloudflareWorkersAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/cloudflare-workers/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/CloudflareWorkersConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/CloudflareWorkersConfigurePage.tsx index bfc4b766a..a5d28bb95 100644 --- a/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/CloudflareWorkersConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/CloudflareWorkersConfigurePage.tsx @@ -6,7 +6,7 @@ import { createNotification } from "@app/components/notifications"; import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -17,7 +17,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations"; export const CloudflareWorkersConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.CloudflareWorkersConfigurePage.id, @@ -29,7 +29,7 @@ export const CloudflareWorkersConfigurePage = () => { }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [secretPath, setSecretPath] = useState("/"); @@ -69,7 +69,7 @@ export const CloudflareWorkersConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -107,7 +107,7 @@ export const CloudflareWorkersConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { @@ -12,7 +12,7 @@ export const CodefreshAuthorizePage = () => { const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const handleButtonClick = async () => { try { @@ -25,7 +25,7 @@ export const CodefreshAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "codefresh", accessToken: apiKey }); @@ -35,7 +35,7 @@ export const CodefreshAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/codefresh/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/CodefreshConfigurePage/CodefreshConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/CodefreshConfigurePage/CodefreshConfigurePage.tsx index e99ad4ea8..80a54b621 100644 --- a/frontend/src/pages/secret-manager/integrations/CodefreshConfigurePage/CodefreshConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/CodefreshConfigurePage/CodefreshConfigurePage.tsx @@ -11,7 +11,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -23,7 +23,7 @@ export const CodefreshConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.CodefreshConfigurePage.id, select: (el) => el.integrationAuthId @@ -35,7 +35,7 @@ export const CodefreshConfigurePage = () => { }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [targetApp, setTargetApp] = useState(""); const [secretPath, setSecretPath] = useState("/"); @@ -73,7 +73,7 @@ export const CodefreshConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -94,7 +94,7 @@ export const CodefreshConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { @@ -17,7 +17,7 @@ export const DatabricksAuthorizePage = () => { const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [instanceURLErrorText, setInstanceURLErrorText] = useState(""); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const handleButtonClick = async () => { try { @@ -33,7 +33,7 @@ export const DatabricksAuthorizePage = () => { } const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "databricks", url: instanceURL.replace(/\/$/, ""), accessToken: apiKey @@ -42,7 +42,7 @@ export const DatabricksAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/databricks/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/DatabricksConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/DatabricksConfigurePage.tsx index 0f3d09728..a140a0339 100644 --- a/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/DatabricksConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/DatabricksConfigurePage.tsx @@ -20,7 +20,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -32,7 +32,7 @@ export const DatabricksConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync, isPending } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.DatabricksConfigurePage.id, @@ -49,7 +49,7 @@ export const DatabricksConfigurePage = () => { }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [targetScope, setTargetScope] = useState(""); const [secretPath, setSecretPath] = useState("/"); @@ -89,7 +89,7 @@ export const DatabricksConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -143,7 +143,7 @@ export const DatabricksConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { @@ -12,7 +12,7 @@ export const DigitalOceanAppPlatformAuthorizePage = () => { const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const handleButtonClick = async () => { try { @@ -25,7 +25,7 @@ export const DigitalOceanAppPlatformAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "digital-ocean-app-platform", accessToken: apiKey }); @@ -35,7 +35,7 @@ export const DigitalOceanAppPlatformAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/digital-ocean-app-platform/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformConfigurePage/DigitalOceanAppPlatformConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformConfigurePage/DigitalOceanAppPlatformConfigurePage.tsx index e88d98976..2c6f08348 100644 --- a/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformConfigurePage/DigitalOceanAppPlatformConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformConfigurePage/DigitalOceanAppPlatformConfigurePage.tsx @@ -11,7 +11,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -23,7 +23,7 @@ export const DigitalOceanAppPlatformConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.DigitalOceanAppPlatformConfigurePage.id, @@ -36,7 +36,7 @@ export const DigitalOceanAppPlatformConfigurePage = () => { }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [targetApp, setTargetApp] = useState(""); const [secretPath, setSecretPath] = useState("/"); @@ -74,7 +74,7 @@ export const DigitalOceanAppPlatformConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -95,7 +95,7 @@ export const DigitalOceanAppPlatformConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( ; export const FlyioAuthorizePage = () => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { control, handleSubmit } = useForm({ resolver: zodResolver(schema), @@ -37,7 +37,7 @@ export const FlyioAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "flyio", accessToken }); @@ -46,7 +46,7 @@ export const FlyioAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/flyio/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/FlyioConfigurePage/FlyioConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/FlyioConfigurePage/FlyioConfigurePage.tsx index 0beb61bdf..c034ce3f1 100644 --- a/frontend/src/pages/secret-manager/integrations/FlyioConfigurePage/FlyioConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/FlyioConfigurePage/FlyioConfigurePage.tsx @@ -21,7 +21,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -34,7 +34,7 @@ export const FlyioConfigurePage = () => { const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.FlyioConfigurePage.id, @@ -50,7 +50,7 @@ export const FlyioConfigurePage = () => { }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [secretPath, setSecretPath] = useState("/"); @@ -88,7 +88,7 @@ export const FlyioConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -144,7 +144,7 @@ export const FlyioConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { }); const { data: cloudIntegrations } = useGetCloudIntegrations(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync } = useSaveIntegrationAccessToken(); @@ -47,7 +47,7 @@ export const GcpSecretManagerAuthorizePage = () => { const state = crypto.randomBytes(16).toString("hex"); localStorage.setItem("latestCSRFToken", state); - localStorageService.setIntegrationProjectId(currentWorkspace.id); + localStorageService.setIntegrationProjectId(currentProject.id); if (!integrationOption.clientId) { createIntegrationMissingEnvVarsNotification(integrationOption.slug); @@ -63,7 +63,7 @@ export const GcpSecretManagerAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "gcp-secret-manager", refreshToken: accessToken }); @@ -72,7 +72,7 @@ export const GcpSecretManagerAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/gcp-secret-manager/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/GcpSecretManagerConfigurePage/GcpSecretManagerConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/GcpSecretManagerConfigurePage/GcpSecretManagerConfigurePage.tsx index c4f8403bd..85af1961f 100644 --- a/frontend/src/pages/secret-manager/integrations/GcpSecretManagerConfigurePage/GcpSecretManagerConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/GcpSecretManagerConfigurePage/GcpSecretManagerConfigurePage.tsx @@ -26,7 +26,7 @@ import { } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useCreateIntegration } from "@app/hooks/api"; import { @@ -59,7 +59,7 @@ export const GcpSecretManagerConfigurePage = () => { "confirmIntegration" ] as const); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { control, handleSubmit, setValue, watch } = useForm({ resolver: zodResolver(schema), defaultValues: { @@ -69,7 +69,7 @@ export const GcpSecretManagerConfigurePage = () => { shouldLabel: false, labelName: "managed-by", labelValue: "infisical", - selectedSourceEnvironment: currentWorkspace.environments[0].slug + selectedSourceEnvironment: currentProject.environments[0].slug } }); @@ -153,7 +153,7 @@ export const GcpSecretManagerConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -241,7 +241,7 @@ export const GcpSecretManagerConfigurePage = () => { onValueChange={(e) => onChange(e)} className="w-full" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { @@ -12,7 +12,7 @@ export const GcpSecretManagerOauthCallbackPage = () => { const { code, state } = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.GcpSecretManagerOauthCallbackPage.id }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); useEffect(() => { (async () => { @@ -22,7 +22,7 @@ export const GcpSecretManagerOauthCallbackPage = () => { if (state !== localStorage.getItem("latestCSRFToken")) return; localStorage.removeItem("latestCSRFToken"); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, code: code as string, integration: "gcp-secret-manager" }); @@ -30,7 +30,7 @@ export const GcpSecretManagerOauthCallbackPage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/gcp-secret-manager/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/GithubAuthorizePage/GithubAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/GithubAuthorizePage/GithubAuthorizePage.tsx index 623c5762c..e4fe73feb 100644 --- a/frontend/src/pages/secret-manager/integrations/GithubAuthorizePage/GithubAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/GithubAuthorizePage/GithubAuthorizePage.tsx @@ -15,7 +15,7 @@ import { Select, SelectItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { localStorageService } from "@app/helpers/localStorage"; import { useGetCloudIntegrations } from "@app/hooks/api"; @@ -31,7 +31,7 @@ export const GithubAuthorizePage = () => { const { data: cloudIntegrations } = useGetCloudIntegrations(); const githubIntegration = cloudIntegrations?.find((integration) => integration.slug === "github"); const [selectedAuthMethod, setSelectedAuthMethod] = useState(AuthMethod.APP); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); return (
@@ -84,7 +84,7 @@ export const GithubAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/select-integration-auth", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationSlug: "github" @@ -102,7 +102,7 @@ export const GithubAuthorizePage = () => { const state = crypto.randomBytes(16).toString("hex"); localStorage.setItem("latestCSRFToken", state); - localStorageService.setIntegrationProjectId(currentWorkspace.id); + localStorageService.setIntegrationProjectId(currentProject.id); window.location.assign( `https://github.com/login/oauth/authorize?client_id=${githubIntegration?.clientId}&response_type=code&scope=repo,admin:org&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}` ); diff --git a/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/GithubConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/GithubConfigurePage.tsx index aa71c793e..7dad0d29a 100644 --- a/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/GithubConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/GithubConfigurePage.tsx @@ -37,7 +37,7 @@ import { Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration, useGetIntegrationAuthApps, @@ -142,7 +142,7 @@ export const GithubConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.GithubConfigurePage.id, select: (el) => el.integrationAuthId @@ -166,7 +166,7 @@ export const GithubConfigurePage = () => { repoIds: [], visibility: "all", shouldEnableDelete: false, - selectedSourceEnvironment: currentWorkspace.environments[0].slug + selectedSourceEnvironment: currentProject.environments[0].slug } }); @@ -269,7 +269,7 @@ export const GithubConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -367,7 +367,7 @@ export const GithubConfigurePage = () => { onValueChange={onChange} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { @@ -16,7 +16,7 @@ export const GithubOauthCallbackPage = () => { } = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.GithubOauthCallbackPage.id }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); useEffect(() => { (async () => { @@ -29,7 +29,7 @@ export const GithubOauthCallbackPage = () => { localStorage.removeItem("latestCSRFToken"); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, code: code as string, installationId, integration: "github" @@ -38,7 +38,7 @@ export const GithubOauthCallbackPage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/github/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/GitlabAuthorizePage/GitlabAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/GitlabAuthorizePage/GitlabAuthorizePage.tsx index 2ce82d97c..cf77aa312 100644 --- a/frontend/src/pages/secret-manager/integrations/GitlabAuthorizePage/GitlabAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/GitlabAuthorizePage/GitlabAuthorizePage.tsx @@ -8,7 +8,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { localStorageService } from "@app/helpers/localStorage"; import { useGetCloudIntegrations } from "@app/hooks/api"; @@ -29,7 +29,7 @@ export const GitlabAuthorizePage = () => { }); const { data: cloudIntegrations } = useGetCloudIntegrations(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const onFormSubmit = ({ gitLabURL }: FormData) => { if (!cloudIntegrations) return; @@ -49,12 +49,12 @@ export const GitlabAuthorizePage = () => { const csrfToken = crypto.randomBytes(16).toString("hex"); localStorage.setItem("latestCSRFToken", csrfToken); - localStorageService.setIntegrationProjectId(currentWorkspace.id); + localStorageService.setIntegrationProjectId(currentProject.id); const state = `${csrfToken}|${ (gitLabURL as string).trim() === "" ? "" : (gitLabURL as string).trim() }`; - localStorageService.setIntegrationProjectId(currentWorkspace.id); + localStorageService.setIntegrationProjectId(currentProject.id); const link = `${baseURL}/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`; window.location.assign(link); diff --git a/frontend/src/pages/secret-manager/integrations/GitlabConfigurePage/GitlabConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/GitlabConfigurePage/GitlabConfigurePage.tsx index 6fb388b71..13d2081ad 100644 --- a/frontend/src/pages/secret-manager/integrations/GitlabConfigurePage/GitlabConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/GitlabConfigurePage/GitlabConfigurePage.tsx @@ -26,7 +26,7 @@ import { } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useCreateIntegration } from "@app/hooks/api"; import { @@ -76,7 +76,7 @@ export const GitlabConfigurePage = () => { const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ "confirmIntegration" ] as const); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { control, handleSubmit, setValue, watch } = useForm({ resolver: zodResolver(schema), @@ -85,7 +85,7 @@ export const GitlabConfigurePage = () => { secretPath: "/", secretPrefix: "", secretSuffix: "", - selectedSourceEnvironment: currentWorkspace.environments[0].slug, + selectedSourceEnvironment: currentProject.environments[0].slug, initialSyncBehavior: IntegrationSyncBehavior.PREFER_SOURCE } }); @@ -177,7 +177,7 @@ export const GitlabConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -267,7 +267,7 @@ export const GitlabConfigurePage = () => { onValueChange={(e) => onChange(e)} className="w-full" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { const { code, state } = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.GitlabOauthCallbackPage.id }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); useEffect(() => { (async () => { @@ -65,7 +65,7 @@ export const GitLabOAuthCallbackPage = () => { }); } else { appConnection = await createAppConnection({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, ...connectionData }); } @@ -87,7 +87,7 @@ export const GitLabOAuthCallbackPage = () => { }); } })(); - }, [code, state, navigate, createAppConnection, updateAppConnection, currentWorkspace.id]); + }, [code, state, navigate, createAppConnection, updateAppConnection, currentProject.id]); return (
diff --git a/frontend/src/pages/secret-manager/integrations/HashicorpVaultAuthorizePage/HashicorpVaultAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/HashicorpVaultAuthorizePage/HashicorpVaultAuthorizePage.tsx index ae200968a..8a8cbbc4c 100644 --- a/frontend/src/pages/secret-manager/integrations/HashicorpVaultAuthorizePage/HashicorpVaultAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/HashicorpVaultAuthorizePage/HashicorpVaultAuthorizePage.tsx @@ -9,7 +9,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, Card, CardBody, CardTitle, FormControl, Input } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; const formSchema = z.object({ @@ -25,7 +25,7 @@ export const HashicorpVaultAuthorizePage = () => { const navigate = useNavigate(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { control, handleSubmit, @@ -43,7 +43,7 @@ export const HashicorpVaultAuthorizePage = () => { const handleFormSubmit = async (formData: TForm) => { try { const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "hashicorp-vault", accessId: formData.vaultRoleID, accessToken: formData.vaultSecretID, @@ -53,7 +53,7 @@ export const HashicorpVaultAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/hashicorp-vault/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/HashicorpVaultConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/HashicorpVaultConfigurePage.tsx index af9af585e..48b31dd80 100644 --- a/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/HashicorpVaultConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/HashicorpVaultConfigurePage.tsx @@ -25,7 +25,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { isValidPath } from "@app/helpers/string"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; @@ -61,7 +61,7 @@ export const HashicorpVaultConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.HashicorpVaultConfigurePage.id, select: (el) => el.integrationAuthId @@ -72,8 +72,8 @@ export const HashicorpVaultConfigurePage = () => { ); const formSchema = useMemo(() => { - return generateFormSchema(currentWorkspace?.environments.map((env) => env.slug) ?? []); - }, [currentWorkspace?.environments]); + return generateFormSchema(currentProject?.environments.map((env) => env.slug) ?? []); + }, [currentProject?.environments]); const { control, @@ -103,7 +103,7 @@ export const HashicorpVaultConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -177,7 +177,7 @@ export const HashicorpVaultConfigurePage = () => { onValueChange={field.onChange} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { const [isLoading, setIsLoading] = useState(false); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { control, handleSubmit } = useForm({ resolver: zodResolver(schema), defaultValues: { @@ -36,7 +36,7 @@ export const HasuraCloudAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "hasura-cloud", accessToken }); @@ -45,7 +45,7 @@ export const HasuraCloudAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/hasura-cloud/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/HasuraCloudConfigurePage/HasuraCloudConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/HasuraCloudConfigurePage/HasuraCloudConfigurePage.tsx index a42451e8e..cfcba8058 100644 --- a/frontend/src/pages/secret-manager/integrations/HasuraCloudConfigurePage/HasuraCloudConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/HasuraCloudConfigurePage/HasuraCloudConfigurePage.tsx @@ -9,7 +9,7 @@ import { z } from "zod"; import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -38,7 +38,7 @@ export const HasuraCloudConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.HasuraCloudConfigurePage.id, select: (el) => el.integrationAuthId @@ -72,7 +72,7 @@ export const HasuraCloudConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -137,7 +137,7 @@ export const HasuraCloudConfigurePage = () => { field.onChange(val); }} > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( ; export const HerokuConfigurePage = () => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { control, handleSubmit, setValue, watch } = useForm({ resolver: zodResolver(schema), defaultValues: { secretPath: "/", initialSyncBehavior: IntegrationSyncBehavior.PREFER_SOURCE, - selectedSourceEnvironment: currentWorkspace.environments[0].slug + selectedSourceEnvironment: currentProject.environments[0].slug } }); @@ -100,7 +100,7 @@ export const HerokuConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -158,7 +158,7 @@ export const HerokuConfigurePage = () => { onValueChange={(e) => onChange(e)} className="w-full" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { from: ROUTE_PATHS.SecretManager.Integratons.HerokuOauthCallbackPage.id }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); useEffect(() => { (async () => { @@ -67,7 +67,7 @@ export const HerokuOAuthCallbackPage = () => { }); } else { appConnection = await createAppConnection({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, ...connectionData }); } @@ -88,7 +88,7 @@ export const HerokuOAuthCallbackPage = () => { }); } })(); - }, [code, state, navigate, createAppConnection, updateAppConnection, currentWorkspace.id]); + }, [code, state, navigate, createAppConnection, updateAppConnection, currentProject.id]); return (
diff --git a/frontend/src/pages/secret-manager/integrations/LaravelForgeAuthorizePage/LaravelForgeAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/LaravelForgeAuthorizePage/LaravelForgeAuthorizePage.tsx index d67a6ce1d..c607472c7 100644 --- a/frontend/src/pages/secret-manager/integrations/LaravelForgeAuthorizePage/LaravelForgeAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/LaravelForgeAuthorizePage/LaravelForgeAuthorizePage.tsx @@ -2,13 +2,13 @@ import { useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; export const LaravelForgeAuthorizePage = () => { const navigate = useNavigate(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [serverId, setServerId] = useState(""); @@ -33,7 +33,7 @@ export const LaravelForgeAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "laravel-forge", accessId: serverId, accessToken: apiKey @@ -44,7 +44,7 @@ export const LaravelForgeAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/laravel-forge/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/LaravelForgeConfigurePage/LaravelForgeConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/LaravelForgeConfigurePage/LaravelForgeConfigurePage.tsx index 91963e749..55a5ac360 100644 --- a/frontend/src/pages/secret-manager/integrations/LaravelForgeConfigurePage/LaravelForgeConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/LaravelForgeConfigurePage/LaravelForgeConfigurePage.tsx @@ -11,7 +11,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -23,7 +23,7 @@ export const LaravelForgeConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.LaravelForgeConfigurePage.id, select: (el) => el.integrationAuthId @@ -35,7 +35,7 @@ export const LaravelForgeConfigurePage = () => { }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [targetApp, setTargetApp] = useState(""); const [secretPath, setSecretPath] = useState("/"); @@ -74,7 +74,7 @@ export const LaravelForgeConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -95,7 +95,7 @@ export const LaravelForgeConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.NetlifyConfigurePage.id, select: (el) => el.integrationAuthId @@ -42,7 +42,7 @@ export const NetlifyConfigurePage = () => { }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [targetApp, setTargetApp] = useState(""); const [targetEnvironment, setTargetEnvironment] = useState(netlifyEnvironments[0].slug); @@ -81,7 +81,7 @@ export const NetlifyConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -106,7 +106,7 @@ export const NetlifyConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { @@ -12,7 +12,7 @@ export const NetlifyOauthCallbackPage = () => { const { code, state } = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.NetlifyOuathCallbackPage.id }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); useEffect(() => { (async () => { @@ -22,7 +22,7 @@ export const NetlifyOauthCallbackPage = () => { localStorage.removeItem("latestCSRFToken"); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, code: code as string, integration: "netlify" }); @@ -30,7 +30,7 @@ export const NetlifyOauthCallbackPage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/netlify/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/NorthflankAuthorizePage/NorthflankAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/NorthflankAuthorizePage/NorthflankAuthorizePage.tsx index b203a02dc..005a4914e 100644 --- a/frontend/src/pages/secret-manager/integrations/NorthflankAuthorizePage/NorthflankAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/NorthflankAuthorizePage/NorthflankAuthorizePage.tsx @@ -2,13 +2,13 @@ import { useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; export const NorthflankAuthorizePage = () => { const navigate = useNavigate(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); @@ -25,7 +25,7 @@ export const NorthflankAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "northflank", accessToken: apiKey }); @@ -35,7 +35,7 @@ export const NorthflankAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/northflank/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/NorthflankConfigurePage/NorthflankConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/NorthflankConfigurePage/NorthflankConfigurePage.tsx index 7f01c32ee..9ff1e18c8 100644 --- a/frontend/src/pages/secret-manager/integrations/NorthflankConfigurePage/NorthflankConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/NorthflankConfigurePage/NorthflankConfigurePage.tsx @@ -11,7 +11,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -24,9 +24,9 @@ export const NorthflankConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [secretPath, setSecretPath] = useState("/"); const [targetAppId, setTargetAppId] = useState(""); @@ -95,7 +95,7 @@ export const NorthflankConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -116,7 +116,7 @@ export const NorthflankConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( ; export const OctopusDeployAuthorizePage = () => { const navigate = useNavigate(); const { mutateAsync, isPending } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { control, handleSubmit } = useForm({ resolver: zodResolver(formSchema) @@ -31,7 +31,7 @@ export const OctopusDeployAuthorizePage = () => { const onSubmit = async ({ instanceUrl, apiKey }: TForm) => { try { const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "octopus-deploy", url: removeTrailingSlash(instanceUrl), accessToken: apiKey @@ -40,7 +40,7 @@ export const OctopusDeployAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/octopus-deploy/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/OctopusDeployConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/OctopusDeployConfigurePage.tsx index 458c4a997..bddd884f4 100644 --- a/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/OctopusDeployConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/OctopusDeployConfigurePage.tsx @@ -16,7 +16,7 @@ import { } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration, useGetIntegrationAuthApps } from "@app/hooks/api"; import { useGetIntegrationAuthOctopusDeployScopeValues, @@ -56,7 +56,7 @@ export const OctopusDeployConfigurePage = () => { } }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.OctopusDeployCloudConfigurePage.id, select: (el) => el.integrationAuthId @@ -137,7 +137,7 @@ export const OctopusDeployConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -153,16 +153,16 @@ export const OctopusDeployConfigurePage = () => { }; useEffect(() => { - if (!octopusDeployResources || !octopusDeploySpaces || !currentWorkspace) return; + if (!octopusDeployResources || !octopusDeploySpaces || !currentProject) return; reset({ targetResource: octopusDeployResources[0], targetSpace: octopusDeploySpaces.find((space) => space.IsDefault), - sourceEnvironment: currentWorkspace.environments[0], + sourceEnvironment: currentProject.environments[0], secretPath: "/", scope: OctopusDeployScope.Project }); - }, [octopusDeploySpaces, octopusDeployResources, currentWorkspace]); + }, [octopusDeploySpaces, octopusDeployResources, currentProject]); if (isLoadingOctopusDeploySpaces || isOctopusDeployResourcesLoading) return ( @@ -196,9 +196,9 @@ export const OctopusDeployConfigurePage = () => { value={value} getOptionLabel={(option) => option.name} onChange={onChange} - options={currentWorkspace?.environments} + options={currentProject?.environments} placeholder="Select a project environment" - isDisabled={!currentWorkspace?.environments.length} + isDisabled={!currentProject?.environments.length} /> )} diff --git a/frontend/src/pages/secret-manager/integrations/QoveryAuthorizePage/QoveryAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/QoveryAuthorizePage/QoveryAuthorizePage.tsx index 607b1962b..369046df9 100644 --- a/frontend/src/pages/secret-manager/integrations/QoveryAuthorizePage/QoveryAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/QoveryAuthorizePage/QoveryAuthorizePage.tsx @@ -5,12 +5,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; export const QoveryAuthorizePage = () => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync } = useSaveIntegrationAccessToken(); const [accessToken, setAccessToken] = useState(""); @@ -28,7 +28,7 @@ export const QoveryAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "qovery", accessToken }); @@ -38,7 +38,7 @@ export const QoveryAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/qovery/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/QoveryConfigurePage/QoveryConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/QoveryConfigurePage/QoveryConfigurePage.tsx index bc0f24c90..977f4fdd1 100644 --- a/frontend/src/pages/secret-manager/integrations/QoveryConfigurePage/QoveryConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/QoveryConfigurePage/QoveryConfigurePage.tsx @@ -19,7 +19,7 @@ import { Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; import { @@ -55,7 +55,7 @@ enum TabSections { export const QoveryConfigurePage = () => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync } = useCreateIntegration(); const integrationAuthId = useSearch({ @@ -67,7 +67,7 @@ export const QoveryConfigurePage = () => { const [scope, setScope] = useState("application"); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [secretPath, setSecretPath] = useState("/"); @@ -178,7 +178,7 @@ export const QoveryConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -241,7 +241,7 @@ export const QoveryConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { const navigate = useNavigate(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); @@ -25,7 +25,7 @@ export const RailwayAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "railway", accessToken: apiKey }); @@ -35,7 +35,7 @@ export const RailwayAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/railway/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/RailwayConfigurePage/RailwayConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/RailwayConfigurePage/RailwayConfigurePage.tsx index 835e59277..1d24f7ce7 100644 --- a/frontend/src/pages/secret-manager/integrations/RailwayConfigurePage/RailwayConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/RailwayConfigurePage/RailwayConfigurePage.tsx @@ -11,7 +11,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -25,13 +25,13 @@ export const RailwayConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [targetAppId, setTargetAppId] = useState(""); const [targetEnvironmentId, setTargetEnvironmentId] = useState(""); const [targetServiceId, setTargetServiceId] = useState(""); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [secretPath, setSecretPath] = useState("/"); const [isLoading, setIsLoading] = useState(false); @@ -110,7 +110,7 @@ export const RailwayConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -139,7 +139,7 @@ export const RailwayConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { const navigate = useNavigate(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); @@ -28,7 +28,7 @@ export const RenderAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "render", accessToken: apiKey }); @@ -38,7 +38,7 @@ export const RenderAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/render/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/RenderConfigurePage/RenderConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/RenderConfigurePage/RenderConfigurePage.tsx index 3d156cdac..4a2ef30d1 100644 --- a/frontend/src/pages/secret-manager/integrations/RenderConfigurePage/RenderConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/RenderConfigurePage/RenderConfigurePage.tsx @@ -23,7 +23,7 @@ import { } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -44,13 +44,13 @@ export const RenderConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { control, handleSubmit, setValue, watch } = useForm({ resolver: zodResolver(schema), defaultValues: { secretPath: "/", shouldAutoRedeploy: false, - selectedSourceEnvironment: currentWorkspace.environments[0].slug + selectedSourceEnvironment: currentProject.environments[0].slug } }); @@ -106,7 +106,7 @@ export const RenderConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -167,7 +167,7 @@ export const RenderConfigurePage = () => { onValueChange={(e) => onChange(e)} className="w-full" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { const [isLoading, setIsLoading] = useState(false); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { control, handleSubmit } = useForm({ resolver: zodResolver(schema), @@ -40,7 +40,7 @@ export const RundeckAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "rundeck", accessToken: authToken, url: rundeckURL.trim() @@ -50,7 +50,7 @@ export const RundeckAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/rundeck/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/RundeckConfigurePage/RundeckConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/RundeckConfigurePage/RundeckConfigurePage.tsx index 7c8dada9f..555a5cc5c 100644 --- a/frontend/src/pages/secret-manager/integrations/RundeckConfigurePage/RundeckConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/RundeckConfigurePage/RundeckConfigurePage.tsx @@ -17,7 +17,7 @@ import { } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; import { IntegrationsListPageTabs } from "@app/types/integrations"; @@ -44,7 +44,7 @@ export const RundeckConfigurePage = () => { }); const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.RundeckConfigurePage.id, @@ -73,7 +73,7 @@ export const RundeckConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -138,7 +138,7 @@ export const RundeckConfigurePage = () => { field.onChange(val); }} > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { isError={Boolean(error)} > { const navigate = useNavigate(); const { data: cloudIntegrations } = useGetCloudIntegrations(); const { currentOrg } = useOrganization(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const orgId = currentOrg?.id || ""; const integrationSlug = useSearch({ @@ -50,10 +50,10 @@ export const SelectIntegrationAuthPage = () => { if (integrationSlug === "github") { const sameProjectIntegrationAuths = filteredIntegrationAuths.filter( - (auth) => auth.projectId === currentWorkspace?.id + (auth) => auth.projectId === currentProject?.id ); const differentProjectIntegrationAuths = filteredIntegrationAuths.filter( - (auth) => auth.projectId !== currentWorkspace?.id + (auth) => auth.projectId !== currentProject?.id ); const installationIds = new Set(); @@ -117,11 +117,11 @@ export const SelectIntegrationAuthPage = () => { const handleConnectionSelect = async (integrationAuth: IntegrationAuth) => { if (integrationSlug === "github") { - if (integrationAuth.projectId === currentWorkspace?.id) { + if (integrationAuth.projectId === currentProject?.id) { navigate({ to: "/projects/secret-management/$projectId/integrations/github/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id @@ -130,14 +130,14 @@ export const SelectIntegrationAuthPage = () => { } else { // we create a copy of the existing integration auth from another project to the current project const newIntegrationAuth = await duplicateIntegrationAuth({ - projectId: currentWorkspace?.id || "", + projectId: currentProject?.id || "", integrationAuthId: integrationAuth.id }); navigate({ to: "/projects/secret-management/$projectId/integrations/github/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: newIntegrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/SupabaseAuthorizePage/SupabaseAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/SupabaseAuthorizePage/SupabaseAuthorizePage.tsx index 5ec969e11..210efea1c 100644 --- a/frontend/src/pages/secret-manager/integrations/SupabaseAuthorizePage/SupabaseAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/SupabaseAuthorizePage/SupabaseAuthorizePage.tsx @@ -2,13 +2,13 @@ import { useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; export const SupabaseAuthorizePage = () => { const navigate = useNavigate(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); @@ -25,7 +25,7 @@ export const SupabaseAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "supabase", accessToken: apiKey }); @@ -35,7 +35,7 @@ export const SupabaseAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/supabase/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/SupabaseConfigurePage/SupabaseConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/SupabaseConfigurePage/SupabaseConfigurePage.tsx index 333c61a8e..38183a5ad 100644 --- a/frontend/src/pages/secret-manager/integrations/SupabaseConfigurePage/SupabaseConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/SupabaseConfigurePage/SupabaseConfigurePage.tsx @@ -11,7 +11,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -22,7 +22,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations"; export const SupabaseConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.SupabaseConfigurePage.id, select: (el) => el.integrationAuthId @@ -34,7 +34,7 @@ export const SupabaseConfigurePage = () => { }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [secretPath, setSecretPath] = useState("/"); const [targetApp, setTargetApp] = useState(""); @@ -73,7 +73,7 @@ export const SupabaseConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -94,7 +94,7 @@ export const SupabaseConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { const navigate = useNavigate(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [serverUrl, setServerUrl] = useState(""); @@ -37,7 +37,7 @@ export const TeamcityAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "teamcity", accessToken: apiKey, url: serverUrl @@ -48,7 +48,7 @@ export const TeamcityAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/teamcity/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/TeamcityConfigurePage/TeamcityConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/TeamcityConfigurePage/TeamcityConfigurePage.tsx index 9291d0d5f..8e7d0fe97 100644 --- a/frontend/src/pages/secret-manager/integrations/TeamcityConfigurePage/TeamcityConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/TeamcityConfigurePage/TeamcityConfigurePage.tsx @@ -14,7 +14,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -26,7 +26,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations"; export const TeamcityConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.TeamcityConfigurePage.id, @@ -34,7 +34,7 @@ export const TeamcityConfigurePage = () => { }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [targetAppId, setTargetAppId] = useState(""); const [targetBuildConfigId, setTargetBuildConfigId] = useState(""); @@ -92,7 +92,7 @@ export const TeamcityConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -149,7 +149,7 @@ export const TeamcityConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { const navigate = useNavigate(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); @@ -37,7 +37,7 @@ export const TerraformCloudAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "terraform-cloud", accessId: workspacesId, accessToken: apiKey @@ -48,7 +48,7 @@ export const TerraformCloudAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/terraform-cloud/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/TerraformCloudConfigurePage/TerraformCloudConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/TerraformCloudConfigurePage/TerraformCloudConfigurePage.tsx index a2d73feca..ba44dfaa4 100644 --- a/frontend/src/pages/secret-manager/integrations/TerraformCloudConfigurePage/TerraformCloudConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/TerraformCloudConfigurePage/TerraformCloudConfigurePage.tsx @@ -14,7 +14,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -44,7 +44,7 @@ export const TerraformCloudConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.TerraformCloudConfigurePage.id, @@ -56,7 +56,7 @@ export const TerraformCloudConfigurePage = () => { }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [targetApp, setTargetApp] = useState(""); const [secretPath, setSecretPath] = useState("/"); @@ -107,7 +107,7 @@ export const TerraformCloudConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -160,7 +160,7 @@ export const TerraformCloudConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { mutateAsync } = useSaveIntegrationAccessToken(); const [apiKey, setApiKey] = useState(""); @@ -25,7 +25,7 @@ export const TravisCIAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "travisci", accessToken: apiKey }); @@ -35,7 +35,7 @@ export const TravisCIAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/travisci/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/TravisCIConfigurePage/TravisCIConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/TravisCIConfigurePage/TravisCIConfigurePage.tsx index 66c3a8022..e71a6ab31 100644 --- a/frontend/src/pages/secret-manager/integrations/TravisCIConfigurePage/TravisCIConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/TravisCIConfigurePage/TravisCIConfigurePage.tsx @@ -11,7 +11,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -23,7 +23,7 @@ export const TravisCIConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.TravisCIConfigurePage.id, @@ -36,7 +36,7 @@ export const TravisCIConfigurePage = () => { }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [targetApp, setTargetApp] = useState(""); const [secretPath, setSecretPath] = useState("/"); @@ -74,7 +74,7 @@ export const TravisCIConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -95,7 +95,7 @@ export const TravisCIConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [secretPath, setSecretPath] = useState("/"); const [initialSyncBehavior, setInitialSyncBehavior] = useState( @@ -134,7 +134,7 @@ export const VercelConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -206,7 +206,7 @@ export const VercelConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { @@ -12,7 +12,7 @@ export const VercelOauthCallbackPage = () => { const { code, state } = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.VercelOauthCallbackPage.id }); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); useEffect(() => { (async () => { @@ -22,7 +22,7 @@ export const VercelOauthCallbackPage = () => { localStorage.removeItem("latestCSRFToken"); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, code: code as string, integration: "vercel" }); @@ -30,7 +30,7 @@ export const VercelOauthCallbackPage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/vercel/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/WindmillAuthorizePage/WindmillAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/WindmillAuthorizePage/WindmillAuthorizePage.tsx index 36e791e56..3cd3ff335 100644 --- a/frontend/src/pages/secret-manager/integrations/WindmillAuthorizePage/WindmillAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/WindmillAuthorizePage/WindmillAuthorizePage.tsx @@ -2,14 +2,14 @@ import { useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { isInfisicalCloud } from "@app/helpers/platform"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; export const WindmillAuthorizePage = () => { const navigate = useNavigate(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [apiUrl, setApiUrl] = useState(null); @@ -68,7 +68,7 @@ export const WindmillAuthorizePage = () => { setIsLoading(true); const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, + workspaceId: currentProject.id, integration: "windmill", accessToken: apiKey, url: apiUrl ?? undefined @@ -79,7 +79,7 @@ export const WindmillAuthorizePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations/windmill/create", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { integrationAuthId: integrationAuth.id diff --git a/frontend/src/pages/secret-manager/integrations/WindmillConfigurePage/WindmillConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/WindmillConfigurePage/WindmillConfigurePage.tsx index 17929b2a3..7f781c89e 100644 --- a/frontend/src/pages/secret-manager/integrations/WindmillConfigurePage/WindmillConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/WindmillConfigurePage/WindmillConfigurePage.tsx @@ -11,7 +11,7 @@ import { SelectItem } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, @@ -23,7 +23,7 @@ export const WindmillConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.WindmillConfigurePage.id, @@ -36,7 +36,7 @@ export const WindmillConfigurePage = () => { }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState( - currentWorkspace.environments[0].slug + currentProject.environments[0].slug ); const [secretPath, setSecretPath] = useState("/"); const [targetApp, setTargetApp] = useState(""); @@ -76,7 +76,7 @@ export const WindmillConfigurePage = () => { navigate({ to: "/projects/secret-management/$projectId/integrations", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations @@ -97,7 +97,7 @@ export const WindmillConfigurePage = () => { onValueChange={(val) => setSelectedSourceEnvironment(val)} className="w-full border border-mineshaft-500" > - {currentWorkspace?.environments.map((sourceEnvironment) => ( + {currentProject?.environments.map((sourceEnvironment) => ( { const project = await context.queryClient.ensureQueryData({ - queryKey: workspaceKeys.getWorkspaceById(params.projectId), - queryFn: () => fetchWorkspaceById(params.projectId) + queryKey: projectKeys.getProjectById(params.projectId), + queryFn: () => fetchProjectById(params.projectId) }); await context.queryClient.ensureQueryData({ queryKey: roleQueryKeys.getUserProjectPermissions({ - workspaceId: params.projectId + projectId: params.projectId }), - queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId }) + queryFn: () => fetchUserProjectPermissions({ projectId: params.projectId }) }); return { diff --git a/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningScanRow.tsx b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningScanRow.tsx index 6dc7ceb35..804c4d790 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningScanRow.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningScanRow.tsx @@ -18,7 +18,7 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useToggle } from "@app/hooks"; import { SecretScanningScanStatus, @@ -41,7 +41,7 @@ export const SecretScanningScanRow = ({ scan }: Props) => { resolvedFindings, type } = scan; - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const totalFindings = resolvedFindings + unresolvedFindings; const navigate = useNavigate(); @@ -97,7 +97,7 @@ export const SecretScanningScanRow = ({ scan }: Props) => { navigate({ to: "/projects/secret-scanning/$projectId/findings", params: { - projectId: currentWorkspace.id + projectId: currentProject.id }, search: { search: `scanId:${id}` diff --git a/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/components/SecretScanningDataSourcesSection.tsx b/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/components/SecretScanningDataSourcesSection.tsx index 019ca3467..03c387440 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/components/SecretScanningDataSourcesSection.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/components/SecretScanningDataSourcesSection.tsx @@ -5,7 +5,7 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { ProjectPermissionCan } from "@app/components/permissions"; import { CreateSecretScanningDataSourceModal } from "@app/components/secret-scanning"; import { Button, Spinner } from "@app/components/v2"; -import { ProjectPermissionSub, useSubscription, useWorkspace } from "@app/context"; +import { ProjectPermissionSub, useProject, useSubscription } from "@app/context"; import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp } from "@app/hooks"; import { useListSecretScanningDataSources } from "@app/hooks/api/secretScanningV2"; @@ -20,10 +20,10 @@ export const SecretScanningDataSourcesSection = () => { const { subscription } = useSubscription(); - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: dataSources = [], isPending: isDataSourcesPending } = - useListSecretScanningDataSources(currentWorkspace.id, { + useListSecretScanningDataSources(currentProject.id, { refetchInterval: 30000, enabled: subscription.secretScanning }); diff --git a/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/route.tsx b/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/route.tsx index b13c99db7..87b2b43fb 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/route.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/route.tsx @@ -1,11 +1,19 @@ import { createFileRoute } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; import { SecretScanningDataSourcesPage } from "./SecretScanningDataSourcesPage"; +const SecretScanningDataSourcesPageQueryParamsSchema = z.object({ + connectionId: z.string().optional(), + connectionName: z.string().optional() +}); + export const Route = createFileRoute( "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/" )({ component: SecretScanningDataSourcesPage, + validateSearch: zodValidator(SecretScanningDataSourcesPageQueryParamsSchema), beforeLoad: ({ context }) => { return { breadcrumbs: [ diff --git a/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningFindingsSection.tsx b/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningFindingsSection.tsx index 82ea049f9..4a9cf1ad0 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningFindingsSection.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningFindingsSection.tsx @@ -2,16 +2,16 @@ import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-sv import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ContentLoader } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useListSecretScanningFindings } from "@app/hooks/api/secretScanningV2"; import { SecretScanningFindingsTable } from "./SecretScanningFindingsTable"; export const SecretScanningFindingsSection = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: findings = [], isPending: isFindingsPending } = useListSecretScanningFindings( - currentWorkspace.id, + currentProject.id, { refetchInterval: 30000 } diff --git a/frontend/src/pages/secret-scanning/SettingsPage/components/ProjectScanningConfigTab/ProjectScanningConfigTab.tsx b/frontend/src/pages/secret-scanning/SettingsPage/components/ProjectScanningConfigTab/ProjectScanningConfigTab.tsx index e8d3206d8..4cf9e2b22 100644 --- a/frontend/src/pages/secret-scanning/SettingsPage/components/ProjectScanningConfigTab/ProjectScanningConfigTab.tsx +++ b/frontend/src/pages/secret-scanning/SettingsPage/components/ProjectScanningConfigTab/ProjectScanningConfigTab.tsx @@ -1,16 +1,16 @@ import { faBan } from "@fortawesome/free-solid-svg-icons"; import { AccessRestrictedBanner, ContentLoader, EmptyState } from "@app/components/v2"; -import { useSubscription, useWorkspace } from "@app/context"; +import { useProject, useSubscription } from "@app/context"; import { useGetSecretScanningConfig } from "@app/hooks/api/secretScanningV2"; import { SecretScanningConfigForm } from "./SecretScanningConfigForm"; export const ProjectScanningConfigTab = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { subscription } = useSubscription(); const { data: config, isPending: isConfigPending } = useGetSecretScanningConfig( - currentWorkspace.id, + currentProject.id, { enabled: subscription.secretScanning } ); diff --git a/frontend/src/pages/secret-scanning/layout.tsx b/frontend/src/pages/secret-scanning/layout.tsx index 38899a233..13a43f895 100644 --- a/frontend/src/pages/secret-scanning/layout.tsx +++ b/frontend/src/pages/secret-scanning/layout.tsx @@ -1,9 +1,9 @@ import { createFileRoute } from "@tanstack/react-router"; import { BreadcrumbTypes } from "@app/components/v2"; -import { workspaceKeys } from "@app/hooks/api"; +import { projectKeys } from "@app/hooks/api"; +import { fetchProjectById } from "@app/hooks/api/projects/queries"; import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; -import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries"; import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect"; import { SecretScanningLayout } from "@app/layouts/SecretScanningLayout"; @@ -13,15 +13,15 @@ export const Route = createFileRoute( component: SecretScanningLayout, beforeLoad: async ({ params, context }) => { const project = await context.queryClient.ensureQueryData({ - queryKey: workspaceKeys.getWorkspaceById(params.projectId), - queryFn: () => fetchWorkspaceById(params.projectId) + queryKey: projectKeys.getProjectById(params.projectId), + queryFn: () => fetchProjectById(params.projectId) }); await context.queryClient.ensureQueryData({ queryKey: roleQueryKeys.getUserProjectPermissions({ - workspaceId: params.projectId + projectId: params.projectId }), - queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId }) + queryFn: () => fetchUserProjectPermissions({ projectId: params.projectId }) }); return { diff --git a/frontend/src/pages/ssh/SettingsPage/components/ProjectSshTab/components/ProjectSshConfigCasSection.tsx b/frontend/src/pages/ssh/SettingsPage/components/ProjectSshTab/components/ProjectSshConfigCasSection.tsx index 503717326..7b00b3ea0 100644 --- a/frontend/src/pages/ssh/SettingsPage/components/ProjectSshTab/components/ProjectSshConfigCasSection.tsx +++ b/frontend/src/pages/ssh/SettingsPage/components/ProjectSshTab/components/ProjectSshConfigCasSection.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, FormControl, Select, SelectItem } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { useGetProjectSshConfig, useListWorkspaceSshCas, @@ -23,9 +23,9 @@ const schema = z export type FormData = z.infer; export const ProjectSshConfigCasSection = () => { - const { currentWorkspace } = useWorkspace(); - const { data: sshConfig } = useGetProjectSshConfig(currentWorkspace.id); - const { data: sshCas } = useListWorkspaceSshCas(currentWorkspace.id); + const { currentProject } = useProject(); + const { data: sshConfig } = useGetProjectSshConfig(currentProject.id); + const { data: sshCas } = useListWorkspaceSshCas(currentProject.id); const { mutate: updateProjectSshConfig } = useUpdateProjectSshConfig(); const { @@ -49,7 +49,7 @@ export const ProjectSshConfigCasSection = () => { const onFormSubmit = async ({ defaultUserSshCaId, defaultHostSshCaId }: FormData) => { try { await updateProjectSshConfig({ - projectId: currentWorkspace.id, + projectId: currentProject.id, defaultUserSshCaId: defaultUserSshCaId || undefined, defaultHostSshCaId: defaultHostSshCaId || undefined }); diff --git a/frontend/src/pages/ssh/SshCaByIDPage/SshCaByIDPage.tsx b/frontend/src/pages/ssh/SshCaByIDPage/SshCaByIDPage.tsx index 8bed6e647..88a8c7f60 100644 --- a/frontend/src/pages/ssh/SshCaByIDPage/SshCaByIDPage.tsx +++ b/frontend/src/pages/ssh/SshCaByIDPage/SshCaByIDPage.tsx @@ -15,7 +15,7 @@ import { Tooltip } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { useDeleteSshCa, useGetSshCaById } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -23,9 +23,9 @@ import { SshCaModal } from "../SshCasPage/components/SshCaModal"; import { SshCaDetailsSection, SshCertificateTemplatesSection } from "./components"; const Page = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const navigate = useNavigate(); - const projectId = currentWorkspace?.id || ""; + const projectId = currentProject?.id || ""; const caId = useParams({ from: ROUTE_PATHS.Ssh.SshCaByIDPage.id, select: (el) => el.caId diff --git a/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateModal.tsx b/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateModal.tsx index 5c4673931..6e0baa502 100644 --- a/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateModal.tsx +++ b/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateModal.tsx @@ -14,7 +14,7 @@ import { Select, SelectItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { SshCertTemplateStatus, useGetSshCertTemplate, @@ -71,8 +71,8 @@ enum SshCertificateOperation { } export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const [operation, setOperation] = useState( SshCertificateOperation.SIGN_SSH_KEY ); diff --git a/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplateModal.tsx b/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplateModal.tsx index 55d4104ac..17c7a552e 100644 --- a/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplateModal.tsx +++ b/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplateModal.tsx @@ -16,7 +16,7 @@ import { SelectItem, Switch } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateSshCertTemplate, useGetSshCaById, @@ -77,7 +77,7 @@ type Props = { }; export const SshCertificateTemplateModal = ({ popUp, handlePopUpToggle, sshCaId }: Props) => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const { data: ca } = useGetSshCaById(sshCaId); @@ -85,7 +85,7 @@ export const SshCertificateTemplateModal = ({ popUp, handlePopUpToggle, sshCaId (popUp?.sshCertificateTemplate?.data as { id: string })?.id || "" ); - const { data: cas } = useListWorkspaceSshCas(currentWorkspace?.id || ""); + const { data: cas } = useListWorkspaceSshCas(currentProject?.id || ""); const { mutateAsync: createSshCertTemplate } = useCreateSshCertTemplate(); const { mutateAsync: updateSshCertTemplate } = useUpdateSshCertTemplate(); diff --git a/frontend/src/pages/ssh/SshCasPage/components/SshCaModal.tsx b/frontend/src/pages/ssh/SshCasPage/components/SshCaModal.tsx index 758af52a9..4c8ce4e19 100644 --- a/frontend/src/pages/ssh/SshCasPage/components/SshCaModal.tsx +++ b/frontend/src/pages/ssh/SshCasPage/components/SshCaModal.tsx @@ -15,7 +15,7 @@ import { SelectItem, TextArea } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateSshCa, useGetSshCaById, useUpdateSshCa } from "@app/hooks/api"; import { SshCaKeySource, @@ -54,8 +54,8 @@ export type FormData = z.infer; export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; const { data: ca } = useGetSshCaById((popUp?.sshCa?.data as { caId: string })?.caId || ""); const { mutateAsync: createMutateAsync } = useCreateSshCa(); diff --git a/frontend/src/pages/ssh/SshCasPage/components/SshCaTable.tsx b/frontend/src/pages/ssh/SshCasPage/components/SshCaTable.tsx index 37a086f37..c94f73664 100644 --- a/frontend/src/pages/ssh/SshCasPage/components/SshCaTable.tsx +++ b/frontend/src/pages/ssh/SshCasPage/components/SshCaTable.tsx @@ -21,7 +21,7 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { SshCaStatus, useListWorkspaceSshCas } from "@app/hooks/api"; import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -35,8 +35,8 @@ type Props = { export const SshCaTable = ({ handlePopUpOpen }: Props) => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); - const { data, isPending } = useListWorkspaceSshCas(currentWorkspace?.id || ""); + const { currentProject } = useProject(); + const { data, isPending } = useListWorkspaceSshCas(currentProject?.id || ""); return (
@@ -63,7 +63,7 @@ export const SshCaTable = ({ handlePopUpOpen }: Props) => { navigate({ to: "/projects/ssh/$projectId/ca/$caId", params: { - projectId: currentWorkspace.id, + projectId: currentProject.id, caId: ca.id } }) diff --git a/frontend/src/pages/ssh/SshCertsPage/components/SshCertificatesTable.tsx b/frontend/src/pages/ssh/SshCertsPage/components/SshCertificatesTable.tsx index c48b407ac..1ebf3112c 100644 --- a/frontend/src/pages/ssh/SshCertsPage/components/SshCertificatesTable.tsx +++ b/frontend/src/pages/ssh/SshCertsPage/components/SshCertificatesTable.tsx @@ -15,7 +15,7 @@ import { THead, Tr } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useListWorkspaceSshCertificates } from "@app/hooks/api"; import { getSshCertStatusBadgeDetails } from "./SshCertificatesTable.utils"; @@ -23,12 +23,12 @@ import { getSshCertStatusBadgeDetails } from "./SshCertificatesTable.utils"; const PER_PAGE_INIT = 25; export const SshCertificatesTable = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(PER_PAGE_INIT); const { data, isPending } = useListWorkspaceSshCertificates({ - projectId: currentWorkspace?.id || "", + projectId: currentProject?.id || "", offset: (page - 1) * perPage, limit: perPage }); diff --git a/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/SshHostGroupDetailsByIDPage.tsx b/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/SshHostGroupDetailsByIDPage.tsx index 6b43e8324..e030a8fa0 100644 --- a/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/SshHostGroupDetailsByIDPage.tsx +++ b/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/SshHostGroupDetailsByIDPage.tsx @@ -16,7 +16,7 @@ import { Tooltip } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { useDeleteSshHostGroup, useGetSshHostGroupById } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -24,9 +24,9 @@ import { SshHostGroupModal } from "../SshHostsPage/components/SshHostGroupModal" import { SshHostGroupDetailsSection, SshHostGroupHostsSection } from "./components"; const Page = () => { - const { currentWorkspace } = useWorkspace(); + const { currentProject } = useProject(); const navigate = useNavigate(); - const projectId = currentWorkspace?.id || ""; + const projectId = currentProject?.id || ""; const sshHostGroupId = useParams({ from: ROUTE_PATHS.Ssh.SshHostGroupDetailsByIDPage.id, select: (el) => el.sshHostGroupId diff --git a/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupModal.tsx b/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupModal.tsx index 84be5a96d..4905695d4 100644 --- a/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupModal.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupModal.tsx @@ -17,7 +17,7 @@ import { Select, SelectItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateSshHostGroup, useGetSshHostGroupById, @@ -56,8 +56,8 @@ const schema = z export type FormData = z.infer; export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace.id; + const { currentProject } = useProject(); + const projectId = currentProject.id; const { data: sshHostGroups } = useListWorkspaceSshHostGroups(projectId); const { data: members = [] } = useGetWorkspaceUsers(projectId); const { data: groups = [] } = useListWorkspaceGroups(projectId); diff --git a/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupsTable.tsx b/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupsTable.tsx index daedabdc9..af3a97dc6 100644 --- a/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupsTable.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupsTable.tsx @@ -28,7 +28,7 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { useListWorkspaceSshHostGroups } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -41,8 +41,8 @@ type Props = { export const SshHostGroupsTable = ({ handlePopUpOpen }: Props) => { const navigate = useNavigate(); - const { currentWorkspace } = useWorkspace(); - const { data, isPending } = useListWorkspaceSshHostGroups(currentWorkspace?.id || ""); + const { currentProject } = useProject(); + const { data, isPending } = useListWorkspaceSshHostGroups(currentProject?.id || ""); return (
@@ -69,7 +69,7 @@ export const SshHostGroupsTable = ({ handlePopUpOpen }: Props) => { navigate({ to: "/projects/ssh/$projectId/ssh-host-groups/$sshHostGroupId", params: { - projectId: currentWorkspace.id, + projectId: currentProject.id, sshHostGroupId: group.id } }) @@ -170,7 +170,7 @@ export const SshHostGroupsTable = ({ handlePopUpOpen }: Props) => { navigate({ to: "/projects/ssh/$projectId/ssh-host-groups/$sshHostGroupId", params: { - projectId: currentWorkspace.id, + projectId: currentProject.id, sshHostGroupId: group.id } }); diff --git a/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx b/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx index 5619a75c4..e1ea75143 100644 --- a/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx @@ -18,7 +18,7 @@ import { Select, SelectItem } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { useProject } from "@app/context"; import { useCreateSshHost, useGetSshHostById, @@ -68,9 +68,9 @@ const schema = z export type FormData = z.infer; export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); - const projectId = currentWorkspace?.id || ""; - const { data: sshHosts } = useListWorkspaceSshHosts(currentWorkspace.id); + const { currentProject } = useProject(); + const projectId = currentProject?.id || ""; + const { data: sshHosts } = useListWorkspaceSshHosts(currentProject.id); const { data: members = [] } = useGetWorkspaceUsers(projectId); const { data: groups = [] } = useListWorkspaceGroups(projectId); const [expandedMappings, setExpandedMappings] = useState>({}); diff --git a/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx b/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx index 62f563cce..f29f0147e 100644 --- a/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx @@ -30,7 +30,7 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionSshHostActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionSshHostActions, ProjectPermissionSub, useProject } from "@app/context"; import { fetchSshHostUserCaPublicKey, useListWorkspaceSshHosts } from "@app/hooks/api"; import { LoginMappingSource } from "@app/hooks/api/sshHost/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -43,8 +43,8 @@ type Props = { }; export const SshHostsTable = ({ handlePopUpOpen }: Props) => { - const { currentWorkspace } = useWorkspace(); - const { data, isPending } = useListWorkspaceSshHosts(currentWorkspace?.id || ""); + const { currentProject } = useProject(); + const { data, isPending } = useListWorkspaceSshHosts(currentProject?.id || ""); const downloadTxtFile = (filename: string, content: string) => { const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); diff --git a/frontend/src/pages/ssh/layout.tsx b/frontend/src/pages/ssh/layout.tsx index 008aca393..9b1a0057b 100644 --- a/frontend/src/pages/ssh/layout.tsx +++ b/frontend/src/pages/ssh/layout.tsx @@ -1,9 +1,9 @@ import { createFileRoute } from "@tanstack/react-router"; import { BreadcrumbTypes } from "@app/components/v2"; -import { workspaceKeys } from "@app/hooks/api"; +import { projectKeys } from "@app/hooks/api"; +import { fetchProjectById } from "@app/hooks/api/projects/queries"; import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; -import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries"; import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect"; import { SshLayout } from "@app/layouts/SshLayout"; @@ -13,15 +13,15 @@ export const Route = createFileRoute( component: SshLayout, beforeLoad: async ({ params, context }) => { const project = await context.queryClient.ensureQueryData({ - queryKey: workspaceKeys.getWorkspaceById(params.projectId), - queryFn: () => fetchWorkspaceById(params.projectId) + queryKey: projectKeys.getProjectById(params.projectId), + queryFn: () => fetchProjectById(params.projectId) }); await context.queryClient.ensureQueryData({ queryKey: roleQueryKeys.getUserProjectPermissions({ - workspaceId: params.projectId + projectId: params.projectId }), - queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId }) + queryFn: () => fetchUserProjectPermissions({ projectId: params.projectId }) }); return { diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/ChangeEmailSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/ChangeEmailSection.tsx new file mode 100644 index 000000000..723ed7498 --- /dev/null +++ b/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/ChangeEmailSection.tsx @@ -0,0 +1,245 @@ +import { useState } from "react"; +import ReactCodeInput from "react-code-input"; +import { Controller, useForm, useWatch } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useNavigate } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; +import { useUser } from "@app/context"; +import { useRequestEmailChangeOTP, useUpdateUserEmail } from "@app/hooks/api/users"; +import { clearSession } from "@app/hooks/api/users/queries"; + +const emailSchema = z + .object({ + newEmail: z.string().email("Please enter a valid email") + }) + .required(); + +export type EmailFormData = z.infer; + +const otpInputProps = { + inputStyle: { + fontFamily: "monospace", + margin: "4px", + MozAppearance: "textfield" as const, + width: "45px", + borderRadius: "6px", + fontSize: "18px", + height: "45px", + padding: "0", + paddingLeft: "0", + paddingRight: "0", + backgroundColor: "#262626", + color: "white", + border: "1px solid #404040", + textAlign: "center" as const, + outlineColor: "#8ca542", + borderColor: "#404040" + } +}; + +export const ChangeEmailSection = () => { + const navigate = useNavigate(); + const { user } = useUser(); + const [isOTPModalOpen, setIsOTPModalOpen] = useState(false); + const [pendingEmail, setPendingEmail] = useState(""); + + const emailForm = useForm({ + defaultValues: { newEmail: "" }, + resolver: zodResolver(emailSchema) + }); + + const { mutateAsync: requestEmailChangeOTP, isPending: isRequestingOTP } = + useRequestEmailChangeOTP(); + const { mutateAsync: updateUserEmail, isPending: isUpdatingEmail } = useUpdateUserEmail(); + + // Watch the email field to enable/disable the button + const watchedEmail = useWatch({ + control: emailForm.control, + name: "newEmail", + defaultValue: "" + }); + + // Helper function to check if email is valid + const isEmailValid = (email: string): boolean => { + try { + emailSchema.parse({ newEmail: email }); + return true; + } catch { + return false; + } + }; + + const handleEmailSubmit = async ({ newEmail }: EmailFormData) => { + if (newEmail.toLowerCase() === user?.email?.toLowerCase()) { + createNotification({ + text: "New email must be different from current email", + type: "error" + }); + return; + } + + try { + await requestEmailChangeOTP({ newEmail }); + setPendingEmail(newEmail); + setIsOTPModalOpen(true); + + createNotification({ + text: "Verification code sent to your new email address. Check your inbox!", + type: "success" + }); + } catch (err: any) { + console.error(err); + const errorMessage = err?.response?.data?.message || "Failed to send verification code"; + createNotification({ + text: errorMessage, + type: "error" + }); + } + }; + + const [typedOTP, setTypedOTP] = useState(""); + + const handleOTPSubmit = async () => { + if (typedOTP.length !== 6) { + createNotification({ + text: "Please enter the complete 6-digit verification code", + type: "error" + }); + return; + } + + try { + await updateUserEmail({ newEmail: pendingEmail, otpCode: typedOTP }); + + createNotification({ + text: "Email updated successfully. You will be redirected to login.", + type: "success" + }); + + // Reset forms and close modal + emailForm.reset(); + setIsOTPModalOpen(false); + setPendingEmail(""); + setTypedOTP(""); + + // Clear frontend session/token to ensure proper logout + clearSession(true); + + // Redirect to login after a short delay + setTimeout(() => { + navigate({ to: "/login" }); + }, 2000); + } catch (err: any) { + console.error(err); + + const errorMessage = err?.response?.data?.message || "Invalid verification code"; + if (errorMessage.includes("Invalid verification code")) { + // Reset to email step so user must request new OTP + setIsOTPModalOpen(false); + setPendingEmail(""); + setTypedOTP(""); + emailForm.reset(); + + createNotification({ + text: "Invalid verification code. Please request a new one.", + type: "error" + }); + } else { + createNotification({ + text: errorMessage, + type: "error" + }); + } + } + }; + + const handleOTPModalClose = () => { + setIsOTPModalOpen(false); + setPendingEmail(""); + setTypedOTP(""); + }; + + return ( + <> +
+

Change email

+ + +
+ ( + + + + )} + /> +
+ +

+ We'll send an 6-digit verification code to your new email address. +

+ +
+ + { + if (!isOpen) handleOTPModalClose(); + }} + > + +
+
+ +
+
+ + +
+
+
+
+ + ); +}; diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/index.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/index.tsx new file mode 100644 index 000000000..5a8804bde --- /dev/null +++ b/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/index.tsx @@ -0,0 +1 @@ +export { ChangeEmailSection } from "./ChangeEmailSection"; diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/PersonalAuthTab/PersonalAuthTab.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/PersonalAuthTab/PersonalAuthTab.tsx index b87d87dbb..2230f1a00 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/PersonalAuthTab/PersonalAuthTab.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/PersonalAuthTab/PersonalAuthTab.tsx @@ -2,6 +2,7 @@ import { useGetUser } from "@app/hooks/api"; import { AuthMethod } from "@app/hooks/api/users/types"; import { AuthMethodSection } from "../AuthMethodSection"; +import { ChangeEmailSection } from "../ChangeEmailSection"; import { ChangePasswordSection } from "../ChangePasswordSection"; import { MFASection } from "../SecuritySection"; @@ -16,6 +17,7 @@ export const PersonalAuthTab = () => { )} + {user && !user.authMethods.includes(AuthMethod.LDAP) && }
); }; diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index c2f1f78a9..4d01ff47d 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -47,15 +47,14 @@ import { Route as adminEnvironmentPageRouteImport } from './pages/admin/Environm import { Route as adminEncryptionPageRouteImport } from './pages/admin/EncryptionPage/route' import { Route as adminCachingPageRouteImport } from './pages/admin/CachingPage/route' import { Route as adminAuthenticationPageRouteImport } from './pages/admin/AuthenticationPage/route' +import { Route as adminAccessManagementPageRouteImport } from './pages/admin/AccessManagementPage/route' import { Route as organizationProjectsPageRouteImport } from './pages/organization/ProjectsPage/route' import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route' import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route' import { Route as organizationAccessManagementPageRouteImport } from './pages/organization/AccessManagementPage/route' import { Route as adminGeneralPageRouteImport } from './pages/admin/GeneralPage/route' import { Route as secretManagerRedirectsRedirectApprovalPageImport } from './pages/secret-manager/redirects/redirect-approval-page' -import { Route as adminUserIdentitiesResourcesPageRouteImport } from './pages/admin/UserIdentitiesResourcesPage/route' -import { Route as adminOrganizationResourcesPageRouteImport } from './pages/admin/OrganizationResourcesPage/route' -import { Route as adminMachineIdentitiesResourcesPageRouteImport } from './pages/admin/MachineIdentitiesResourcesPage/route' +import { Route as adminResourceOverviewPageRouteImport } from './pages/admin/ResourceOverviewPage/route' import { Route as organizationSecretSharingSettingsPageRouteImport } from './pages/organization/SecretSharingSettingsPage/route' import { Route as organizationRoleByIDPageRouteImport } from './pages/organization/RoleByIDPage/route' import { Route as organizationUserDetailsByIDPageRouteImport } from './pages/organization/UserDetailsByIDPage/route' @@ -83,12 +82,15 @@ import { Route as organizationSettingsPageOauthCallbackPageRouteImport } from '. import { Route as projectAuditLogsPageRouteSshImport } from './pages/project/AuditLogsPage/route-ssh' import { Route as projectAccessControlPageRouteSshImport } from './pages/project/AccessControlPage/route-ssh' import { Route as projectAuditLogsPageRouteSecretScanningImport } from './pages/project/AuditLogsPage/route-secret-scanning' +import { Route as projectAppConnectionsPageRouteSecretScanningImport } from './pages/project/AppConnectionsPage/route-secret-scanning' import { Route as projectAccessControlPageRouteSecretScanningImport } from './pages/project/AccessControlPage/route-secret-scanning' import { Route as projectAuditLogsPageRouteSecretManagerImport } from './pages/project/AuditLogsPage/route-secret-manager' +import { Route as projectAppConnectionsPageRouteSecretManagerImport } from './pages/project/AppConnectionsPage/route-secret-manager' import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager' import { Route as projectAuditLogsPageRouteKmsImport } from './pages/project/AuditLogsPage/route-kms' import { Route as projectAccessControlPageRouteKmsImport } from './pages/project/AccessControlPage/route-kms' import { Route as projectAuditLogsPageRouteCertManagerImport } from './pages/project/AuditLogsPage/route-cert-manager' +import { Route as projectAppConnectionsPageRouteCertManagerImport } from './pages/project/AppConnectionsPage/route-cert-manager' import { Route as projectAccessControlPageRouteCertManagerImport } from './pages/project/AccessControlPage/route-cert-manager' import { Route as sshSettingsPageRouteImport } from './pages/ssh/SettingsPage/route' import { Route as sshSshHostsPageRouteImport } from './pages/ssh/SshHostsPage/route' @@ -606,6 +608,13 @@ const adminAuthenticationPageRouteRoute = getParentRoute: () => adminLayoutRoute, } as any) +const adminAccessManagementPageRouteRoute = + adminAccessManagementPageRouteImport.update({ + id: '/access-management', + path: '/access-management', + getParentRoute: () => adminLayoutRoute, + } as any) + const organizationProjectsPageRouteRoute = organizationProjectsPageRouteImport.update({ id: '/projects', @@ -693,24 +702,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRoute } as any, ) -const adminUserIdentitiesResourcesPageRouteRoute = - adminUserIdentitiesResourcesPageRouteImport.update({ - id: '/resources/user-identities', - path: '/resources/user-identities', - getParentRoute: () => adminLayoutRoute, - } as any) - -const adminOrganizationResourcesPageRouteRoute = - adminOrganizationResourcesPageRouteImport.update({ - id: '/resources/organizations', - path: '/resources/organizations', - getParentRoute: () => adminLayoutRoute, - } as any) - -const adminMachineIdentitiesResourcesPageRouteRoute = - adminMachineIdentitiesResourcesPageRouteImport.update({ - id: '/resources/machine-identities', - path: '/resources/machine-identities', +const adminResourceOverviewPageRouteRoute = + adminResourceOverviewPageRouteImport.update({ + id: '/resources/overview', + path: '/resources/overview', getParentRoute: () => adminLayoutRoute, } as any) @@ -928,6 +923,13 @@ const projectAuditLogsPageRouteSecretScanningRoute = getParentRoute: () => secretScanningLayoutRoute, } as any) +const projectAppConnectionsPageRouteSecretScanningRoute = + projectAppConnectionsPageRouteSecretScanningImport.update({ + id: '/app-connections', + path: '/app-connections', + getParentRoute: () => secretScanningLayoutRoute, + } as any) + const projectAccessControlPageRouteSecretScanningRoute = projectAccessControlPageRouteSecretScanningImport.update({ id: '/access-management', @@ -951,6 +953,13 @@ const projectAuditLogsPageRouteSecretManagerRoute = getParentRoute: () => secretManagerLayoutRoute, } as any) +const projectAppConnectionsPageRouteSecretManagerRoute = + projectAppConnectionsPageRouteSecretManagerImport.update({ + id: '/app-connections', + path: '/app-connections', + getParentRoute: () => secretManagerLayoutRoute, + } as any) + const projectAccessControlPageRouteSecretManagerRoute = projectAccessControlPageRouteSecretManagerImport.update({ id: '/access-management', @@ -997,6 +1006,13 @@ const projectAuditLogsPageRouteCertManagerRoute = getParentRoute: () => certManagerLayoutRoute, } as any) +const projectAppConnectionsPageRouteCertManagerRoute = + projectAppConnectionsPageRouteCertManagerImport.update({ + id: '/app-connections', + path: '/app-connections', + getParentRoute: () => certManagerLayoutRoute, + } as any) + const projectAccessControlPageRouteCertManagerRoute = projectAccessControlPageRouteCertManagerImport.update({ id: '/access-management', @@ -2304,6 +2320,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationProjectsPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } + '/_authenticate/_inject-org-details/admin/_admin-layout/access-management': { + id: '/_authenticate/_inject-org-details/admin/_admin-layout/access-management' + path: '/access-management' + fullPath: '/admin/access-management' + preLoaderRoute: typeof adminAccessManagementPageRouteImport + parentRoute: typeof adminLayoutImport + } '/_authenticate/_inject-org-details/admin/_admin-layout/authentication': { id: '/_authenticate/_inject-org-details/admin/_admin-layout/authentication' path: '/authentication' @@ -2437,25 +2460,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationSecretSharingSettingsPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport } - '/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities': { - id: '/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities' - path: '/resources/machine-identities' - fullPath: '/admin/resources/machine-identities' - preLoaderRoute: typeof adminMachineIdentitiesResourcesPageRouteImport - parentRoute: typeof adminLayoutImport - } - '/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations': { - id: '/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations' - path: '/resources/organizations' - fullPath: '/admin/resources/organizations' - preLoaderRoute: typeof adminOrganizationResourcesPageRouteImport - parentRoute: typeof adminLayoutImport - } - '/_authenticate/_inject-org-details/admin/_admin-layout/resources/user-identities': { - id: '/_authenticate/_inject-org-details/admin/_admin-layout/resources/user-identities' - path: '/resources/user-identities' - fullPath: '/admin/resources/user-identities' - preLoaderRoute: typeof adminUserIdentitiesResourcesPageRouteImport + '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview': { + id: '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview' + path: '/resources/overview' + fullPath: '/admin/resources/overview' + preLoaderRoute: typeof adminResourceOverviewPageRouteImport parentRoute: typeof adminLayoutImport } '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId': { @@ -2745,6 +2754,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteCertManagerImport parentRoute: typeof certManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections': { + id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections' + path: '/app-connections' + fullPath: '/projects/cert-management/$projectId/app-connections' + preLoaderRoute: typeof projectAppConnectionsPageRouteCertManagerImport + parentRoute: typeof certManagerLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs': { id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs' path: '/audit-logs' @@ -2787,6 +2803,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteSecretManagerImport parentRoute: typeof secretManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections': { + id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections' + path: '/app-connections' + fullPath: '/projects/secret-management/$projectId/app-connections' + preLoaderRoute: typeof projectAppConnectionsPageRouteSecretManagerImport + parentRoute: typeof secretManagerLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs': { id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs' path: '/audit-logs' @@ -2808,6 +2831,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteSecretScanningImport parentRoute: typeof secretScanningLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections': { + id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections' + path: '/app-connections' + fullPath: '/projects/secret-scanning/$projectId/app-connections' + preLoaderRoute: typeof projectAppConnectionsPageRouteSecretScanningImport + parentRoute: typeof secretScanningLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs': { id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs' path: '/audit-logs' @@ -3837,6 +3867,7 @@ interface certManagerLayoutRouteChildren { certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute + projectAppConnectionsPageRouteCertManagerRoute: typeof projectAppConnectionsPageRouteCertManagerRoute projectAuditLogsPageRouteCertManagerRoute: typeof projectAuditLogsPageRouteCertManagerRoute AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren @@ -3856,6 +3887,8 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = { certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute, projectAccessControlPageRouteCertManagerRoute: projectAccessControlPageRouteCertManagerRoute, + projectAppConnectionsPageRouteCertManagerRoute: + projectAppConnectionsPageRouteCertManagerRoute, projectAuditLogsPageRouteCertManagerRoute: projectAuditLogsPageRouteCertManagerRoute, AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRoute: @@ -4227,6 +4260,7 @@ interface secretManagerLayoutRouteChildren { secretManagerSecretRotationPageRouteRoute: typeof secretManagerSecretRotationPageRouteRoute secretManagerSettingsPageRouteRoute: typeof secretManagerSettingsPageRouteRoute projectAccessControlPageRouteSecretManagerRoute: typeof projectAccessControlPageRouteSecretManagerRoute + projectAppConnectionsPageRouteSecretManagerRoute: typeof projectAppConnectionsPageRouteSecretManagerRoute projectAuditLogsPageRouteSecretManagerRoute: typeof projectAuditLogsPageRouteSecretManagerRoute AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren secretManagerSecretDashboardPageRouteRoute: typeof secretManagerSecretDashboardPageRouteRoute @@ -4248,6 +4282,8 @@ const secretManagerLayoutRouteChildren: secretManagerLayoutRouteChildren = { secretManagerSettingsPageRouteRoute: secretManagerSettingsPageRouteRoute, projectAccessControlPageRouteSecretManagerRoute: projectAccessControlPageRouteSecretManagerRoute, + projectAppConnectionsPageRouteSecretManagerRoute: + projectAppConnectionsPageRouteSecretManagerRoute, projectAuditLogsPageRouteSecretManagerRoute: projectAuditLogsPageRouteSecretManagerRoute, AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute: @@ -4305,6 +4341,7 @@ interface secretScanningLayoutRouteChildren { secretScanningSecretScanningFindingsPageRouteRoute: typeof secretScanningSecretScanningFindingsPageRouteRoute secretScanningSettingsPageRouteRoute: typeof secretScanningSettingsPageRouteRoute projectAccessControlPageRouteSecretScanningRoute: typeof projectAccessControlPageRouteSecretScanningRoute + projectAppConnectionsPageRouteSecretScanningRoute: typeof projectAppConnectionsPageRouteSecretScanningRoute projectAuditLogsPageRouteSecretScanningRoute: typeof projectAuditLogsPageRouteSecretScanningRoute AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren projectGroupDetailsByIDPageRouteSecretScanningRoute: typeof projectGroupDetailsByIDPageRouteSecretScanningRoute @@ -4319,6 +4356,8 @@ const secretScanningLayoutRouteChildren: secretScanningLayoutRouteChildren = { secretScanningSettingsPageRouteRoute: secretScanningSettingsPageRouteRoute, projectAccessControlPageRouteSecretScanningRoute: projectAccessControlPageRouteSecretScanningRoute, + projectAppConnectionsPageRouteSecretScanningRoute: + projectAppConnectionsPageRouteSecretScanningRoute, projectAuditLogsPageRouteSecretScanningRoute: projectAuditLogsPageRouteSecretScanningRoute, AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute: @@ -4438,29 +4477,24 @@ const organizationLayoutRouteWithChildren = interface adminLayoutRouteChildren { adminGeneralPageRouteRoute: typeof adminGeneralPageRouteRoute + adminAccessManagementPageRouteRoute: typeof adminAccessManagementPageRouteRoute adminAuthenticationPageRouteRoute: typeof adminAuthenticationPageRouteRoute adminCachingPageRouteRoute: typeof adminCachingPageRouteRoute adminEncryptionPageRouteRoute: typeof adminEncryptionPageRouteRoute adminEnvironmentPageRouteRoute: typeof adminEnvironmentPageRouteRoute adminIntegrationsPageRouteRoute: typeof adminIntegrationsPageRouteRoute - adminMachineIdentitiesResourcesPageRouteRoute: typeof adminMachineIdentitiesResourcesPageRouteRoute - adminOrganizationResourcesPageRouteRoute: typeof adminOrganizationResourcesPageRouteRoute - adminUserIdentitiesResourcesPageRouteRoute: typeof adminUserIdentitiesResourcesPageRouteRoute + adminResourceOverviewPageRouteRoute: typeof adminResourceOverviewPageRouteRoute } const adminLayoutRouteChildren: adminLayoutRouteChildren = { adminGeneralPageRouteRoute: adminGeneralPageRouteRoute, + adminAccessManagementPageRouteRoute: adminAccessManagementPageRouteRoute, adminAuthenticationPageRouteRoute: adminAuthenticationPageRouteRoute, adminCachingPageRouteRoute: adminCachingPageRouteRoute, adminEncryptionPageRouteRoute: adminEncryptionPageRouteRoute, adminEnvironmentPageRouteRoute: adminEnvironmentPageRouteRoute, adminIntegrationsPageRouteRoute: adminIntegrationsPageRouteRoute, - adminMachineIdentitiesResourcesPageRouteRoute: - adminMachineIdentitiesResourcesPageRouteRoute, - adminOrganizationResourcesPageRouteRoute: - adminOrganizationResourcesPageRouteRoute, - adminUserIdentitiesResourcesPageRouteRoute: - adminUserIdentitiesResourcesPageRouteRoute, + adminResourceOverviewPageRouteRoute: adminResourceOverviewPageRouteRoute, } const adminLayoutRouteWithChildren = adminLayoutRoute._addFileChildren( @@ -4652,6 +4686,7 @@ export interface FileRoutesByFullPath { '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute '/organization/projects': typeof organizationProjectsPageRouteRoute + '/admin/access-management': typeof adminAccessManagementPageRouteRoute '/admin/authentication': typeof adminAuthenticationPageRouteRoute '/admin/caching': typeof adminCachingPageRouteRoute '/admin/encryption': typeof adminEncryptionPageRouteRoute @@ -4671,9 +4706,7 @@ export interface FileRoutesByFullPath { '/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute '/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute - '/admin/resources/machine-identities': typeof adminMachineIdentitiesResourcesPageRouteRoute - '/admin/resources/organizations': typeof adminOrganizationResourcesPageRouteRoute - '/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute + '/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute '/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren '/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren '/projects/secret-management/$projectId': typeof secretManagerLayoutRouteWithChildren @@ -4710,15 +4743,18 @@ export interface FileRoutesByFullPath { '/projects/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute '/projects/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute '/projects/cert-management/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/projects/cert-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteCertManagerRoute '/projects/cert-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute '/projects/cert-management/$projectId/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren '/projects/cert-management/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren '/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute '/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute '/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute + '/projects/secret-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute '/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute '/projects/secret-management/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/projects/secret-scanning/$projectId/access-management': typeof projectAccessControlPageRouteSecretScanningRoute + '/projects/secret-scanning/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretScanningRoute '/projects/secret-scanning/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute '/projects/secret-scanning/$projectId/data-sources': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren '/projects/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute @@ -4870,6 +4906,7 @@ export interface FileRoutesByTo { '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute '/organization/projects': typeof organizationProjectsPageRouteRoute + '/admin/access-management': typeof adminAccessManagementPageRouteRoute '/admin/authentication': typeof adminAuthenticationPageRouteRoute '/admin/caching': typeof adminCachingPageRouteRoute '/admin/encryption': typeof adminEncryptionPageRouteRoute @@ -4885,9 +4922,7 @@ export interface FileRoutesByTo { '/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute '/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute - '/admin/resources/machine-identities': typeof adminMachineIdentitiesResourcesPageRouteRoute - '/admin/resources/organizations': typeof adminOrganizationResourcesPageRouteRoute - '/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute + '/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute '/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren '/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren '/projects/secret-management/$projectId': typeof secretManagerLayoutRouteWithChildren @@ -4924,12 +4959,15 @@ export interface FileRoutesByTo { '/projects/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute '/projects/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute '/projects/cert-management/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/projects/cert-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteCertManagerRoute '/projects/cert-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute '/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute '/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute '/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute + '/projects/secret-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute '/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute '/projects/secret-scanning/$projectId/access-management': typeof projectAccessControlPageRouteSecretScanningRoute + '/projects/secret-scanning/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretScanningRoute '/projects/secret-scanning/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute '/projects/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute '/projects/ssh/$projectId/audit-logs': typeof projectAuditLogsPageRouteSshRoute @@ -5088,6 +5126,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/projects': typeof organizationProjectsPageRouteRoute + '/_authenticate/_inject-org-details/admin/_admin-layout/access-management': typeof adminAccessManagementPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/authentication': typeof adminAuthenticationPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/caching': typeof adminCachingPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/encryption': typeof adminEncryptionPageRouteRoute @@ -5107,9 +5146,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute - '/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities': typeof adminMachineIdentitiesResourcesPageRouteRoute - '/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations': typeof adminOrganizationResourcesPageRouteRoute - '/_authenticate/_inject-org-details/admin/_admin-layout/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute + '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview': typeof adminResourceOverviewPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRouteWithChildren @@ -5151,15 +5188,18 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/overview': typeof sshSshHostsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections': typeof projectAppConnectionsPageRouteCertManagerRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management': typeof projectAccessControlPageRouteKmsRoute '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs': typeof projectAuditLogsPageRouteKmsRoute '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management': typeof projectAccessControlPageRouteSecretManagerRoute + '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management': typeof projectAccessControlPageRouteSecretScanningRoute + '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections': typeof projectAppConnectionsPageRouteSecretScanningRoute '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute @@ -5317,6 +5357,7 @@ export interface FileRouteTypes { | '/organization/audit-logs' | '/organization/billing' | '/organization/projects' + | '/admin/access-management' | '/admin/authentication' | '/admin/caching' | '/admin/encryption' @@ -5336,9 +5377,7 @@ export interface FileRouteTypes { | '/organization/members/$membershipId' | '/organization/roles/$roleId' | '/organization/secret-sharing/settings' - | '/admin/resources/machine-identities' - | '/admin/resources/organizations' - | '/admin/resources/user-identities' + | '/admin/resources/overview' | '/projects/cert-management/$projectId' | '/projects/kms/$projectId' | '/projects/secret-management/$projectId' @@ -5375,15 +5414,18 @@ export interface FileRouteTypes { | '/projects/ssh/$projectId/overview' | '/projects/ssh/$projectId/settings' | '/projects/cert-management/$projectId/access-management' + | '/projects/cert-management/$projectId/app-connections' | '/projects/cert-management/$projectId/audit-logs' | '/projects/cert-management/$projectId/certificate-templates' | '/projects/cert-management/$projectId/subscribers' | '/projects/kms/$projectId/access-management' | '/projects/kms/$projectId/audit-logs' | '/projects/secret-management/$projectId/access-management' + | '/projects/secret-management/$projectId/app-connections' | '/projects/secret-management/$projectId/audit-logs' | '/projects/secret-management/$projectId/integrations' | '/projects/secret-scanning/$projectId/access-management' + | '/projects/secret-scanning/$projectId/app-connections' | '/projects/secret-scanning/$projectId/audit-logs' | '/projects/secret-scanning/$projectId/data-sources' | '/projects/ssh/$projectId/access-management' @@ -5534,6 +5576,7 @@ export interface FileRouteTypes { | '/organization/audit-logs' | '/organization/billing' | '/organization/projects' + | '/admin/access-management' | '/admin/authentication' | '/admin/caching' | '/admin/encryption' @@ -5549,9 +5592,7 @@ export interface FileRouteTypes { | '/organization/members/$membershipId' | '/organization/roles/$roleId' | '/organization/secret-sharing/settings' - | '/admin/resources/machine-identities' - | '/admin/resources/organizations' - | '/admin/resources/user-identities' + | '/admin/resources/overview' | '/projects/cert-management/$projectId' | '/projects/kms/$projectId' | '/projects/secret-management/$projectId' @@ -5588,12 +5629,15 @@ export interface FileRouteTypes { | '/projects/ssh/$projectId/overview' | '/projects/ssh/$projectId/settings' | '/projects/cert-management/$projectId/access-management' + | '/projects/cert-management/$projectId/app-connections' | '/projects/cert-management/$projectId/audit-logs' | '/projects/kms/$projectId/access-management' | '/projects/kms/$projectId/audit-logs' | '/projects/secret-management/$projectId/access-management' + | '/projects/secret-management/$projectId/app-connections' | '/projects/secret-management/$projectId/audit-logs' | '/projects/secret-scanning/$projectId/access-management' + | '/projects/secret-scanning/$projectId/app-connections' | '/projects/secret-scanning/$projectId/audit-logs' | '/projects/ssh/$projectId/access-management' | '/projects/ssh/$projectId/audit-logs' @@ -5750,6 +5794,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/organization/billing' | '/_authenticate/_inject-org-details/_org-layout/organization/projects' + | '/_authenticate/_inject-org-details/admin/_admin-layout/access-management' | '/_authenticate/_inject-org-details/admin/_admin-layout/authentication' | '/_authenticate/_inject-org-details/admin/_admin-layout/caching' | '/_authenticate/_inject-org-details/admin/_admin-layout/encryption' @@ -5769,9 +5814,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings' - | '/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities' - | '/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations' - | '/_authenticate/_inject-org-details/admin/_admin-layout/resources/user-identities' + | '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId' | '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId' @@ -5813,15 +5856,18 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/overview' | '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/settings' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers' | '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management' | '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources' | '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management' @@ -6195,14 +6241,13 @@ export const routeTree = rootRoute "parent": "/_authenticate/_inject-org-details/admin", "children": [ "/_authenticate/_inject-org-details/admin/_admin-layout/", + "/_authenticate/_inject-org-details/admin/_admin-layout/access-management", "/_authenticate/_inject-org-details/admin/_admin-layout/authentication", "/_authenticate/_inject-org-details/admin/_admin-layout/caching", "/_authenticate/_inject-org-details/admin/_admin-layout/encryption", "/_authenticate/_inject-org-details/admin/_admin-layout/environment", "/_authenticate/_inject-org-details/admin/_admin-layout/integrations", - "/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities", - "/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations", - "/_authenticate/_inject-org-details/admin/_admin-layout/resources/user-identities" + "/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview" ] }, "/_authenticate/_inject-org-details/admin/_admin-layout/": { @@ -6225,6 +6270,10 @@ export const routeTree = rootRoute "filePath": "organization/ProjectsPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, + "/_authenticate/_inject-org-details/admin/_admin-layout/access-management": { + "filePath": "admin/AccessManagementPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/admin/_admin-layout" + }, "/_authenticate/_inject-org-details/admin/_admin-layout/authentication": { "filePath": "admin/AuthenticationPage/route.tsx", "parent": "/_authenticate/_inject-org-details/admin/_admin-layout" @@ -6319,16 +6368,8 @@ export const routeTree = rootRoute "filePath": "organization/SecretSharingSettingsPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing" }, - "/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities": { - "filePath": "admin/MachineIdentitiesResourcesPage/route.tsx", - "parent": "/_authenticate/_inject-org-details/admin/_admin-layout" - }, - "/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations": { - "filePath": "admin/OrganizationResourcesPage/route.tsx", - "parent": "/_authenticate/_inject-org-details/admin/_admin-layout" - }, - "/_authenticate/_inject-org-details/admin/_admin-layout/resources/user-identities": { - "filePath": "admin/UserIdentitiesResourcesPage/route.tsx", + "/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview": { + "filePath": "admin/ResourceOverviewPage/route.tsx", "parent": "/_authenticate/_inject-org-details/admin/_admin-layout" }, "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId": { @@ -6419,6 +6460,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers", @@ -6455,6 +6497,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secret-rotation", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/settings", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug", @@ -6472,6 +6515,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/findings", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/settings", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId", @@ -6578,6 +6622,10 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-cert-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections": { + "filePath": "project/AppConnectionsPage/route-cert-manager.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout" + }, "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs": { "filePath": "project/AuditLogsPage/route-cert-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout" @@ -6609,6 +6657,10 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-secret-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections": { + "filePath": "project/AppConnectionsPage/route-secret-manager.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout" + }, "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs": { "filePath": "project/AuditLogsPage/route-secret-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout" @@ -6701,6 +6753,10 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-secret-scanning.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout" }, + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections": { + "filePath": "project/AppConnectionsPage/route-secret-scanning.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout" + }, "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs": { "filePath": "project/AuditLogsPage/route-secret-scanning.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index cca823f21..76680a851 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -11,9 +11,8 @@ const adminRoute = route("/admin", [ route("/environment", "admin/EnvironmentPage/route.tsx"), route("/integrations", "admin/IntegrationsPage/route.tsx"), route("/caching", "admin/CachingPage/route.tsx"), - route("/resources/organizations", "admin/OrganizationResourcesPage/route.tsx"), - route("/resources/user-identities", "admin/UserIdentitiesResourcesPage/route.tsx"), - route("/resources/machine-identities", "admin/MachineIdentitiesResourcesPage/route.tsx") + route("/resources/overview", "admin/ResourceOverviewPage/route.tsx"), + route("/access-management", "admin/AccessManagementPage/route.tsx") ]) ]); @@ -64,6 +63,7 @@ const secretManagerRoutes = route("/projects/secret-management/$projectId", [ ]), route("/audit-logs", "project/AuditLogsPage/route-secret-manager.tsx"), route("/access-management", "project/AccessControlPage/route-secret-manager.tsx"), + route("/app-connections", "project/AppConnectionsPage/route-secret-manager.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-manager.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-manager.tsx"), route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-manager.tsx"), @@ -314,6 +314,7 @@ const certManagerRoutes = route("/projects/cert-management/$projectId", [ route("/settings", "cert-manager/SettingsPage/route.tsx"), route("/audit-logs", "project/AuditLogsPage/route-cert-manager.tsx"), route("/access-management", "project/AccessControlPage/route-cert-manager.tsx"), + route("/app-connections", "project/AppConnectionsPage/route-cert-manager.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-cert-manager.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-cert-manager.tsx"), route("/members/$membershipId", "project/MemberDetailsByIDPage/route-cert-manager.tsx"), @@ -362,6 +363,7 @@ const secretScanningRoutes = route("/projects/secret-scanning/$projectId", [ route("/settings", "secret-scanning/SettingsPage/route.tsx"), route("/audit-logs", "project/AuditLogsPage/route-secret-scanning.tsx"), route("/access-management", "project/AccessControlPage/route-secret-scanning.tsx"), + route("/app-connections", "project/AppConnectionsPage/route-secret-scanning.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-scanning.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-scanning.tsx"), route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-scanning.tsx"), diff --git a/helm-charts/infisical-gateway/Chart.yaml b/helm-charts/infisical-gateway/Chart.yaml index 17c0a3785..2dc9ef796 100644 --- a/helm-charts/infisical-gateway/Chart.yaml +++ b/helm-charts/infisical-gateway/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.0.5 +version: 1.0.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.0.5" +appVersion: "1.0.0" diff --git a/helm-charts/infisical-gateway/templates/deployment.yaml b/helm-charts/infisical-gateway/templates/deployment.yaml index a6fac0e7c..d31a9c9e9 100644 --- a/helm-charts/infisical-gateway/templates/deployment.yaml +++ b/helm-charts/infisical-gateway/templates/deployment.yaml @@ -39,6 +39,7 @@ spec: imagePullPolicy: {{ .Values.image.pullPolicy }} args: - gateway + - start envFrom: - secretRef: name: {{ .Values.secret.name }} diff --git a/helm-charts/infisical-gateway/values.yaml b/helm-charts/infisical-gateway/values.yaml index 2e293d1e1..67874055f 100644 --- a/helm-charts/infisical-gateway/values.yaml +++ b/helm-charts/infisical-gateway/values.yaml @@ -1,6 +1,6 @@ image: pullPolicy: IfNotPresent - tag: "0.41.84" + tag: "0.42.0" secret: # The secret that contains the environment variables to be used by the gateway, such as INFISICAL_API_URL and TOKEN diff --git a/sink/redis-cluster/README.md b/sink/redis-cluster/README.md new file mode 100644 index 000000000..6f4a40a80 --- /dev/null +++ b/sink/redis-cluster/README.md @@ -0,0 +1,42 @@ +# Redis Cluster Setup + +## Quick Start + +1. **Update IP Address**: Replace `192.168.1.33` with your system's IP address in `docker-compose.yml`: + ```bash + # Find your IP + ifconfig | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | head -1 + ``` + +2. **Start Cluster**: + ```bash + docker compose up -d + ``` + +3. **Verify Cluster**: + ```bash + docker exec redis-node-1 redis-cli -p 7001 cluster info + ``` + +## Connection Details + +- **Redis Cluster**: `YOUR_IP:7001`, `YOUR_IP:7002`, `YOUR_IP:7003` +- **RedisInsight UI**: `localhost:5540` + +## Clean Restart + +To completely reset the cluster: +```bash +docker compose down -v +# Update IP in docker-compose.yml if needed +docker compose up -d +``` + +## External Docker Compose Usage + +```yaml +environment: + - REDIS_CLUSTER_URLS=redis://YOUR_IP:7001,redis://YOUR_IP:7002,redis://YOUR_IP:7003 +``` + +**Important**: Always replace `YOUR_IP` with your actual system IP address. \ No newline at end of file diff --git a/sink/redis-cluster/docker-compose.yml b/sink/redis-cluster/docker-compose.yml new file mode 100644 index 000000000..05c7f7cc7 --- /dev/null +++ b/sink/redis-cluster/docker-compose.yml @@ -0,0 +1,87 @@ +version: "3.8" + +services: + redis-node-1: + image: redis:7 + container_name: redis-node-1 + ports: + - "7001:7001" + - "17001:17001" + volumes: + - redis-node-1-data:/data + command: > + redis-server + --port 7001 + --cluster-enabled yes + --cluster-config-file nodes.conf + --cluster-node-timeout 5000 + --appendonly yes + --bind 0.0.0.0 + --cluster-announce-ip 192.168.1.33 + --cluster-announce-port 7001 + --cluster-announce-bus-port 17001 + + redis-node-2: + image: redis:7 + container_name: redis-node-2 + ports: + - "7002:7002" + - "17002:17002" + volumes: + - redis-node-2-data:/data + command: > + redis-server + --port 7002 + --cluster-enabled yes + --cluster-config-file nodes.conf + --cluster-node-timeout 5000 + --appendonly yes + --bind 0.0.0.0 + --cluster-announce-ip 192.168.1.33 + --cluster-announce-port 7002 + --cluster-announce-bus-port 17002 + + redis-node-3: + image: redis:7 + container_name: redis-node-3 + ports: + - "7003:7003" + - "17003:17003" + volumes: + - redis-node-3-data:/data + command: > + redis-server + --port 7003 + --cluster-enabled yes + --cluster-config-file nodes.conf + --cluster-node-timeout 5000 + --appendonly yes + --bind 0.0.0.0 + --cluster-announce-ip 192.168.1.33 + --cluster-announce-port 7003 + --cluster-announce-bus-port 17003 + + redis-insight: + container_name: redis-insight + image: redis/redisinsight + ports: + - "5540:5540" + + redis-cluster-init: + image: redis:7 + depends_on: + - redis-node-1 + - redis-node-2 + - redis-node-3 + restart: "no" + command: > + sh -c " + sleep 10 + redis-cli --cluster create 192.168.1.33:7001 192.168.1.33:7002 192.168.1.33:7003 --cluster-yes + echo 'Cluster initialized' + " + +volumes: + redis-node-1-data: + redis-node-2-data: + redis-node-3-data: \ No newline at end of file