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/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts
index 10c84cc4a..88b4480bc 100644
--- a/backend/src/ee/services/audit-log/audit-log-types.ts
+++ b/backend/src/ee/services/audit-log/audit-log-types.ts
@@ -392,6 +392,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",
@@ -2781,14 +2783,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 {
@@ -3697,6 +3716,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/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-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 d61694d8f..97aa17874 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -2185,11 +2185,15 @@ export const CertificateAuthorities = {
};
export const AppConnections = {
+ LIST: (app?: AppConnection) => ({
+ projectId: `The ID of the project to list ${app ? APP_CONNECTION_NAME_MAP[app] : "App"} Connections from.`
+ }),
GET_BY_ID: (app: AppConnection) => ({
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
}),
GET_BY_NAME: (app: AppConnection) => ({
- connectionName: `The name of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
+ connectionName: `The name of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`,
+ projectId: `The project ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection is associated with. Leave unspecified to get organization-level connections.`
}),
CREATE: (app: AppConnection) => {
const appName = APP_CONNECTION_NAME_MAP[app];
@@ -2198,7 +2202,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 eccad2956..1df89cd19 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -1837,7 +1837,8 @@ export const registerRoutes = async (
gatewayService,
gatewayV2Service,
gatewayDAL,
- gatewayV2DAL
+ gatewayV2DAL,
+ projectDAL
});
const secretSyncService = secretSyncServiceFactory({
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..88e572426 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/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..bfad05213 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,19 @@ 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])
+);
+
+export const listAppConnectionOptions = (projectType?: ProjectType) => {
return [
getAwsConnectionListItem(),
getGitHubConnectionListItem(),
@@ -173,22 +189,55 @@ 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 (
+ option.app === AppConnection.AWS ||
+ option.app === AppConnection.Cloudflare ||
+ option.app === AppConnection.AzureADCS
+ );
+ 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 +249,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 +398,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 +469,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..efd3bb7ad 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,41 @@ 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 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 { permission: orgPermission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
@@ -578,28 +768,67 @@ export const appConnectionServiceFactory = ({
actor.orgId
);
- const appConnections = await appConnectionDAL.find({ app, orgId: actor.orgId });
+ const orgAppConnections = await appConnectionDAL.find({ app, orgId: actor.orgId, projectId: null });
- const availableConnections = appConnections.filter((connection) =>
+ const availableOrgConnections = orgAppConnections.filter((connection) =>
orgPermission.can(
OrgPermissionAppConnectionActions.Connect,
subject(OrgPermissionSubjects.AppConnections, { connectionId: connection.id })
)
);
- return availableConnections as Omit[];
+ const projectAppConnections = await appConnectionDAL.find({ app, projectId });
+
+ const availableProjectConnections = projectAppConnections.filter((connection) =>
+ projectPermission.can(
+ ProjectPermissionAppConnectionActions.Connect,
+ subject(ProjectPermissionSub.AppConnections, { connectionId: connection.id })
+ )
+ );
+
+ 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/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/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/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/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/secret-rotations-v2/CreateSecretRotationV2Modal.tsx b/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx
index f5b9a39b3..9666c3c52 100644
--- a/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx
+++ b/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx
@@ -1,8 +1,10 @@
-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";
@@ -24,14 +26,21 @@ type ContentProps = {
onComplete: (secretRotation: TSecretRotationV2) => void;
selectedRotation: SecretRotation | null;
setSelectedRotation: (selectedRotation: SecretRotation | null) => void;
+ initialFormData?: Partial;
} & SharedProps;
-const Content = ({ setSelectedRotation, selectedRotation, ...props }: ContentProps) => {
+const Content = ({
+ setSelectedRotation,
+ selectedRotation,
+ initialFormData,
+ ...props
+}: ContentProps) => {
if (selectedRotation) {
return (
setSelectedRotation(null)}
type={selectedRotation}
+ initialFormData={initialFormData}
{...props}
/>
);
@@ -42,12 +51,56 @@ 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]);
return (
{
- if (!open) setSelectedRotation(null);
+ if (!open) {
+ setSelectedRotation(null);
+ setInitialFormData(undefined);
+ }
onOpenChange(open);
}}
>
@@ -87,6 +140,7 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
setSelectedRotation(null);
onOpenChange(false);
}}
+ initialFormData={initialFormData}
selectedRotation={selectedRotation}
setSelectedRotation={setSelectedRotation}
{...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..4b0503648 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, useProjectPermission, useWorkspace } 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 { currentWorkspace } = useWorkspace();
+
+ const { data: availableConnections, isPending } = useListAvailableAppConnections(
+ app,
+ currentWorkspace.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={currentWorkspace.type}
+ projectId={currentWorkspace.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..62efaa8d2 100644
--- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx
+++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx
@@ -34,6 +34,7 @@ type Props = {
environment?: string;
environments?: WorkspaceEnv[];
secretRotation?: TSecretRotationV2;
+ initialFormData?: Partial;
};
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretRotationV2Form)[] }[] = [
@@ -64,7 +65,8 @@ export const SecretRotationV2Form = ({
environment: envSlug,
secretPath,
secretRotation,
- environments
+ environments,
+ initialFormData
}: Props) => {
const createSecretRotation = useCreateSecretRotationV2();
const updateSecretRotation = useUpdateSecretRotationV2();
@@ -93,7 +95,8 @@ export const SecretRotationV2Form = ({
},
environment: currentWorkspace?.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"
});
diff --git a/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx b/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx
index c95baaae5..520aa0365 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,6 +23,7 @@ type ContentProps = {
onComplete: (dataSource: TSecretScanningDataSource) => void;
selectedDataSource: SecretScanningDataSource | null;
setSelectedDataSource: (selectedDataSource: SecretScanningDataSource | null) => void;
+ initialFormData?: Partial;
};
const Content = ({ setSelectedDataSource, selectedDataSource, ...props }: ContentProps) => {
@@ -41,6 +44,47 @@ 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]);
return (
diff --git a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConnectionField.tsx b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceConnectionField.tsx
index 3f995e1d0..6b252e55c 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, useProjectPermission, useWorkspace } 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 { currentWorkspace } = useWorkspace();
+
+ const { data: availableConnections, isPending } = useListAvailableAppConnections(
+ app,
+ currentWorkspace.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={currentWorkspace.type}
+ projectId={currentWorkspace.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..49568dfec 100644
--- a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceForm.tsx
+++ b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceForm.tsx
@@ -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,7 +37,13 @@ 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();
@@ -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"
});
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..082054ad2 100644
--- a/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx
+++ b/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx
@@ -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 { 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"
});
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx
index 94e587709..ffae4ed88 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, useProjectPermission, useWorkspace } 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 { currentWorkspace } = useWorkspace();
+
+ const { data: availableConnections, isPending } = useListAvailableAppConnections(
+ app,
+ currentWorkspace.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={currentWorkspace.type}
+ projectId={currentWorkspace.id}
+ app={app}
+ onComplete={(connection) => {
+ if (connection) {
+ setValue("connection", connection);
+ }
+ }}
+ />
>
);
};
diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts
index 4a569be06..60f069c1d 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",
@@ -126,7 +126,6 @@ export type OrgPermissionSet =
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
- | [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [
@@ -134,14 +133,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/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/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/hooks/api/appConnections/mutations.tsx b/frontend/src/hooks/api/appConnections/mutations.tsx
index bb8831342..9679df466 100644
--- a/frontend/src/hooks/api/appConnections/mutations.tsx
+++ b/frontend/src/hooks/api/appConnections/mutations.tsx
@@ -6,6 +6,7 @@ import {
TAppConnectionResponse,
TCreateAppConnectionDTO,
TDeleteAppConnectionDTO,
+ TMigrateAppConnectionDTO,
TUpdateAppConnectionDTO
} from "@app/hooks/api/appConnections/types";
@@ -20,7 +21,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 +39,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 +55,28 @@ 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) });
+ }
+ });
+};
+
+export const useMigrateAppConnection = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ connectionId, app }: TMigrateAppConnectionDTO) => {
+ const { data } = await apiRequest.post(
+ `/api/v1/app-connections/${app}/${connectionId}/migrate`
+ );
+
+ return data;
+ },
+ 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..48c084a4e 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/workspace/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..3a5147b92 100644
--- a/frontend/src/hooks/api/appConnections/types/index.ts
+++ b/frontend/src/hooks/api/appConnections/types/index.ts
@@ -1,3 +1,5 @@
+import { ProjectType } from "@app/hooks/api/workspace/types";
+
import { AppConnection } from "../enums";
import { TOnePassConnection } from "./1password-connection";
import { TAppConnectionOption } from "./app-options";
@@ -116,10 +118,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 +136,7 @@ export type TCreateAppConnectionDTO = Pick<
| "description"
| "isPlatformManagedCredentials"
| "gatewayId"
+ | "projectId"
>;
export type TUpdateAppConnectionDTO = Partial<
@@ -150,43 +154,64 @@ 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;
+// };
+
+export type TMigrateAppConnectionDTO = {
+ app: AppConnection;
+ connectionId: string;
+};
+
+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..3a90ca12b 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/workspace/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/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx
index 186a8e539..1166da805 100644
--- a/frontend/src/hooks/api/auditLogs/constants.tsx
+++ b/frontend/src/hooks/api/auditLogs/constants.tsx
@@ -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 4fe8948dd..3ea573bdb 100644
--- a/frontend/src/hooks/api/auditLogs/enums.tsx
+++ b/frontend/src/hooks/api/auditLogs/enums.tsx
@@ -140,6 +140,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/layouts/PkiManagerLayout/PkiManagerLayout.tsx b/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx
index da9b5acdf..4e17e4b17 100644
--- a/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx
+++ b/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx
@@ -7,6 +7,7 @@ import {
faFileLines,
faHome,
faMobile,
+ faPlug,
faSitemap,
faStamp,
faUsers
@@ -130,6 +131,23 @@ export const PkiManagerLayout = () => {
)}
+
+ {({ isActive }) => (
+
+ )}
+
{
)}
+
+ {({ isActive }) => (
+
+ )}
+
{
)}
+
+ {({ isActive }) => (
+
+ )}
+
{
}, [popUp?.ca?.isOpen, popUp?.ca?.data, reset, ca]);
const { data: availableRoute53Connections, isPending: isRoute53Pending } =
- useListAvailableAppConnections(AppConnection.AWS, {
+ useListAvailableAppConnections(AppConnection.AWS, currentWorkspace.id, {
enabled: caType === CaType.ACME
});
const { data: availableCloudflareConnections, isPending: isCloudflarePending } =
- useListAvailableAppConnections(AppConnection.Cloudflare, {
+ useListAvailableAppConnections(AppConnection.Cloudflare, currentWorkspace.id, {
enabled: caType === CaType.ACME
});
const { data: availableAzureConnections, isPending: isAzurePending } =
- useListAvailableAppConnections(AppConnection.AzureADCS, {
+ useListAvailableAppConnections(AppConnection.AzureADCS, currentWorkspace.id, {
enabled: caType === CaType.AZURE_AD_CS
});
@@ -457,6 +458,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
+ components={{ Option: AppConnectionOption }}
/>
)}
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/AppConnectionsPage.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/AppConnectionsPage.tsx
index ff3f31c9e..80aefe3f2 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/AppConnectionsPage.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/AppConnectionsPage.tsx
@@ -1,24 +1,17 @@
import { Helmet } from "react-helmet";
-import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
+import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { OrgPermissionCan } from "@app/components/permissions";
-import { Button, PageHeader } from "@app/components/v2";
+import { PageHeader } from "@app/components/v2";
import {
OrgPermissionAppConnectionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
-import { usePopUp } from "@app/hooks";
-import {
- AddAppConnectionModal,
- AppConnectionsTable
-} from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
+import { AppConnectionsTable } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
export const AppConnectionsPage = withPermission(
() => {
- const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addConnection"] as const);
-
return (
@@ -30,54 +23,18 @@ export const AppConnectionsPage = withPermission(
- App Connections
-
-
-
- Docs
-
-
-
-
- {(isAllowed) => (
- }
- onClick={() => {
- handlePopUpOpen("addConnection");
- }}
- isDisabled={!isAllowed}
- className="ml-auto"
- >
- Add Connection
-
- )}
-
-
- }
- description="Create and configure connections with third-party apps for re-use across Infisical projects"
+ title="App Connections"
+ description="Manage organization App Connections"
/>
-
-
-
handlePopUpToggle("addConnection", isOpen)}
- />
+
+
+
+
+ App connections can also be created and managed independently in projects now.
+
+
+
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AddAppConnectionModal.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AddAppConnectionModal.tsx
index c74985a3f..b88b283da 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AddAppConnectionModal.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AddAppConnectionModal.tsx
@@ -3,6 +3,7 @@ import { useState } from "react";
import { Modal, ModalContent } from "@app/components/v2";
import { TAppConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { ProjectType } from "@app/hooks/api/workspace/types";
import { AppConnectionForm } from "./AppConnectionForm";
import { AppConnectionsSelect } from "./AppConnectionList";
@@ -10,29 +11,44 @@ import { AppConnectionsSelect } from "./AppConnectionList";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
+ projectId?: string;
+ projectType?: ProjectType;
+ app?: AppConnection;
+ onComplete?: (appConnection: TAppConnection) => void;
};
type ContentProps = {
onComplete: (appConnection: TAppConnection) => void;
+ projectId?: string;
+ projectType?: ProjectType;
+ app?: AppConnection;
};
-const Content = ({ onComplete }: ContentProps) => {
+const Content = ({ onComplete, projectId, projectType, app }: ContentProps) => {
const [selectedApp, setSelectedApp] = useState(null);
- if (selectedApp) {
+ if (app ?? selectedApp) {
return (
setSelectedApp(null)}
- app={selectedApp}
+ app={(app ?? selectedApp)!}
+ projectId={projectId}
/>
);
}
- return ;
+ return ;
};
-export const AddAppConnectionModal = ({ isOpen, onOpenChange }: Props) => {
+export const AddAppConnectionModal = ({
+ isOpen,
+ onOpenChange,
+ projectId,
+ projectType,
+ app,
+ onComplete
+}: Props) => {
return (
{
title="Add Connection"
subTitle="Select a third-party app to connect to."
>
- onOpenChange(false)} />
+ {
+ if (onComplete) onComplete(appConnection);
+ onOpenChange(false);
+ }}
+ />
);
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx
index f1826e5ae..f82e4107d 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx
@@ -52,12 +52,15 @@ type FormProps = {
onComplete: (appConnection: TAppConnection) => void;
} & ({ appConnection: TAppConnection } | { app: AppConnection });
-type CreateFormProps = FormProps & { app: AppConnection };
+type CreateFormProps = FormProps & {
+ app: AppConnection;
+ projectId?: string;
+};
type UpdateFormProps = FormProps & {
appConnection: TAppConnection;
};
-const CreateForm = ({ app, onComplete }: CreateFormProps) => {
+const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
const createAppConnection = useCreateAppConnection();
const { name: appName } = APP_CONNECTION_MAP[app];
@@ -68,7 +71,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
>
) => {
try {
- const connection = await createAppConnection.mutateAsync(formData);
+ const connection = await createAppConnection.mutateAsync({
+ ...formData,
+ projectId
+ });
createNotification({
text: `Successfully added ${appName} Connection`,
type: "success"
@@ -88,15 +94,15 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.AWS:
return ;
case AppConnection.GitHub:
- return ;
+ return ;
case AppConnection.GitHubRadar:
- return ;
+ return ;
case AppConnection.GCP:
return ;
case AppConnection.AzureKeyVault:
- return ;
+ return ;
case AppConnection.AzureAppConfiguration:
- return ;
+ return ;
case AppConnection.AzureADCS:
return ;
case AppConnection.Databricks:
@@ -118,9 +124,9 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.Camunda:
return ;
case AppConnection.AzureClientSecrets:
- return ;
+ return ;
case AppConnection.AzureDevOps:
- return ;
+ return ;
case AppConnection.Windmill:
return ;
case AppConnection.Auth0:
@@ -142,7 +148,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.Flyio:
return ;
case AppConnection.GitLab:
- return ;
+ return ;
case AppConnection.Cloudflare:
return ;
case AppConnection.Bitbucket:
@@ -200,16 +206,33 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.AWS:
return ;
case AppConnection.GitHub:
- return ;
+ return (
+
+ );
case AppConnection.GitHubRadar:
- return ;
+ return (
+
+ );
case AppConnection.GCP:
return ;
case AppConnection.AzureKeyVault:
- return ;
+ return (
+
+ );
case AppConnection.AzureAppConfiguration:
return (
-
+
);
case AppConnection.AzureADCS:
return ;
@@ -232,9 +255,21 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.Camunda:
return ;
case AppConnection.AzureClientSecrets:
- return ;
+ return (
+
+ );
case AppConnection.AzureDevOps:
- return ;
+ return (
+
+ );
case AppConnection.Windmill:
return ;
case AppConnection.Auth0:
@@ -256,7 +291,13 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.Flyio:
return ;
case AppConnection.GitLab:
- return ;
+ return (
+
+ );
case AppConnection.Cloudflare:
return ;
case AppConnection.Bitbucket:
@@ -278,12 +319,12 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
}
};
-type Props = { onBack?: () => void } & Pick &
+type Props = { onBack?: () => void; projectId?: string } & Pick &
(
| { app: AppConnection; appConnection?: undefined }
| { app?: undefined; appConnection: TAppConnection }
);
-export const AppConnectionForm = ({ onBack, ...props }: Props) => {
+export const AppConnectionForm = ({ onBack, projectId, ...props }: Props) => {
const { app, appConnection } = props;
return (
@@ -296,7 +337,7 @@ export const AppConnectionForm = ({ onBack, ...props }: Props) => {
{appConnection ? (
) : (
-
+
)}
);
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx
index 433abf076..3772478f0 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx
@@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
-import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
+import {
+ APP_CONNECTION_MAP,
+ getAppConnectionMethodDetails,
+ useGetAppConnectionOauthReturnUrl
+} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
AzureAppConfigurationConnectionMethod,
@@ -15,6 +19,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { AzureAppConfigurationFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -25,6 +30,7 @@ type ClientSecretForm = z.infer;
type Props = {
appConnection?: TAzureAppConfigurationConnection;
onSubmit: (formData: ClientSecretForm) => Promise;
+ projectId: string | undefined | null;
};
const baseSchema = genericAppConnectionFieldsSchema.extend({
@@ -96,7 +102,11 @@ const getDefaultValues = (appConnection?: TAzureAppConfigurationConnection): Par
return base;
};
-export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }: Props) => {
+export const AzureAppConfigurationConnectionForm = ({
+ appConnection,
+ onSubmit,
+ projectId
+}: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -110,6 +120,8 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }:
defaultValues: getDefaultValues(appConnection)
});
+ const returnUrl = useGetAppConnectionOauthReturnUrl();
+
const {
handleSubmit,
control,
@@ -128,7 +140,12 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }:
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureAppConfigurationConnectionFormData",
- JSON.stringify({ ...formData, connectionId: appConnection?.id })
+ JSON.stringify({
+ ...formData,
+ connectionId: appConnection?.id,
+ projectId,
+ returnUrl
+ } as AzureAppConfigurationFormData)
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-app-configuration`
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx
index 6f70f789e..dcfdbaa08 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx
@@ -7,7 +7,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
-import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
+import {
+ APP_CONNECTION_MAP,
+ getAppConnectionMethodDetails,
+ useGetAppConnectionOauthReturnUrl
+} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
AzureClientSecretsConnectionMethod,
@@ -16,6 +20,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { AzureClientSecretsFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -26,6 +31,7 @@ type ClientSecretForm = z.infer;
type Props = {
appConnection?: TAzureClientSecretsConnection;
onSubmit: (formData: ClientSecretForm) => Promise;
+ projectId: string | undefined | null;
};
const baseSchema = genericAppConnectionFieldsSchema.extend({
@@ -97,7 +103,7 @@ const getDefaultValues = (appConnection?: TAzureClientSecretsConnection): Partia
return base;
};
-export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Props) => {
+export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -111,6 +117,8 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
defaultValues: getDefaultValues(appConnection)
});
+ const returnUrl = useGetAppConnectionOauthReturnUrl();
+
const {
handleSubmit,
control,
@@ -129,7 +137,12 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureClientSecretsConnectionFormData",
- JSON.stringify({ ...formData, connectionId: appConnection?.id })
+ JSON.stringify({
+ ...formData,
+ connectionId: appConnection?.id,
+ projectId,
+ returnUrl
+ } as AzureClientSecretsFormData)
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://graph.microsoft.com/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureDevOpsConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureDevOpsConnectionForm.tsx
index c4511a0da..7d2a4ce7d 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureDevOpsConnectionForm.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureDevOpsConnectionForm.tsx
@@ -7,7 +7,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
-import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
+import {
+ APP_CONNECTION_MAP,
+ getAppConnectionMethodDetails,
+ useGetAppConnectionOauthReturnUrl
+} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
AzureDevOpsConnectionMethod,
@@ -16,6 +20,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { AzureDevOpsFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -65,6 +70,7 @@ type OnSubmitForm = z.infer | z.infer Promise;
+ projectId: string | undefined | null;
};
const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial => {
@@ -132,7 +138,7 @@ const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial {
+export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -146,6 +152,8 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
defaultValues: getDefaultValues(appConnection)
});
+ const returnUrl = useGetAppConnectionOauthReturnUrl();
+
const {
handleSubmit,
control,
@@ -164,7 +172,12 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureDevOpsConnectionFormData",
- JSON.stringify({ ...formData, connectionId: appConnection?.id })
+ JSON.stringify({
+ ...formData,
+ connectionId: appConnection?.id,
+ projectId,
+ returnUrl
+ } as AzureDevOpsFormData)
);
window.location.assign(
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx
index ed99fda01..8c0f00acf 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx
@@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
-import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
+import {
+ APP_CONNECTION_MAP,
+ getAppConnectionMethodDetails,
+ useGetAppConnectionOauthReturnUrl
+} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useGetAppConnectionOption } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
@@ -15,6 +19,7 @@ import {
TAzureKeyVaultConnection
} from "@app/hooks/api/appConnections/types/azure-key-vault-connection";
+import { AzureKeyVaultFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -25,6 +30,7 @@ type ClientSecretForm = z.infer;
type Props = {
appConnection?: TAzureKeyVaultConnection;
onSubmit: (formData: ClientSecretForm) => Promise;
+ projectId: string | undefined | null;
};
const baseSchema = genericAppConnectionFieldsSchema.extend({
@@ -96,7 +102,7 @@ const getDefaultValues = (appConnection?: TAzureKeyVaultConnection): Partial {
+export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -110,6 +116,8 @@ export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props)
defaultValues: getDefaultValues(appConnection)
});
+ const returnUrl = useGetAppConnectionOauthReturnUrl();
+
const {
handleSubmit,
control,
@@ -129,7 +137,12 @@ export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props)
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureKeyVaultConnectionFormData",
- JSON.stringify({ ...formData, connectionId: appConnection?.id })
+ JSON.stringify({
+ ...formData,
+ connectionId: appConnection?.id,
+ projectId,
+ returnUrl
+ } as AzureKeyVaultFormData)
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://vault.azure.net/.default%20openid%20offline_access&state=${state}<:>azure-key-vault`
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx
index 5c9a5bcd3..c69f662c6 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx
@@ -25,7 +25,11 @@ import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
-import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
+import {
+ APP_CONNECTION_MAP,
+ getAppConnectionMethodDetails,
+ useGetAppConnectionOauthReturnUrl
+} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import { gatewaysQueryKeys } from "@app/hooks/api";
import {
@@ -35,6 +39,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { GithubFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -42,6 +47,7 @@ import {
type Props = {
appConnection?: TGitHubConnection;
+ projectId: string | undefined | null;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
@@ -63,7 +69,7 @@ const formSchema = genericAppConnectionFieldsSchema.extend({
type FormData = z.infer;
-export const GitHubConnectionForm = ({ appConnection }: Props) => {
+export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -98,13 +104,21 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
const selectedMethod = watch("method");
const instanceType = watch("credentials.instanceType");
+ const returnUrl = useGetAppConnectionOauthReturnUrl();
+
const onSubmit = (formData: FormData) => {
setIsRedirecting(true);
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"githubConnectionFormData",
- JSON.stringify({ ...formData, connectionId: appConnection?.id })
+ JSON.stringify({
+ ...formData,
+ credentials: formData.credentials as TGitHubConnection["credentials"],
+ connectionId: appConnection?.id,
+ projectId,
+ returnUrl
+ } as GithubFormData)
);
const githubHost =
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubRadarConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubRadarConnectionForm.tsx
index f9c39a502..446eeb4cc 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubRadarConnectionForm.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubRadarConnectionForm.tsx
@@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
-import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
+import {
+ APP_CONNECTION_MAP,
+ getAppConnectionMethodDetails,
+ useGetAppConnectionOauthReturnUrl
+} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
GitHubRadarConnectionMethod,
@@ -15,6 +19,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { GithubRadarFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -22,6 +27,7 @@ import {
type Props = {
appConnection?: TGitHubRadarConnection;
+ projectId: string | undefined | null;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
@@ -31,7 +37,7 @@ const formSchema = genericAppConnectionFieldsSchema.extend({
type FormData = z.infer;
-export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
+export const GitHubRadarConnectionForm = ({ appConnection, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -48,6 +54,8 @@ export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
}
});
+ const returnUrl = useGetAppConnectionOauthReturnUrl();
+
const {
handleSubmit,
control,
@@ -63,7 +71,12 @@ export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"githubRadarConnectionFormData",
- JSON.stringify({ ...formData, connectionId: appConnection?.id })
+ JSON.stringify({
+ ...formData,
+ connectionId: appConnection?.id,
+ projectId,
+ returnUrl
+ } as GithubRadarFormData)
);
switch (formData.method) {
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitLabConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitLabConnectionForm.tsx
index 8cbc2c54c..ed4e022c4 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitLabConnectionForm.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitLabConnectionForm.tsx
@@ -16,7 +16,11 @@ import {
Select,
SelectItem
} from "@app/components/v2";
-import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
+import {
+ APP_CONNECTION_MAP,
+ getAppConnectionMethodDetails,
+ useGetAppConnectionOauthReturnUrl
+} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useGetAppConnectionOption } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
@@ -26,6 +30,7 @@ import {
TGitLabConnection
} from "@app/hooks/api/appConnections/types/gitlab-connection";
+import { GitLabFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -34,6 +39,7 @@ import {
type Props = {
appConnection?: TGitLabConnection;
onSubmit: (formData: FormData) => Promise;
+ projectId: string | undefined | null;
};
const formSchema = z.discriminatedUnion("method", [
@@ -72,7 +78,7 @@ const formSchema = z.discriminatedUnion("method", [
type FormData = z.infer;
-export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Props) => {
+export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -98,6 +104,8 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
} as FormData))
});
+ const returnUrl = useGetAppConnectionOauthReturnUrl();
+
const {
handleSubmit,
control,
@@ -132,8 +140,10 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
JSON.stringify({
...formData,
connectionId: appConnection?.id,
- isUpdate
- })
+ isUpdate,
+ projectId,
+ returnUrl
+ } as GitLabFormData)
);
// Redirect to Gitlab OAuth
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx
index 2087ebf63..3170ad07f 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx
@@ -9,14 +9,16 @@ import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useAppConnectionOptions } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { ProjectType } from "@app/hooks/api/workspace/types";
type Props = {
onSelect: (app: AppConnection) => void;
+ projectType?: ProjectType;
};
-export const AppConnectionsSelect = ({ onSelect }: Props) => {
+export const AppConnectionsSelect = ({ onSelect, projectType }: Props) => {
const { subscription } = useSubscription();
- const { isPending, data: appConnectionOptions } = useAppConnectionOptions();
+ const { isPending, data: appConnectionOptions } = useAppConnectionOptions(projectType);
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionRow.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionRow.tsx
index fb7f880c4..941a90db4 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionRow.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionRow.tsx
@@ -1,19 +1,23 @@
import { useCallback } from "react";
+import { subject } from "@casl/ability";
import {
faAsterisk,
+ faBuilding,
faCheck,
faCopy,
faEdit,
faEllipsisV,
faInfoCircle,
faServer,
+ faTable,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Link } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
-import { OrgPermissionCan } from "@app/components/permissions";
+import { VariablePermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
@@ -25,9 +29,11 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
-import { OrgPermissionSubjects } from "@app/context";
+import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
+import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
+import { getProjectBaseURL } from "@app/helpers/project";
import { useToggle } from "@app/hooks";
import { TAppConnection } from "@app/hooks/api/appConnections";
@@ -36,15 +42,18 @@ type Props = {
onDelete: (appConnection: TAppConnection) => void;
onEditCredentials: (appConnection: TAppConnection) => void;
onEditDetails: (appConnection: TAppConnection) => void;
+ isProjectView: boolean;
};
export const AppConnectionRow = ({
appConnection,
onDelete,
onEditCredentials,
- onEditDetails
+ onEditDetails,
+ isProjectView
}: Props) => {
- const { id, name, method, app, description, isPlatformManagedCredentials } = appConnection;
+ const { id, name, method, app, description, isPlatformManagedCredentials, project } =
+ appConnection;
const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -111,7 +120,38 @@ export const AppConnectionRow = ({
{methodDetails.name}
-
+ {!isProjectView && (
+
+ {project ? (
+
+
+
+ {project.name}
+
+
+ ) : (
+
+
+ Organization
+
+ )}
+ |
+ )}
{isPlatformManagedCredentials && (
@@ -143,48 +183,91 @@ export const AppConnectionRow = ({
>
Copy Connection ID
-
- {(isAllowed: boolean) => (
- }
- onClick={() => onEditDetails(appConnection)}
+ {(isProjectView || !project) && (
+ <>
+
- Edit Details
-
- )}
-
-
- {(isAllowed: boolean) => (
- }
- onClick={() => onEditCredentials(appConnection)}
+ {(isAllowed: boolean) => (
+ }
+ onClick={() => onEditDetails(appConnection)}
+ >
+ Edit Details
+
+ )}
+
+
- {isPlatformManagedCredentials ? "View" : "Edit"} Credentials
-
- )}
-
-
- {(isAllowed: boolean) => (
- }
- onClick={() => onDelete(appConnection)}
+ {(isAllowed: boolean) => (
+ }
+ onClick={() => onEditCredentials(appConnection)}
+ >
+ {isPlatformManagedCredentials ? "View" : "Edit"} Credentials
+
+ )}
+
+
- Delete Connection
-
- )}
-
+ {(isAllowed: boolean) => (
+ }
+ onClick={() => onDelete(appConnection)}
+ >
+ Delete Connection
+
+ )}
+
+ >
+ )}
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionsTable.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionsTable.tsx
index bc448663a..18ae6034c 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionsTable.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionsTable.tsx
@@ -2,16 +2,21 @@ import { useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
+ faArrowUpRightFromSquare,
+ faBookOpen,
faCheckCircle,
faFilter,
faMagnifyingGlass,
faPlug,
+ faPlus,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
+import { VariablePermissionCan } from "@app/components/permissions";
import {
+ Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -29,6 +34,9 @@ import {
THead,
Tr
} from "@app/components/v2";
+import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context";
+import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
+import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
getUserTablePreference,
@@ -39,7 +47,9 @@ import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { TAppConnection, useListAppConnections } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { OrderByDirection } from "@app/hooks/api/generic/types";
+import { ProjectType } from "@app/hooks/api/workspace/types";
+import { AddAppConnectionModal } from "./AddAppConnectionModal";
import { AppConnectionRow } from "./AppConnectionRow";
import { DeleteAppConnectionModal } from "./DeleteAppConnectionModal";
import { EditAppConnectionCredentialsModal } from "./EditAppConnectionCredentialsModal";
@@ -48,22 +58,45 @@ import { EditAppConnectionDetailsModal } from "./EditAppConnectionDetailsModal";
enum AppConnectionsOrderBy {
App = "app",
Name = "name",
- Method = "method"
+ Method = "method",
+ ManagedBy = "managed-by"
}
type AppConnectionFilters = {
apps: AppConnection[];
};
-export const AppConnectionsTable = () => {
- const { isPending, data: appConnections = [] } = useListAppConnections();
+enum View {
+ All = "all",
+ Scope = "scope"
+}
+
+const APP_CONNECTION_VIEW_STORAGE_KEY = "app-connection-view";
+
+type Props = {
+ projectId?: string;
+ projectType?: ProjectType;
+};
+
+export const AppConnectionsTable = ({ projectId, projectType }: Props) => {
+ const isProjectView = Boolean(projectId);
+ const { isPending, data: appConnections = [] } = useListAppConnections(projectId);
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
+ "addConnection",
"deleteConnection",
"editCredentials",
"editDetails"
] as const);
+ const [view, setView] = useState (() => {
+ const storedView = localStorage.getItem(APP_CONNECTION_VIEW_STORAGE_KEY) as View | null;
+
+ if (storedView && Object.values(View).includes(storedView)) return storedView;
+
+ return View.Scope;
+ });
+
const [filters, setFilters] = useState({
apps: []
});
@@ -96,6 +129,10 @@ export const AppConnectionsTable = () => {
.filter((appConnection) => {
const { app, method, name } = appConnection;
+ if (view === View.Scope && !isProjectView && appConnection.projectId) {
+ return false;
+ }
+
if (filters.apps.length && !filters.apps.includes(app)) return false;
const searchValue = search.trim().toLowerCase();
@@ -121,6 +158,13 @@ export const AppConnectionsTable = () => {
.localeCompare(
getAppConnectionMethodDetails(connectionTwo.method).name.toLowerCase()
);
+ case AppConnectionsOrderBy.ManagedBy:
+ if (!connectionOne.project) return 1;
+ if (!connectionTwo.project) return -1;
+
+ return connectionOne.project.name
+ .toLowerCase()
+ .localeCompare(connectionTwo.project.name.toLowerCase());
case AppConnectionsOrderBy.App:
default:
return APP_CONNECTION_MAP[connectionOne.app].name
@@ -128,7 +172,7 @@ export const AppConnectionsTable = () => {
.localeCompare(APP_CONNECTION_MAP[connectionTwo.app].name.toLowerCase());
}
}),
- [appConnections, orderDirection, search, orderBy, filters]
+ [appConnections, orderDirection, search, orderBy, filters, view]
);
useResetPageHelper({
@@ -165,8 +209,87 @@ export const AppConnectionsTable = () => {
handlePopUpOpen("editDetails", appConnection);
return (
-
+
+
+
+
+
+ Create and configure connections with third-party apps for re-use across your project
+ {isProjectView ? "" : "s"}.
+
+
+
+ {(isAllowed) => (
+ }
+ onClick={() => handlePopUpOpen("addConnection")}
+ isDisabled={!isAllowed}
+ >
+ Add Connection
+
+ )}
+
+
+ {!isProjectView && (
+
+
+
+
+ )}
setSearch(e.target.value)}
@@ -269,7 +392,21 @@ export const AppConnectionsTable = () => {
-
+ {!isProjectView && (
+
+
+ Managed By
+ handleSort(AppConnectionsOrderBy.ManagedBy)}
+ >
+
+
+
+ |
+ )}
|
@@ -284,6 +421,7 @@ export const AppConnectionsTable = () => {
onDelete={handleDelete}
onEditCredentials={handleEditCredentials}
onEditDetails={handleEditDetails}
+ isProjectView={isProjectView}
/>
))}
@@ -323,6 +461,12 @@ export const AppConnectionsTable = () => {
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
appConnection={popUp.editDetails.data}
/>
+ handlePopUpToggle("addConnection", isOpen)}
+ projectId={projectId}
+ projectType={projectType}
+ />
);
};
diff --git a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx
index 2798a7121..f5f6d2c88 100644
--- a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx
+++ b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx
@@ -12,73 +12,14 @@ import {
AzureKeyVaultConnectionMethod,
GitHubConnectionMethod,
GitLabConnectionMethod,
- TAzureAppConfigurationConnection,
- TAzureClientSecretsConnection,
- TAzureDevOpsConnection,
- TAzureKeyVaultConnection,
- TGitHubConnection,
- TGitHubRadarConnection,
- TGitLabConnection,
+ TAppConnection,
useCreateAppConnection,
useUpdateAppConnection
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { IntegrationsListPageTabs } from "@app/types/integrations";
-type BaseFormData = {
- returnUrl?: string;
- connectionId?: string;
- isUpdate?: boolean;
-};
-
-type GithubFormData = BaseFormData &
- Pick ;
-
-type GithubRadarFormData = BaseFormData &
- Pick;
-
-type GitLabFormData = BaseFormData &
- Pick;
-
-type AzureKeyVaultFormData = BaseFormData &
- Pick &
- Pick;
-
-type AzureAppConfigurationFormData = BaseFormData &
- Pick &
- Pick;
-
-type AzureClientSecretsFormData = BaseFormData &
- Pick &
- Pick;
-
-type OAuthCredentials = Extract<
- TAzureDevOpsConnection,
- { method: AzureDevOpsConnectionMethod.OAuth }
->["credentials"];
-type AccessTokenCredentials = Extract<
- TAzureDevOpsConnection,
- { method: AzureDevOpsConnectionMethod.AccessToken }
->["credentials"];
-
-type AzureDevOpsFormData = BaseFormData &
- Pick &
- (Pick | Pick);
-
-type FormDataMap = {
- [AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
- [AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
- [AppConnection.GitLab]: GitLabFormData & { app: AppConnection.GitLab };
- [AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
- [AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
- app: AppConnection.AzureAppConfiguration;
- };
- [AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & {
- app: AppConnection.AzureClientSecrets;
- };
- [AppConnection.AzureDevOps]: AzureDevOpsFormData & {
- app: AppConnection.AzureDevOps;
- };
-};
+import { FormDataMap } from "./OauthCallbackPage.types";
const formDataStorageFieldMap: Partial> = {
[AppConnection.GitHub]: "githubConnectionFormData",
@@ -148,11 +89,14 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitLab);
- const { connectionId, name, description, returnUrl, isUpdate, credentials } = formData;
+ const { connectionId, name, description, returnUrl, isUpdate, projectId, credentials } =
+ formData;
+
+ let connection: TAppConnection;
try {
if (isUpdate && connectionId) {
- await updateAppConnection.mutateAsync({
+ connection = await updateAppConnection.mutateAsync({
app: AppConnection.GitLab,
connectionId,
credentials: {
@@ -161,10 +105,11 @@ export const OAuthCallbackPage = () => {
}
});
} else {
- await createAppConnection.mutateAsync({
+ connection = await createAppConnection.mutateAsync({
app: AppConnection.GitLab,
name,
description,
+ projectId,
method: GitLabConnectionMethod.OAuth,
credentials: {
code: code as string,
@@ -173,14 +118,12 @@ export const OAuthCallbackPage = () => {
});
}
- navigate({
- to: returnUrl ?? "/organization/app-connections"
- });
-
return {
connectionId,
returnUrl,
- appConnectionName: formData.app
+ appConnectionName: formData.app,
+ projectId,
+ connection
};
} catch (err: any) {
createNotification({
@@ -189,7 +132,10 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
- to: returnUrl ?? "/organization/app-connections"
+ to: returnUrl,
+ params: {
+ projectId
+ }
});
return null;
}
@@ -201,11 +147,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureKeyVault);
- const { connectionId, name, description, returnUrl } = formData;
+ const { connectionId, name, description, returnUrl, projectId } = formData;
+
+ let connection: TAppConnection;
try {
if (connectionId) {
- await updateAppConnection.mutateAsync({
+ connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureKeyVault,
connectionId,
credentials: {
@@ -214,10 +162,11 @@ export const OAuthCallbackPage = () => {
}
});
} else {
- await createAppConnection.mutateAsync({
+ connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureKeyVault,
name,
description,
+ projectId,
method: AzureKeyVaultConnectionMethod.OAuth,
credentials: {
tenantId: formData.tenantId,
@@ -232,14 +181,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
- to: returnUrl ?? "/organization/app-connections"
+ to: returnUrl,
+ params: {
+ projectId
+ }
});
+ return null;
}
return {
connectionId,
returnUrl,
- appConnectionName: formData.app
+ appConnectionName: formData.app,
+ projectId,
+ connection
};
}, []);
@@ -249,11 +204,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureAppConfiguration);
- const { connectionId, name, description, returnUrl } = formData;
+ const { connectionId, name, description, returnUrl, projectId } = formData;
+
+ let connection: TAppConnection;
try {
if (connectionId) {
- await updateAppConnection.mutateAsync({
+ connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureAppConfiguration,
connectionId,
credentials: {
@@ -262,10 +219,11 @@ export const OAuthCallbackPage = () => {
}
});
} else {
- await createAppConnection.mutateAsync({
+ connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureAppConfiguration,
name,
description,
+ projectId,
method: AzureAppConfigurationConnectionMethod.OAuth,
credentials: {
code: code as string,
@@ -280,14 +238,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
- to: returnUrl ?? "/organization/app-connections"
+ to: returnUrl,
+ params: {
+ projectId
+ }
});
+ return null;
}
return {
connectionId,
returnUrl,
- appConnectionName: formData.app
+ appConnectionName: formData.app,
+ projectId,
+ connection
};
}, []);
@@ -297,11 +261,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureClientSecrets);
- const { connectionId, name, description, returnUrl } = formData;
+ const { connectionId, name, description, returnUrl, projectId } = formData;
+
+ let connection: TAppConnection;
try {
if (connectionId) {
- await updateAppConnection.mutateAsync({
+ connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureClientSecrets,
connectionId,
credentials: {
@@ -310,11 +276,12 @@ export const OAuthCallbackPage = () => {
}
});
} else {
- await createAppConnection.mutateAsync({
+ connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureClientSecrets,
name,
description,
method: AzureClientSecretsConnectionMethod.OAuth,
+ projectId,
credentials: {
code: code as string,
tenantId: formData.tenantId
@@ -328,14 +295,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
- to: returnUrl ?? "/organization/app-connections"
+ to: returnUrl,
+ params: {
+ projectId
+ }
});
+ return null;
}
return {
connectionId,
returnUrl,
- appConnectionName: formData.app
+ appConnectionName: formData.app,
+ projectId,
+ connection
};
}, []);
@@ -345,7 +318,9 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureDevOps);
- const { connectionId, name, description, returnUrl } = formData;
+ const { connectionId, name, description, returnUrl, projectId } = formData;
+
+ let connection: TAppConnection;
try {
if (!("tenantId" in formData)) {
@@ -353,7 +328,7 @@ export const OAuthCallbackPage = () => {
}
if (connectionId) {
- await updateAppConnection.mutateAsync({
+ connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureDevOps,
connectionId,
credentials: {
@@ -363,11 +338,12 @@ export const OAuthCallbackPage = () => {
}
});
} else {
- await createAppConnection.mutateAsync({
+ connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureDevOps,
name,
description,
method: AzureDevOpsConnectionMethod.OAuth,
+ projectId,
credentials: {
code: code as string,
tenantId: formData.tenantId as string,
@@ -382,14 +358,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
- to: returnUrl ?? "/organization/app-connections"
+ to: returnUrl,
+ params: {
+ projectId
+ }
});
+ return null;
}
return {
connectionId,
returnUrl,
- appConnectionName: formData.app
+ appConnectionName: formData.app,
+ projectId,
+ connection
};
}, []);
@@ -399,11 +381,14 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitHub);
- const { connectionId, name, description, returnUrl, gatewayId, credentials } = formData;
+ const { connectionId, name, description, returnUrl, gatewayId, credentials, projectId } =
+ formData;
+
+ let connection: TAppConnection;
try {
if (connectionId) {
- await updateAppConnection.mutateAsync({
+ connection = await updateAppConnection.mutateAsync({
app: AppConnection.GitHub,
...(installationId
? {
@@ -427,10 +412,11 @@ export const OAuthCallbackPage = () => {
})
});
} else {
- await createAppConnection.mutateAsync({
+ connection = await createAppConnection.mutateAsync({
app: AppConnection.GitHub,
name,
description,
+ projectId,
...(installationId
? {
method: GitHubConnectionMethod.App,
@@ -460,14 +446,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
- to: returnUrl ?? "/organization/app-connections"
+ to: returnUrl,
+ params: {
+ projectId
+ }
});
+ return null;
}
return {
connectionId,
returnUrl,
- appConnectionName: formData.app
+ appConnectionName: formData.app,
+ projectId,
+ connection
};
}, []);
@@ -477,11 +469,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitHubRadar);
- const { connectionId, name, description, returnUrl } = formData;
+ const { connectionId, name, description, returnUrl, projectId } = formData;
+
+ let connection: TAppConnection;
try {
if (connectionId) {
- await updateAppConnection.mutateAsync({
+ connection = await updateAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
connectionId,
credentials: {
@@ -490,11 +484,12 @@ export const OAuthCallbackPage = () => {
}
});
} else {
- await createAppConnection.mutateAsync({
+ connection = await createAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
name,
description,
method: GitHubConnectionMethod.App,
+ projectId,
credentials: {
code: code as string,
installationId: installationId as string
@@ -508,14 +503,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
- to: returnUrl ?? "/organization/app-connections"
+ to: returnUrl,
+ params: {
+ projectId
+ }
});
+ return null;
}
return {
connectionId,
returnUrl,
- appConnectionName: formData.app
+ appConnectionName: formData.app,
+ projectId,
+ connection
};
}, []);
@@ -530,8 +531,13 @@ export const OAuthCallbackPage = () => {
if (!isReady) return;
(async () => {
- let data: { connectionId?: string; returnUrl?: string; appConnectionName?: string } | null =
- null;
+ let data: {
+ returnUrl: string;
+ appConnectionName: string;
+ connectionId?: string;
+ projectId?: string;
+ connection: TAppConnection;
+ } | null = null;
if (appConnection === AppConnection.GitHub) {
data = await handleGithub();
@@ -554,16 +560,26 @@ export const OAuthCallbackPage = () => {
text: `Successfully ${data.connectionId ? "updated" : "added"} ${data.appConnectionName ? APP_CONNECTION_MAP[data.appConnectionName as AppConnection].name : ""} Connection`,
type: "success"
});
- } else {
- createNotification({
- text: "Failed to add connection",
- type: "error"
+
+ await navigate({
+ to: data.returnUrl,
+ params: {
+ projectId: data.projectId ?? undefined
+ },
+ // scott: if it's not an app connection page we need to pass connection details as it's an inline creation
+ search: data.returnUrl.includes("app-connections")
+ ? undefined
+ : {
+ connectionId: data.connection.id,
+ connectionName: data.connection.name,
+ ...(data.returnUrl.includes("integrations")
+ ? {
+ selectedTab: IntegrationsListPageTabs.SecretSyncs
+ }
+ : {})
+ }
});
}
-
- await navigate({
- to: data?.returnUrl ?? "/organization/app-connections"
- });
})();
}, [isReady]);
diff --git a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.types.ts b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.types.ts
new file mode 100644
index 000000000..6c76a60b3
--- /dev/null
+++ b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.types.ts
@@ -0,0 +1,68 @@
+import {
+ AzureDevOpsConnectionMethod,
+ TAzureAppConfigurationConnection,
+ TAzureClientSecretsConnection,
+ TAzureDevOpsConnection,
+ TAzureKeyVaultConnection,
+ TGitHubConnection,
+ TGitHubRadarConnection,
+ TGitLabConnection
+} from "@app/hooks/api/appConnections";
+import { AppConnection } from "@app/hooks/api/appConnections/enums";
+
+type BaseFormData = {
+ returnUrl: string;
+ connectionId?: string;
+ isUpdate?: boolean;
+ projectId: string;
+};
+
+export type GithubFormData = BaseFormData &
+ Pick;
+
+export type GithubRadarFormData = BaseFormData &
+ Pick;
+
+export type GitLabFormData = BaseFormData &
+ Pick;
+
+export type AzureKeyVaultFormData = BaseFormData &
+ Pick &
+ Pick;
+
+export type AzureAppConfigurationFormData = BaseFormData &
+ Pick &
+ Pick;
+
+export type AzureClientSecretsFormData = BaseFormData &
+ Pick &
+ Pick;
+
+type OAuthCredentials = Extract<
+ TAzureDevOpsConnection,
+ { method: AzureDevOpsConnectionMethod.OAuth }
+>["credentials"];
+type AccessTokenCredentials = Extract<
+ TAzureDevOpsConnection,
+ { method: AzureDevOpsConnectionMethod.AccessToken }
+>["credentials"];
+
+export type AzureDevOpsFormData = BaseFormData &
+ Pick &
+ (Pick | Pick);
+
+export type FormDataMap = {
+ [AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
+ [AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
+ [AppConnection.GitLab]: GitLabFormData & { app: AppConnection.GitLab };
+ [AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
+ [AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
+ app: AppConnection.AzureAppConfiguration;
+ };
+ [AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & {
+ app: AppConnection.AzureClientSecrets;
+ };
+ [AppConnection.AzureDevOps]: AzureDevOpsFormData & {
+ app: AppConnection.AzureDevOps;
+ };
+};
diff --git a/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx b/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx
index 729654d76..a0d440872 100644
--- a/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx
+++ b/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx
@@ -4,11 +4,11 @@ import {
faArrowUpRightFromSquare,
faBookOpen,
faCopy,
+ faDoorClosed,
faEdit,
faEllipsisV,
faInfoCircle,
faMagnifyingGlass,
- faPlug,
faSearch,
faTrash
} from "@fortawesome/free-solid-svg-icons";
@@ -243,7 +243,7 @@ export const GatewayListPage = withPermission(
? "No Gateways match search..."
: "No Gateways have been configured"
}
- icon={gateways?.length ? faSearch : faPlug}
+ icon={gateways?.length ? faSearch : faDoorClosed}
/>
)}
{
+ const { currentWorkspace } = useWorkspace();
+
+ return (
+
+
+ Infisical | App Connections
+
+
+
+
+
+ );
+ },
+ {
+ action: ProjectPermissionAppConnectionActions.Read,
+ subject: ProjectPermissionSub.AppConnections
+ }
+);
diff --git a/frontend/src/pages/project/AppConnectionsPage/route-cert-manager.tsx b/frontend/src/pages/project/AppConnectionsPage/route-cert-manager.tsx
new file mode 100644
index 000000000..8be255e73
--- /dev/null
+++ b/frontend/src/pages/project/AppConnectionsPage/route-cert-manager.tsx
@@ -0,0 +1,19 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+import { AppConnectionsPage } from "./AppConnectionsPage";
+
+export const Route = createFileRoute(
+ "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections"
+)({
+ component: AppConnectionsPage,
+ beforeLoad: ({ context }) => {
+ return {
+ breadcrumbs: [
+ ...context.breadcrumbs,
+ {
+ label: "App Connections"
+ }
+ ]
+ };
+ }
+});
diff --git a/frontend/src/pages/project/AppConnectionsPage/route-secret-manager.tsx b/frontend/src/pages/project/AppConnectionsPage/route-secret-manager.tsx
new file mode 100644
index 000000000..2767c8689
--- /dev/null
+++ b/frontend/src/pages/project/AppConnectionsPage/route-secret-manager.tsx
@@ -0,0 +1,19 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+import { AppConnectionsPage } from "./AppConnectionsPage";
+
+export const Route = createFileRoute(
+ "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections"
+)({
+ component: AppConnectionsPage,
+ beforeLoad: ({ context }) => {
+ return {
+ breadcrumbs: [
+ ...context.breadcrumbs,
+ {
+ label: "App Connections"
+ }
+ ]
+ };
+ }
+});
diff --git a/frontend/src/pages/project/AppConnectionsPage/route-secret-scanning.tsx b/frontend/src/pages/project/AppConnectionsPage/route-secret-scanning.tsx
new file mode 100644
index 000000000..724926495
--- /dev/null
+++ b/frontend/src/pages/project/AppConnectionsPage/route-secret-scanning.tsx
@@ -0,0 +1,19 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+import { AppConnectionsPage } from "./AppConnectionsPage";
+
+export const Route = createFileRoute(
+ "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections"
+)({
+ component: AppConnectionsPage,
+ beforeLoad: ({ context }) => {
+ return {
+ breadcrumbs: [
+ ...context.breadcrumbs,
+ {
+ label: "App Connections"
+ }
+ ]
+ };
+ }
+});
diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/AppConnectionPermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/AppConnectionPermissionConditions.tsx
new file mode 100644
index 000000000..2991d895c
--- /dev/null
+++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/AppConnectionPermissionConditions.tsx
@@ -0,0 +1,19 @@
+import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
+
+import { ConditionsFields } from "./ConditionsFields";
+
+type Props = {
+ position?: number;
+ isDisabled?: boolean;
+};
+
+export const AppConnectionPermissionConditions = ({ position = 0, isDisabled }: Props) => {
+ return (
+
+ );
+};
diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx
index a39ae8d64..f6c44b386 100644
--- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx
+++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx
@@ -23,6 +23,7 @@ export const renderOperatorSelectItems = (type: string) => {
case "secretTags":
return Contains;
case "identityId":
+ case "connectionId":
return (
<>
Equal
diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx
index 856275374..f8032ac37 100644
--- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx
+++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx
@@ -12,6 +12,7 @@ import {
} from "@app/context";
import {
PermissionConditionOperators,
+ ProjectPermissionAppConnectionActions,
ProjectPermissionAuditLogsActions,
ProjectPermissionCommitsActions,
ProjectPermissionDynamicSecretActions,
@@ -134,6 +135,14 @@ const SecretScanningConfigPolicyActionSchema = z.object({
[ProjectPermissionSecretScanningConfigActions.Update]: z.boolean().optional()
});
+const AppConnectionPolicyActionSchema = z.object({
+ [ProjectPermissionAppConnectionActions.Create]: z.boolean().optional(),
+ [ProjectPermissionAppConnectionActions.Read]: z.boolean().optional(),
+ [ProjectPermissionAppConnectionActions.Edit]: z.boolean().optional(),
+ [ProjectPermissionAppConnectionActions.Delete]: z.boolean().optional(),
+ [ProjectPermissionAppConnectionActions.Connect]: z.boolean().optional()
+});
+
const KmipPolicyActionSchema = z.object({
[ProjectPermissionKmipActions.ReadClients]: z.boolean().optional(),
[ProjectPermissionKmipActions.CreateClients]: z.boolean().optional(),
@@ -311,6 +320,12 @@ export const projectRoleFormSchema = z.object({
})
.array()
.default([]),
+ [ProjectPermissionSub.AppConnections]: AppConnectionPolicyActionSchema.extend({
+ inverted: z.boolean().optional(),
+ conditions: ConditionSchema
+ })
+ .array()
+ .default([]),
[ProjectPermissionSub.Commits]: CommitPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Member]: MemberPolicyActionSchema.array().default([]),
@@ -393,7 +408,8 @@ type TConditionalFields =
| ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.Identity
| ProjectPermissionSub.SecretSyncs
- | ProjectPermissionSub.SecretEvents;
+ | ProjectPermissionSub.SecretEvents
+ | ProjectPermissionSub.AppConnections;
export const isConditionalSubjects = (
subject: ProjectPermissionSub
@@ -408,7 +424,8 @@ export const isConditionalSubjects = (
subject === ProjectPermissionSub.PkiSubscribers ||
subject === ProjectPermissionSub.CertificateTemplates ||
subject === ProjectPermissionSub.SecretSyncs ||
- subject === ProjectPermissionSub.SecretEvents;
+ subject === ProjectPermissionSub.SecretEvents ||
+ subject === ProjectPermissionSub.AppConnections;
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
const formConditions: z.infer = [];
@@ -515,7 +532,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshHostGroups,
ProjectPermissionSub.SecretSyncs,
- ProjectPermissionSub.SecretEvents
+ ProjectPermissionSub.SecretEvents,
+ ProjectPermissionSub.AppConnections
].includes(subject)
) {
// from above statement we are sure it won't be undefined
@@ -654,6 +672,27 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
return;
}
+ if (subject === ProjectPermissionSub.AppConnections) {
+ const canCreate = action.includes(ProjectPermissionAppConnectionActions.Create);
+ const canRead = action.includes(ProjectPermissionAppConnectionActions.Read);
+ const canEdit = action.includes(ProjectPermissionAppConnectionActions.Edit);
+ const canDelete = action.includes(ProjectPermissionAppConnectionActions.Delete);
+ const canConnect = action.includes(ProjectPermissionAppConnectionActions.Connect);
+
+ // from above statement we are sure it won't be undefined
+ formVal[subject]!.push({
+ [ProjectPermissionAppConnectionActions.Read]: canRead,
+ [ProjectPermissionAppConnectionActions.Create]: canCreate,
+ [ProjectPermissionAppConnectionActions.Edit]: canEdit,
+ [ProjectPermissionAppConnectionActions.Delete]: canDelete,
+ [ProjectPermissionAppConnectionActions.Connect]: canConnect,
+ conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
+ inverted
+ });
+
+ return;
+ }
+
// for other subjects
const canRead = action.includes(ProjectPermissionActions.Read);
const canEdit = action.includes(ProjectPermissionActions.Edit);
@@ -1597,6 +1636,31 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
value: ProjectPermissionSecretEventActions.SubscribeImportMutations
}
]
+ },
+ [ProjectPermissionSub.AppConnections]: {
+ title: "App Connections",
+ actions: [
+ {
+ label: "Read",
+ value: ProjectPermissionAppConnectionActions.Read
+ },
+ {
+ label: "Create",
+ value: ProjectPermissionAppConnectionActions.Create
+ },
+ {
+ label: "Update",
+ value: ProjectPermissionAppConnectionActions.Edit
+ },
+ {
+ label: "Delete",
+ value: ProjectPermissionAppConnectionActions.Delete
+ },
+ {
+ label: "Connect",
+ value: ProjectPermissionAppConnectionActions.Connect
+ }
+ ]
}
};
@@ -1669,7 +1733,8 @@ export const ProjectTypePermissionSubjects: Record<
...KmsPermissionSubjects(),
...CertificateManagerPermissionSubjects(),
...SshPermissionSubjects(),
- ...SecretScanningSubject()
+ ...SecretScanningSubject(),
+ [ProjectPermissionSub.AppConnections]: true
},
[ProjectType.KMS]: {
...SharedPermissionSubjects,
@@ -1677,7 +1742,8 @@ export const ProjectTypePermissionSubjects: Record<
...SecretsManagerPermissionSubjects(),
...CertificateManagerPermissionSubjects(),
...SshPermissionSubjects(),
- ...SecretScanningSubject()
+ ...SecretScanningSubject(),
+ [ProjectPermissionSub.AppConnections]: false
},
[ProjectType.CertificateManager]: {
...SharedPermissionSubjects,
@@ -1685,7 +1751,8 @@ export const ProjectTypePermissionSubjects: Record<
...KmsPermissionSubjects(),
...SecretsManagerPermissionSubjects(),
...SshPermissionSubjects(),
- ...SecretScanningSubject()
+ ...SecretScanningSubject(),
+ [ProjectPermissionSub.AppConnections]: true
},
[ProjectType.SSH]: {
...SharedPermissionSubjects,
@@ -1693,7 +1760,8 @@ export const ProjectTypePermissionSubjects: Record<
...CertificateManagerPermissionSubjects(),
...KmsPermissionSubjects(),
...SecretsManagerPermissionSubjects(),
- ...SecretScanningSubject()
+ ...SecretScanningSubject(),
+ [ProjectPermissionSub.AppConnections]: false
},
[ProjectType.SecretScanning]: {
...SharedPermissionSubjects,
@@ -1701,7 +1769,8 @@ export const ProjectTypePermissionSubjects: Record<
...SshPermissionSubjects(),
...CertificateManagerPermissionSubjects(),
...KmsPermissionSubjects(),
- ...SecretsManagerPermissionSubjects()
+ ...SecretsManagerPermissionSubjects(),
+ [ProjectPermissionSub.AppConnections]: true
}
};
diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx
index f50446a8e..f11756dc9 100644
--- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx
+++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx
@@ -16,6 +16,7 @@ import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { AddPoliciesButton } from "./AddPoliciesButton";
+import { AppConnectionPermissionConditions } from "./AppConnectionPermissionConditions";
import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions";
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
@@ -77,6 +78,10 @@ export const renderConditionalComponents = (
return ;
}
+ if (subject === ProjectPermissionSub.AppConnections) {
+ return ;
+ }
+
return ;
}
diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncsTab.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncsTab.tsx
index 20a3c74df..036fcf03f 100644
--- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncsTab.tsx
+++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncsTab.tsx
@@ -1,10 +1,11 @@
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { CreateSecretSyncModal } from "@app/components/secret-syncs";
+import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Button, Spinner } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub, useWorkspace } from "@app/context";
@@ -16,8 +17,9 @@ import { SecretSyncsTable } from "./SecretSyncTable";
export const SecretSyncsTab = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addSync"] as const);
+ const [initialSyncFormData, setInitialSyncFormData] = useState>();
- const { addSync, ...search } = useSearch({
+ const { addSync, connectionId, connectionName, ...search } = useSearch({
from: ROUTE_PATHS.SecretManager.IntegrationsListPage.id
});
@@ -38,6 +40,38 @@ export const SecretSyncsTab = () => {
});
}, [addSync]);
+ useEffect(() => {
+ if (connectionId && connectionName) {
+ const storedFormData = localStorage.getItem("secretSyncFormData");
+
+ if (!storedFormData) return;
+
+ let form: Partial = {};
+ try {
+ form = JSON.parse(storedFormData) as TSecretSyncForm;
+ } catch {
+ return;
+ } finally {
+ localStorage.removeItem("secretSyncFormData");
+ }
+
+ handlePopUpOpen("addSync", form.destination);
+
+ setInitialSyncFormData({
+ ...form,
+ connection: { id: connectionId, name: connectionName }
+ });
+
+ navigate({
+ to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path,
+ params: {
+ projectId: currentWorkspace.id
+ },
+ search
+ });
+ }
+ }, [connectionId, connectionName]);
+
const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs(
currentWorkspace.id,
{
@@ -100,7 +134,11 @@ export const SecretSyncsTab = () => {
handlePopUpToggle("addSync", isOpen)}
+ initialFormData={initialSyncFormData}
+ onOpenChange={(isOpen) => {
+ if (!isOpen) setInitialSyncFormData(undefined);
+ handlePopUpToggle("addSync", isOpen);
+ }}
/>
>
);
diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx
index b7f8236da..fa25fbe20 100644
--- a/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx
+++ b/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx
@@ -17,7 +17,9 @@ import { IntegrationsListPage } from "./IntegrationsListPage";
const IntegrationsListPageQuerySchema = z.object({
selectedTab: z.nativeEnum(IntegrationsListPageTabs).optional(),
- addSync: z.nativeEnum(SecretSync).optional()
+ addSync: z.nativeEnum(SecretSync).optional(),
+ connectionId: z.string().optional(),
+ connectionName: z.string().optional()
});
export const Route = createFileRoute(
diff --git a/frontend/src/pages/secret-manager/OverviewPage/route.tsx b/frontend/src/pages/secret-manager/OverviewPage/route.tsx
index 968286e13..2be6c6f25 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/route.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/route.tsx
@@ -6,7 +6,9 @@ import { OverviewPage } from "./OverviewPage";
const SecretOverviewPageQuerySchema = z.object({
search: z.string().catch(""),
- secretPath: z.string().catch("/")
+ secretPath: z.string().catch("/"),
+ connectionId: z.string().optional(),
+ connectionName: z.string().optional()
});
export const Route = createFileRoute(
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx
index 09970f698..0700b3322 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx
@@ -10,7 +10,9 @@ import { SecretDashboardPage } from "./SecretDashboardPage";
const SecretDashboardPageQueryParamsSchema = z.object({
secretPath: z.string().catch("/"),
search: z.string().catch(""),
- tags: z.string().catch("")
+ tags: z.string().catch(""),
+ connectionId: z.string().optional(),
+ connectionName: z.string().optional()
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug"
diff --git a/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/route.tsx b/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/route.tsx
index b13c99db7..87b2b43fb 100644
--- a/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/route.tsx
+++ b/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/route.tsx
@@ -1,11 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
+import { zodValidator } from "@tanstack/zod-adapter";
+import { z } from "zod";
import { SecretScanningDataSourcesPage } from "./SecretScanningDataSourcesPage";
+const SecretScanningDataSourcesPageQueryParamsSchema = z.object({
+ connectionId: z.string().optional(),
+ connectionName: z.string().optional()
+});
+
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/"
)({
component: SecretScanningDataSourcesPage,
+ validateSearch: zodValidator(SecretScanningDataSourcesPageQueryParamsSchema),
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts
index 1f62ba7b9..4d01ff47d 100644
--- a/frontend/src/routeTree.gen.ts
+++ b/frontend/src/routeTree.gen.ts
@@ -82,12 +82,15 @@ import { Route as organizationSettingsPageOauthCallbackPageRouteImport } from '.
import { Route as projectAuditLogsPageRouteSshImport } from './pages/project/AuditLogsPage/route-ssh'
import { Route as projectAccessControlPageRouteSshImport } from './pages/project/AccessControlPage/route-ssh'
import { Route as projectAuditLogsPageRouteSecretScanningImport } from './pages/project/AuditLogsPage/route-secret-scanning'
+import { Route as projectAppConnectionsPageRouteSecretScanningImport } from './pages/project/AppConnectionsPage/route-secret-scanning'
import { Route as projectAccessControlPageRouteSecretScanningImport } from './pages/project/AccessControlPage/route-secret-scanning'
import { Route as projectAuditLogsPageRouteSecretManagerImport } from './pages/project/AuditLogsPage/route-secret-manager'
+import { Route as projectAppConnectionsPageRouteSecretManagerImport } from './pages/project/AppConnectionsPage/route-secret-manager'
import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager'
import { Route as projectAuditLogsPageRouteKmsImport } from './pages/project/AuditLogsPage/route-kms'
import { Route as projectAccessControlPageRouteKmsImport } from './pages/project/AccessControlPage/route-kms'
import { Route as projectAuditLogsPageRouteCertManagerImport } from './pages/project/AuditLogsPage/route-cert-manager'
+import { Route as projectAppConnectionsPageRouteCertManagerImport } from './pages/project/AppConnectionsPage/route-cert-manager'
import { Route as projectAccessControlPageRouteCertManagerImport } from './pages/project/AccessControlPage/route-cert-manager'
import { Route as sshSettingsPageRouteImport } from './pages/ssh/SettingsPage/route'
import { Route as sshSshHostsPageRouteImport } from './pages/ssh/SshHostsPage/route'
@@ -920,6 +923,13 @@ const projectAuditLogsPageRouteSecretScanningRoute =
getParentRoute: () => secretScanningLayoutRoute,
} as any)
+const projectAppConnectionsPageRouteSecretScanningRoute =
+ projectAppConnectionsPageRouteSecretScanningImport.update({
+ id: '/app-connections',
+ path: '/app-connections',
+ getParentRoute: () => secretScanningLayoutRoute,
+ } as any)
+
const projectAccessControlPageRouteSecretScanningRoute =
projectAccessControlPageRouteSecretScanningImport.update({
id: '/access-management',
@@ -943,6 +953,13 @@ const projectAuditLogsPageRouteSecretManagerRoute =
getParentRoute: () => secretManagerLayoutRoute,
} as any)
+const projectAppConnectionsPageRouteSecretManagerRoute =
+ projectAppConnectionsPageRouteSecretManagerImport.update({
+ id: '/app-connections',
+ path: '/app-connections',
+ getParentRoute: () => secretManagerLayoutRoute,
+ } as any)
+
const projectAccessControlPageRouteSecretManagerRoute =
projectAccessControlPageRouteSecretManagerImport.update({
id: '/access-management',
@@ -989,6 +1006,13 @@ const projectAuditLogsPageRouteCertManagerRoute =
getParentRoute: () => certManagerLayoutRoute,
} as any)
+const projectAppConnectionsPageRouteCertManagerRoute =
+ projectAppConnectionsPageRouteCertManagerImport.update({
+ id: '/app-connections',
+ path: '/app-connections',
+ getParentRoute: () => certManagerLayoutRoute,
+ } as any)
+
const projectAccessControlPageRouteCertManagerRoute =
projectAccessControlPageRouteCertManagerImport.update({
id: '/access-management',
@@ -2730,6 +2754,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof projectAccessControlPageRouteCertManagerImport
parentRoute: typeof certManagerLayoutImport
}
+ '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections': {
+ id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections'
+ path: '/app-connections'
+ fullPath: '/projects/cert-management/$projectId/app-connections'
+ preLoaderRoute: typeof projectAppConnectionsPageRouteCertManagerImport
+ parentRoute: typeof certManagerLayoutImport
+ }
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs'
path: '/audit-logs'
@@ -2772,6 +2803,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof projectAccessControlPageRouteSecretManagerImport
parentRoute: typeof secretManagerLayoutImport
}
+ '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections': {
+ id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections'
+ path: '/app-connections'
+ fullPath: '/projects/secret-management/$projectId/app-connections'
+ preLoaderRoute: typeof projectAppConnectionsPageRouteSecretManagerImport
+ parentRoute: typeof secretManagerLayoutImport
+ }
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs'
path: '/audit-logs'
@@ -2793,6 +2831,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof projectAccessControlPageRouteSecretScanningImport
parentRoute: typeof secretScanningLayoutImport
}
+ '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections': {
+ id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections'
+ path: '/app-connections'
+ fullPath: '/projects/secret-scanning/$projectId/app-connections'
+ preLoaderRoute: typeof projectAppConnectionsPageRouteSecretScanningImport
+ parentRoute: typeof secretScanningLayoutImport
+ }
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs'
path: '/audit-logs'
@@ -3822,6 +3867,7 @@ interface certManagerLayoutRouteChildren {
certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute
certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute
projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute
+ projectAppConnectionsPageRouteCertManagerRoute: typeof projectAppConnectionsPageRouteCertManagerRoute
projectAuditLogsPageRouteCertManagerRoute: typeof projectAuditLogsPageRouteCertManagerRoute
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren
@@ -3841,6 +3887,8 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = {
certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute,
projectAccessControlPageRouteCertManagerRoute:
projectAccessControlPageRouteCertManagerRoute,
+ projectAppConnectionsPageRouteCertManagerRoute:
+ projectAppConnectionsPageRouteCertManagerRoute,
projectAuditLogsPageRouteCertManagerRoute:
projectAuditLogsPageRouteCertManagerRoute,
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRoute:
@@ -4212,6 +4260,7 @@ interface secretManagerLayoutRouteChildren {
secretManagerSecretRotationPageRouteRoute: typeof secretManagerSecretRotationPageRouteRoute
secretManagerSettingsPageRouteRoute: typeof secretManagerSettingsPageRouteRoute
projectAccessControlPageRouteSecretManagerRoute: typeof projectAccessControlPageRouteSecretManagerRoute
+ projectAppConnectionsPageRouteSecretManagerRoute: typeof projectAppConnectionsPageRouteSecretManagerRoute
projectAuditLogsPageRouteSecretManagerRoute: typeof projectAuditLogsPageRouteSecretManagerRoute
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren
secretManagerSecretDashboardPageRouteRoute: typeof secretManagerSecretDashboardPageRouteRoute
@@ -4233,6 +4282,8 @@ const secretManagerLayoutRouteChildren: secretManagerLayoutRouteChildren = {
secretManagerSettingsPageRouteRoute: secretManagerSettingsPageRouteRoute,
projectAccessControlPageRouteSecretManagerRoute:
projectAccessControlPageRouteSecretManagerRoute,
+ projectAppConnectionsPageRouteSecretManagerRoute:
+ projectAppConnectionsPageRouteSecretManagerRoute,
projectAuditLogsPageRouteSecretManagerRoute:
projectAuditLogsPageRouteSecretManagerRoute,
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute:
@@ -4290,6 +4341,7 @@ interface secretScanningLayoutRouteChildren {
secretScanningSecretScanningFindingsPageRouteRoute: typeof secretScanningSecretScanningFindingsPageRouteRoute
secretScanningSettingsPageRouteRoute: typeof secretScanningSettingsPageRouteRoute
projectAccessControlPageRouteSecretScanningRoute: typeof projectAccessControlPageRouteSecretScanningRoute
+ projectAppConnectionsPageRouteSecretScanningRoute: typeof projectAppConnectionsPageRouteSecretScanningRoute
projectAuditLogsPageRouteSecretScanningRoute: typeof projectAuditLogsPageRouteSecretScanningRoute
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren
projectGroupDetailsByIDPageRouteSecretScanningRoute: typeof projectGroupDetailsByIDPageRouteSecretScanningRoute
@@ -4304,6 +4356,8 @@ const secretScanningLayoutRouteChildren: secretScanningLayoutRouteChildren = {
secretScanningSettingsPageRouteRoute: secretScanningSettingsPageRouteRoute,
projectAccessControlPageRouteSecretScanningRoute:
projectAccessControlPageRouteSecretScanningRoute,
+ projectAppConnectionsPageRouteSecretScanningRoute:
+ projectAppConnectionsPageRouteSecretScanningRoute,
projectAuditLogsPageRouteSecretScanningRoute:
projectAuditLogsPageRouteSecretScanningRoute,
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute:
@@ -4689,15 +4743,18 @@ export interface FileRoutesByFullPath {
'/projects/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute
'/projects/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
'/projects/cert-management/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
+ '/projects/cert-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteCertManagerRoute
'/projects/cert-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute
'/projects/cert-management/$projectId/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren
'/projects/cert-management/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren
'/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
'/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
'/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
+ '/projects/secret-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren
'/projects/secret-scanning/$projectId/access-management': typeof projectAccessControlPageRouteSecretScanningRoute
+ '/projects/secret-scanning/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretScanningRoute
'/projects/secret-scanning/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute
'/projects/secret-scanning/$projectId/data-sources': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren
'/projects/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
@@ -4902,12 +4959,15 @@ export interface FileRoutesByTo {
'/projects/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute
'/projects/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
'/projects/cert-management/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
+ '/projects/cert-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteCertManagerRoute
'/projects/cert-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute
'/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
'/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
'/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
+ '/projects/secret-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute
'/projects/secret-scanning/$projectId/access-management': typeof projectAccessControlPageRouteSecretScanningRoute
+ '/projects/secret-scanning/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretScanningRoute
'/projects/secret-scanning/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute
'/projects/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
'/projects/ssh/$projectId/audit-logs': typeof projectAuditLogsPageRouteSshRoute
@@ -5128,15 +5188,18 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/overview': typeof sshSshHostsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute
+ '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections': typeof projectAppConnectionsPageRouteCertManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management': typeof projectAccessControlPageRouteKmsRoute
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
+ '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management': typeof projectAccessControlPageRouteSecretScanningRoute
+ '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections': typeof projectAppConnectionsPageRouteSecretScanningRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute
@@ -5351,15 +5414,18 @@ export interface FileRouteTypes {
| '/projects/ssh/$projectId/overview'
| '/projects/ssh/$projectId/settings'
| '/projects/cert-management/$projectId/access-management'
+ | '/projects/cert-management/$projectId/app-connections'
| '/projects/cert-management/$projectId/audit-logs'
| '/projects/cert-management/$projectId/certificate-templates'
| '/projects/cert-management/$projectId/subscribers'
| '/projects/kms/$projectId/access-management'
| '/projects/kms/$projectId/audit-logs'
| '/projects/secret-management/$projectId/access-management'
+ | '/projects/secret-management/$projectId/app-connections'
| '/projects/secret-management/$projectId/audit-logs'
| '/projects/secret-management/$projectId/integrations'
| '/projects/secret-scanning/$projectId/access-management'
+ | '/projects/secret-scanning/$projectId/app-connections'
| '/projects/secret-scanning/$projectId/audit-logs'
| '/projects/secret-scanning/$projectId/data-sources'
| '/projects/ssh/$projectId/access-management'
@@ -5563,12 +5629,15 @@ export interface FileRouteTypes {
| '/projects/ssh/$projectId/overview'
| '/projects/ssh/$projectId/settings'
| '/projects/cert-management/$projectId/access-management'
+ | '/projects/cert-management/$projectId/app-connections'
| '/projects/cert-management/$projectId/audit-logs'
| '/projects/kms/$projectId/access-management'
| '/projects/kms/$projectId/audit-logs'
| '/projects/secret-management/$projectId/access-management'
+ | '/projects/secret-management/$projectId/app-connections'
| '/projects/secret-management/$projectId/audit-logs'
| '/projects/secret-scanning/$projectId/access-management'
+ | '/projects/secret-scanning/$projectId/app-connections'
| '/projects/secret-scanning/$projectId/audit-logs'
| '/projects/ssh/$projectId/access-management'
| '/projects/ssh/$projectId/audit-logs'
@@ -5787,15 +5856,18 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/overview'
| '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/settings'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management'
+ | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management'
+ | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management'
+ | '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources'
| '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management'
@@ -6388,6 +6460,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management",
+ "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers",
@@ -6424,6 +6497,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secret-rotation",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/settings",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management",
+ "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug",
@@ -6441,6 +6515,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/findings",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/settings",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management",
+ "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId",
@@ -6547,6 +6622,10 @@ export const routeTree = rootRoute
"filePath": "project/AccessControlPage/route-cert-manager.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout"
},
+ "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections": {
+ "filePath": "project/AppConnectionsPage/route-cert-manager.tsx",
+ "parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout"
+ },
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs": {
"filePath": "project/AuditLogsPage/route-cert-manager.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout"
@@ -6578,6 +6657,10 @@ export const routeTree = rootRoute
"filePath": "project/AccessControlPage/route-secret-manager.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout"
},
+ "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections": {
+ "filePath": "project/AppConnectionsPage/route-secret-manager.tsx",
+ "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout"
+ },
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs": {
"filePath": "project/AuditLogsPage/route-secret-manager.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout"
@@ -6670,6 +6753,10 @@ export const routeTree = rootRoute
"filePath": "project/AccessControlPage/route-secret-scanning.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout"
},
+ "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections": {
+ "filePath": "project/AppConnectionsPage/route-secret-scanning.tsx",
+ "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout"
+ },
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs": {
"filePath": "project/AuditLogsPage/route-secret-scanning.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout"
diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts
index 70c34edc6..76680a851 100644
--- a/frontend/src/routes.ts
+++ b/frontend/src/routes.ts
@@ -63,6 +63,7 @@ const secretManagerRoutes = route("/projects/secret-management/$projectId", [
]),
route("/audit-logs", "project/AuditLogsPage/route-secret-manager.tsx"),
route("/access-management", "project/AccessControlPage/route-secret-manager.tsx"),
+ route("/app-connections", "project/AppConnectionsPage/route-secret-manager.tsx"),
route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-manager.tsx"),
route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-manager.tsx"),
route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-manager.tsx"),
@@ -313,6 +314,7 @@ const certManagerRoutes = route("/projects/cert-management/$projectId", [
route("/settings", "cert-manager/SettingsPage/route.tsx"),
route("/audit-logs", "project/AuditLogsPage/route-cert-manager.tsx"),
route("/access-management", "project/AccessControlPage/route-cert-manager.tsx"),
+ route("/app-connections", "project/AppConnectionsPage/route-cert-manager.tsx"),
route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-cert-manager.tsx"),
route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-cert-manager.tsx"),
route("/members/$membershipId", "project/MemberDetailsByIDPage/route-cert-manager.tsx"),
@@ -361,6 +363,7 @@ const secretScanningRoutes = route("/projects/secret-scanning/$projectId", [
route("/settings", "secret-scanning/SettingsPage/route.tsx"),
route("/audit-logs", "project/AuditLogsPage/route-secret-scanning.tsx"),
route("/access-management", "project/AccessControlPage/route-secret-scanning.tsx"),
+ route("/app-connections", "project/AppConnectionsPage/route-secret-scanning.tsx"),
route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-scanning.tsx"),
route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-scanning.tsx"),
route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-scanning.tsx"),
|