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/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/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/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/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/index.ts b/backend/src/ee/routes/v1/index.ts
index 5a43de381..56d450df3 100644
--- a/backend/src/ee/routes/v1/index.ts
+++ b/backend/src/ee/routes/v1/index.ts
@@ -5,6 +5,9 @@ import { registerAccessApprovalRequestRouter } from "./access-approval-request-r
import { registerAssumePrivilegeRouter } from "./assume-privilege-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";
@@ -27,7 +30,6 @@ 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";
@@ -47,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"
});
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/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/index.ts b/backend/src/ee/routes/v2/index.ts
index e082773dd..c402ab00a 100644
--- a/backend/src/ee/routes/v2/index.ts
+++ b/backend/src/ee/routes/v2/index.ts
@@ -7,15 +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" }
);
@@ -26,6 +27,8 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
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/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts
index fa5208ad4..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",
@@ -250,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",
@@ -265,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",
@@ -393,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",
@@ -665,8 +667,8 @@ interface DeleteSecretBatchEvent {
};
}
-interface GetWorkspaceKeyEvent {
- type: EventType.GET_WORKSPACE_KEY;
+interface GetProjectKeyEvent {
+ type: EventType.GET_PROJECT_KEY;
metadata: {
keyId: string;
};
@@ -1573,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;
@@ -1729,7 +1731,7 @@ interface DeleteSecretImportEvent {
}
interface UpdateUserRole {
- type: EventType.UPDATE_USER_WORKSPACE_ROLE;
+ type: EventType.UPDATE_USER_PROJECT_ROLE;
metadata: {
userId: string;
email: string;
@@ -1739,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;
@@ -2797,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 {
@@ -3493,7 +3512,7 @@ export type Event =
| MoveSecretsEvent
| DeleteSecretEvent
| DeleteSecretBatchEvent
- | GetWorkspaceKeyEvent
+ | GetProjectKeyEvent
| AuthorizeIntegrationEvent
| UpdateIntegrationAuthEvent
| UnauthorizeIntegrationEvent
@@ -3583,9 +3602,9 @@ export type Event =
| GetEnvironmentEvent
| UpdateEnvironmentEvent
| DeleteEnvironmentEvent
- | AddWorkspaceMemberEvent
- | AddBatchWorkspaceMemberEvent
- | RemoveWorkspaceMemberEvent
+ | AddProjectMemberEvent
+ | AddBatchProjectMemberEvent
+ | RemoveProjectMemberEvent
| CreateFolderEvent
| UpdateFolderEvent
| DeleteFolderEvent
@@ -3714,6 +3733,8 @@ export type Event =
| CreateAppConnectionEvent
| UpdateAppConnectionEvent
| DeleteAppConnectionEvent
+ | GetAppConnectionUsageEvent
+ | MigrateAppConnectionEvent
| GetSshHostGroupEvent
| CreateSshHostGroupEvent
| UpdateSshHostGroupEvent
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/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;
@@ -459,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](
{
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 557e71e6c..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
@@ -431,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/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index 64ed15073..53a2ca993 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -724,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.",
@@ -742,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.",
@@ -772,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."
@@ -828,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."
}
@@ -890,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)",
@@ -926,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.",
@@ -940,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."
@@ -977,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.",
@@ -997,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."
@@ -1005,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.",
@@ -1024,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.",
@@ -1038,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."
},
@@ -1056,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."
},
@@ -1066,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: {
@@ -1083,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."
@@ -2198,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];
@@ -2211,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/server/routes/index.ts b/backend/src/server/routes/index.ts
index 69dea2f8d..bb7d202da 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -328,6 +328,7 @@ 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();
@@ -1838,7 +1839,8 @@ export const registerRoutes = async (
gatewayService,
gatewayV2Service,
gatewayDAL,
- gatewayV2DAL
+ gatewayV2DAL,
+ projectDAL
});
const secretSyncService = secretSyncServiceFactory({
@@ -2295,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/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/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 6108be32b..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";
@@ -45,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";
@@ -87,8 +93,8 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
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) => {
@@ -101,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/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/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/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 5aacab1b1..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,
@@ -8,6 +9,8 @@ import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee
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";
@@ -16,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,
@@ -133,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(),
@@ -173,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))
@@ -200,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
@@ -343,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")
@@ -413,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 f5654d2fb..19d66b9fd 100644
--- a/backend/src/services/app-connection/app-connection-service.ts
+++ b/backend/src/services/app-connection/app-connection-service.ts
@@ -1,5 +1,6 @@
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";
@@ -14,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";
@@ -27,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";
@@ -41,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";
@@ -73,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";
@@ -108,13 +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;
@@ -168,29 +177,64 @@ export const appConnectionServiceFactory = ({
gatewayService,
gatewayV2Service,
gatewayDAL,
- gatewayV2DAL
+ 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
@@ -204,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}"` });
@@ -223,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}"` });
@@ -249,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,
@@ -260,13 +345,33 @@ 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
);
@@ -304,7 +409,8 @@ export const appConnectionServiceFactory = ({
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: connectionCredentials,
orgId: actor.orgId,
- kmsService
+ kmsService,
+ projectId
});
return appConnectionDAL.create({
@@ -313,6 +419,7 @@ export const appConnectionServiceFactory = ({
method,
app,
gatewayId,
+ projectId,
...params
});
};
@@ -365,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,
@@ -373,13 +480,29 @@ 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
);
@@ -441,7 +564,8 @@ export const appConnectionServiceFactory = ({
? await encryptAppConnectionCredentials({
credentials: connectionCredentials,
orgId: actor.orgId,
- kmsService
+ kmsService,
+ projectId: appConnection.projectId
})
: undefined;
@@ -491,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}"` });
@@ -544,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({
@@ -569,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,
@@ -578,28 +750,89 @@ 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,
+ findAppConnectionUsageById,
github: githubConnectionService(connectAppConnectionById, gatewayService, gatewayV2Service),
githubRadar: githubRadarConnectionService(connectAppConnectionById),
gcp: gcpConnectionService(connectAppConnectionById),
diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts
index e4af79926..600438fc9 100644
--- a/backend/src/services/app-connection/app-connection-types.ts
+++ b/backend/src/services/app-connection/app-connection-types.ts
@@ -316,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
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/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/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/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/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/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 d59f20bc6..57539f002 100644
--- a/backend/src/services/project/project-service.ts
+++ b/backend/src/services/project/project-service.ts
@@ -237,8 +237,8 @@ export const projectServiceFactory = ({
actorId,
actorOrgId,
actorAuthMethod,
- workspaceName,
- workspaceDescription,
+ projectName: workspaceName,
+ projectDescription: workspaceDescription,
slug: projectSlug,
kmsKeyId,
tx: trx,
@@ -254,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)]);
@@ -591,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;
@@ -684,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
@@ -1806,7 +1815,8 @@ export const projectServiceFactory = ({
limit,
type,
orderBy,
- orderDirection
+ orderDirection,
+ projectIds
}: TSearchProjectsDTO) => {
// check user belong to org
await permissionService.getOrgPermission(
@@ -1822,6 +1832,7 @@ export const projectServiceFactory = ({
offset,
name,
type,
+ projectIds,
orgId: permission.orgId,
actor: permission.type,
actorId: 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/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-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts
index f75d84eda..a31bb202d 100644
--- a/backend/src/services/secret-sync/secret-sync-queue.ts
+++ b/backend/src/services/secret-sync/secret-sync-queue.ts
@@ -484,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 = {
@@ -624,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(
@@ -744,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);
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/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/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/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/docs.json b/docs/docs.json
index 285e11a60..8b64210a0 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -1009,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"
+ ]
+ }
]
},
{
@@ -1040,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"
+ ]
+ }
]
},
{
@@ -1050,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"
+ ]
+ }
]
},
{
@@ -1060,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"
+ ]
+ }
]
},
{
@@ -1078,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"
+ ]
+ }
]
},
{
@@ -1088,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"
+ ]
+ }
]
},
{
@@ -1099,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"
+ ]
+ }
]
},
{
@@ -1113,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"
+ ]
+ }
]
},
{
@@ -1144,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"
+ ]
+ }
]
},
{
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 a3aba04e2..3a87f6da9 100644
--- a/docs/documentation/platform/identities/universal-auth.mdx
+++ b/docs/documentation/platform/identities/universal-auth.mdx
@@ -65,25 +65,30 @@ 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 more guidance on each field:
+ Here's some guidance on each field:
**Configuration Tab**
+
+ 
+
- 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**
+
+ 
+
- 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**
+
+ 
+
- 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.
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/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.

@@ -72,7 +72,7 @@ Infisical supports the use of [Service Accounts](https://developer.1password.com

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

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

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.

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.

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

@@ -95,7 +95,7 @@ Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbuck

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

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

@@ -55,7 +55,7 @@ Infisical supports the use of [API Keys](https://app.checklyhq.com/settings/user

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

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

@@ -63,7 +63,7 @@ Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/accoun

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

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

@@ -48,7 +48,7 @@ Infisical supports the use of [Access Tokens](https://fly.io/docs/security/token

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

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

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.

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

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.

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

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.

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

@@ -93,7 +93,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.

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.

- Navigate to the **App Connections** tab on the **Organization Settings** page.
+ Navigate to the **App Connections** page in the desired project.

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.

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.

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.

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.

@@ -53,7 +53,7 @@ Infisical supports the use of [Personal Access Tokens](https://docs.netlify.com/

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

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

@@ -139,7 +139,7 @@ Infisical supports the use of [API Signing Key Authentication](https://docs.orac

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

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

@@ -48,7 +48,7 @@ Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guide

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

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

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.

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.

@@ -115,7 +115,7 @@ Infisical supports the use of [API Tokens](https://docs.railway.com/guides/publi

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

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

@@ -53,7 +53,7 @@ Infisical supports the use of [Personal Access Tokens](https://supabase.com/dash

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

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

2. Add Connection
@@ -68,7 +68,7 @@ Infisical supports connecting to TeamCity using Access Tokens.

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.

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

2. Select the **Terraform Cloud Connection** option from the connection options modal.

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

2. Add Connection
@@ -52,7 +52,7 @@ Infisical supports connecting to Vercel using API Tokens.

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.

@@ -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.
+

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

@@ -50,7 +50,7 @@ Infisical supports the use of [API Tokens](https://www.zabbix.com/documentation/

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

@@ -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/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/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 8e0bf08ad..bdb39ba1b 100644
--- a/frontend/src/components/permissions/OrgPermissionCan.tsx
+++ b/frontend/src/components/permissions/OrgPermissionCan.tsx
@@ -1,8 +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 { TooltipProps } from "@app/components/v2/Tooltip/Tooltip";
-import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext";
+import { useOrgPermission } from "@app/context/OrgPermissionContext";
+import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types";
import { AccessRestrictedBanner, Tooltip } from "../v2";
@@ -14,7 +16,7 @@ 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
@@ -22,9 +24,18 @@ type Props = {
allowedLabel?: string;
renderGuardBanner?: boolean;
tooltipProps?: Omit;
-} & BoundCanProps;
+ 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,
@@ -41,9 +52,7 @@ 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 (
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/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/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/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/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts
index 4a569be06..8e4f7514c 100644
--- a/frontend/src/context/OrgPermissionContext/types.ts
+++ b/frontend/src/context/OrgPermissionContext/types.ts
@@ -1,4 +1,4 @@
-import { MongoAbility } from "@casl/ability";
+import { ForcedSubject, MongoAbility } from "@casl/ability";
export enum OrgPermissionActions {
Read = "read",
@@ -32,6 +32,7 @@ export enum OrgPermissionMachineIdentityAuthTemplateActions {
export enum OrgPermissionSubjects {
Workspace = "workspace",
+ Project = "project",
Role = "role",
Member = "member",
Settings = "settings",
@@ -109,6 +110,7 @@ export type AppConnectionSubjectFields = {
export type OrgPermissionSet =
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
+ | [OrgPermissionActions.Create, OrgPermissionSubjects.Project]
| [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions, OrgPermissionSubjects.Role]
| [OrgPermissionActions, OrgPermissionSubjects.Member]
@@ -126,7 +128,6 @@ export type OrgPermissionSet =
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
- | [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [
@@ -134,14 +135,13 @@ export type OrgPermissionSet =
OrgPermissionSubjects.MachineIdentityAuthTemplate
]
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway]
- | [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
-// TODO(scott): add back once org UI refactored
-// | [
-// OrgPermissionAppConnectionActions,
-// (
-// | OrgPermissionSubjects.AppConnections
-// | (ForcedSubject & 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/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/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/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/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx
index 9b9280cf0..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",
diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx
index 90cb5e237..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",
@@ -59,8 +59,8 @@ export enum EventType {
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",
@@ -72,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",
@@ -141,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 a0b1ff50a..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;
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/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/types.ts b/frontend/src/hooks/api/identities/types.ts
index a2fa17acc..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;
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