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. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -72,7 +72,7 @@ Infisical supports the use of [Service Accounts](https://developer.1password.com ![1Password Connection Modal](/images/app-connections/1password/app-connection-modal.png) - After clicking Create, your **1Password Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **1Password Connection** is established and ready to use with your Infisical project. ![1Password Connection Created](/images/app-connections/1password/app-connection-created.png) @@ -90,6 +90,7 @@ Infisical supports the use of [Service Accounts](https://developer.1password.com --data '{ "name": "my-1password-connection", "method": "api-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "instanceUrl": "https://1pass.example.com", "apiToken": "" @@ -104,6 +105,7 @@ Infisical supports the use of [Service Accounts](https://developer.1password.com "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-1password-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/auth0.mdx b/docs/integrations/app-connections/auth0.mdx index 42e78cb66..91c5f5e60 100644 --- a/docs/integrations/app-connections/auth0.mdx +++ b/docs/integrations/app-connections/auth0.mdx @@ -42,7 +42,7 @@ Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-st - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **Auth0 Connection** option. @@ -67,6 +67,7 @@ Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-st --data '{ "name": "my-auth0-connection", "method": "client-credentials", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "domain": "xxx-xxxxxxxxx.us.auth0.com", "clientId": "...", @@ -83,6 +84,7 @@ Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-st "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-auth0-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/aws.mdx b/docs/integrations/app-connections/aws.mdx index 195f98247..08ae05be7 100644 --- a/docs/integrations/app-connections/aws.mdx +++ b/docs/integrations/app-connections/aws.mdx @@ -184,7 +184,7 @@ Infisical supports two methods for connecting to AWS. - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **AWS Connection** option. @@ -209,6 +209,7 @@ Infisical supports two methods for connecting to AWS. --data '{ "name": "my-aws-connection", "method": "assume-role", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "roleArn": "...", } @@ -222,6 +223,7 @@ Infisical supports two methods for connecting to AWS. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-aws-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 123, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", @@ -361,7 +363,7 @@ Infisical supports two methods for connecting to AWS. - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **AWS Connection** option. @@ -386,6 +388,7 @@ Infisical supports two methods for connecting to AWS. --data '{ "name": "my-aws-connection", "method": "access-key", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "accessKeyId": "...", "secretKey": "..." @@ -400,6 +403,7 @@ Infisical supports two methods for connecting to AWS. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-aws-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 123, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/azure-app-configuration.mdx b/docs/integrations/app-connections/azure-app-configuration.mdx index 4efd19f30..cd9c707be 100644 --- a/docs/integrations/app-connections/azure-app-configuration.mdx +++ b/docs/integrations/app-connections/azure-app-configuration.mdx @@ -83,7 +83,7 @@ Infisical currently only supports two methods for connecting to Azure, which are - Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/azure-client-secrets.mdx b/docs/integrations/app-connections/azure-client-secrets.mdx index 37e7d49b0..cb25fb596 100644 --- a/docs/integrations/app-connections/azure-client-secrets.mdx +++ b/docs/integrations/app-connections/azure-client-secrets.mdx @@ -94,7 +94,7 @@ Infisical currently only supports two methods for connecting to Azure, which are - Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/azure-devops.mdx b/docs/integrations/app-connections/azure-devops.mdx index 4744a35c6..7eabff84b 100644 --- a/docs/integrations/app-connections/azure-devops.mdx +++ b/docs/integrations/app-connections/azure-devops.mdx @@ -117,7 +117,7 @@ Infisical currently supports three methods for connecting to Azure DevOps, which - Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/azure-key-vault.mdx b/docs/integrations/app-connections/azure-key-vault.mdx index 866a1de82..b2989efae 100644 --- a/docs/integrations/app-connections/azure-key-vault.mdx +++ b/docs/integrations/app-connections/azure-key-vault.mdx @@ -83,7 +83,7 @@ Infisical currently only supports two methods for connecting to Azure, which are - Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/bitbucket.mdx b/docs/integrations/app-connections/bitbucket.mdx index be4fdbee7..3b3385202 100644 --- a/docs/integrations/app-connections/bitbucket.mdx +++ b/docs/integrations/app-connections/bitbucket.mdx @@ -78,7 +78,7 @@ Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbuck - In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -95,7 +95,7 @@ Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbuck ![Bitbucket Connection Modal](/images/app-connections/bitbucket/step-6.png) - After clicking Create, your **Bitbucket Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **Bitbucket Connection** is established and ready to use with your Infisical project. ![Bitbucket Connection Created](/images/app-connections/bitbucket/step-7.png) @@ -113,6 +113,7 @@ Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbuck --data '{ "name": "my-bitbucket-connection", "method": "api-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "email": "user@example.com", "apiToken": "" @@ -127,6 +128,7 @@ Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbuck "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-bitbucket-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/camunda.mdx b/docs/integrations/app-connections/camunda.mdx index 68084cea3..7ad0f8ef9 100644 --- a/docs/integrations/app-connections/camunda.mdx +++ b/docs/integrations/app-connections/camunda.mdx @@ -50,8 +50,7 @@ Infisical supports connecting to Camunda APIs using [client credentials](https:/ - Navigate to the **App Connections** tab on the **Organization Settings** - page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/checkly.mdx b/docs/integrations/app-connections/checkly.mdx index 38234744d..943a470c2 100644 --- a/docs/integrations/app-connections/checkly.mdx +++ b/docs/integrations/app-connections/checkly.mdx @@ -37,7 +37,7 @@ Infisical supports the use of [API Keys](https://app.checklyhq.com/settings/user - In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -55,7 +55,7 @@ Infisical supports the use of [API Keys](https://app.checklyhq.com/settings/user ![Checkly Connection Modal](/images/app-connections/checkly/checkly-app-connection-form.png) - After submitting the form, your **Checkly Connection** will be successfully created and ready to use with your Infisical projects. + After submitting the form, your **Checkly Connection** will be successfully created and ready to use with your Infisical project. ![Checkly Connection Created](/images/app-connections/checkly/checkly-app-connection-generated.png) @@ -75,6 +75,7 @@ Infisical supports the use of [API Keys](https://app.checklyhq.com/settings/user --data '{ "name": "my-checkly-connection", "method": "api-key", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "apiKey": "[API KEY]" } @@ -88,6 +89,7 @@ Infisical supports the use of [API Keys](https://app.checklyhq.com/settings/user "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-checkly-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/cloudflare.mdx b/docs/integrations/app-connections/cloudflare.mdx index 241c737bc..33a9a992c 100644 --- a/docs/integrations/app-connections/cloudflare.mdx +++ b/docs/integrations/app-connections/cloudflare.mdx @@ -88,8 +88,7 @@ Infisical supports connecting to Cloudflare using API tokens and Account ID for - Navigate to the **App Connections** tab on the **Organization Settings** - page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/databricks.mdx b/docs/integrations/app-connections/databricks.mdx index 38d125ea6..e0b861184 100644 --- a/docs/integrations/app-connections/databricks.mdx +++ b/docs/integrations/app-connections/databricks.mdx @@ -43,8 +43,7 @@ Infisical supports the use of [service principals](https://docs.databricks.com/e - Navigate to the **App Connections** tab on the **Organization Settings** - page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/digital-ocean.mdx b/docs/integrations/app-connections/digital-ocean.mdx index 5b047f017..ff2eccfc1 100644 --- a/docs/integrations/app-connections/digital-ocean.mdx +++ b/docs/integrations/app-connections/digital-ocean.mdx @@ -45,7 +45,7 @@ Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/accoun - In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -63,7 +63,7 @@ Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/accoun ![DigitalOcean Connection Modal](/images/app-connections/digital-ocean/app-connection-form.png) - After submitting the form, your **DigitalOcean Connection** will be successfully created and ready to use with your Infisical projects. + After submitting the form, your **DigitalOcean Connection** will be successfully created and ready to use with your Infisical project. ![DigitalOcean Connection Created](/images/app-connections/digital-ocean/app-connection-generated.png) @@ -82,6 +82,7 @@ Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/accoun --data '{ "name": "my-digitalocean-connection", "method": "api-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "apiToken": "[API TOKEN]" } @@ -95,6 +96,7 @@ Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/accoun "appConnection": { "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "name": "my-digitalocean-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "abcdef12-3456-7890-abcd-ef1234567890", diff --git a/docs/integrations/app-connections/flyio.mdx b/docs/integrations/app-connections/flyio.mdx index e42756254..bf36ccc9b 100644 --- a/docs/integrations/app-connections/flyio.mdx +++ b/docs/integrations/app-connections/flyio.mdx @@ -30,7 +30,7 @@ Infisical supports the use of [Access Tokens](https://fly.io/docs/security/token - In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -48,7 +48,7 @@ Infisical supports the use of [Access Tokens](https://fly.io/docs/security/token ![Fly.io Connection Modal](/images/app-connections/flyio/app-connection-modal.png) - After clicking Create, your **Fly.io Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **Fly.io Connection** is established and ready to use with your Infisical project. ![Fly.io Connection Created](/images/app-connections/flyio/app-connection-created.png) @@ -66,6 +66,7 @@ Infisical supports the use of [Access Tokens](https://fly.io/docs/security/token --data '{ "name": "my-flyio-connection", "method": "access-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "accessToken": "[PRIVATE TOKEN]" } @@ -79,6 +80,7 @@ Infisical supports the use of [Access Tokens](https://fly.io/docs/security/token "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-flyio-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/gcp.mdx b/docs/integrations/app-connections/gcp.mdx index 129c26c2a..9458339af 100644 --- a/docs/integrations/app-connections/gcp.mdx +++ b/docs/integrations/app-connections/gcp.mdx @@ -82,8 +82,7 @@ Infisical supports [service account impersonation](https://cloud.google.com/iam/ - Navigate to the **App Connections** tab on the **Organization Settings** - page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/github-radar.mdx b/docs/integrations/app-connections/github-radar.mdx index 376973efd..491070a8b 100644 --- a/docs/integrations/app-connections/github-radar.mdx +++ b/docs/integrations/app-connections/github-radar.mdx @@ -97,7 +97,7 @@ Infisical supports GitHub App installation for creating a GitHub Radar Connectio - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/github.mdx b/docs/integrations/app-connections/github.mdx index 9a952f815..e44fc405a 100644 --- a/docs/integrations/app-connections/github.mdx +++ b/docs/integrations/app-connections/github.mdx @@ -85,7 +85,7 @@ Infisical supports two methods for connecting to GitHub. - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -156,7 +156,7 @@ Infisical supports two methods for connecting to GitHub. - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/gitlab.mdx b/docs/integrations/app-connections/gitlab.mdx index 60e9236ca..4f7223d93 100644 --- a/docs/integrations/app-connections/gitlab.mdx +++ b/docs/integrations/app-connections/gitlab.mdx @@ -70,7 +70,7 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -193,7 +193,7 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/hashicorp-vault.mdx b/docs/integrations/app-connections/hashicorp-vault.mdx index c49b53ab8..7d5502fc5 100644 --- a/docs/integrations/app-connections/hashicorp-vault.mdx +++ b/docs/integrations/app-connections/hashicorp-vault.mdx @@ -131,7 +131,7 @@ Infisical supports two methods for connecting to Hashicorp Vault. - In your Infisical dashboard, go to **Organization Settings** and select the **App Connections** tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -184,6 +184,7 @@ Infisical supports two methods for connecting to Hashicorp Vault. --data '{ "name": "my-vault-connection", "method": "app-role", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "instanceUrl": "https://vault.example.com", "roleId": "4797c4fa-7794-71f0-c8b1-7c87759df5bf", @@ -199,6 +200,7 @@ Infisical supports two methods for connecting to Hashicorp Vault. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-vault-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2025-04-01T05:31:56Z", diff --git a/docs/integrations/app-connections/heroku.mdx b/docs/integrations/app-connections/heroku.mdx index fc2d1fbcc..9c3397e02 100644 --- a/docs/integrations/app-connections/heroku.mdx +++ b/docs/integrations/app-connections/heroku.mdx @@ -51,7 +51,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -93,7 +93,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To ![Heroku API Token](/images/app-connections/heroku/heroku-api-token.png) - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/humanitec.mdx b/docs/integrations/app-connections/humanitec.mdx index 570d3ba5d..669798eeb 100644 --- a/docs/integrations/app-connections/humanitec.mdx +++ b/docs/integrations/app-connections/humanitec.mdx @@ -53,7 +53,7 @@ Infisical supports connecting to Humanitec using a service user. ![Humanitec Connection Created](/images/app-connections/humanitec/humanitec-user-added.png) - Navigate to the **App Connections** tab on the **Organization Settings** page. + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/ldap.mdx b/docs/integrations/app-connections/ldap.mdx index db0b596ce..f0afa1157 100644 --- a/docs/integrations/app-connections/ldap.mdx +++ b/docs/integrations/app-connections/ldap.mdx @@ -33,7 +33,7 @@ Depending on how you intend to use your LDAP connection, there may be additional - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **LDAP Connection** option. @@ -58,6 +58,7 @@ Depending on how you intend to use your LDAP connection, there may be additional --data '{ "name": "my-ldap-connection", "method": "simple-bind", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "provider": "active-directory", "url": "ldaps://domain-or-ip:636", @@ -76,6 +77,7 @@ Depending on how you intend to use your LDAP connection, there may be additional "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-ldap-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/mssql.mdx b/docs/integrations/app-connections/mssql.mdx index 7e940804d..77e8e676d 100644 --- a/docs/integrations/app-connections/mssql.mdx +++ b/docs/integrations/app-connections/mssql.mdx @@ -62,7 +62,7 @@ Infisical supports connecting to Microsoft SQL Server using database principals. - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **Microsoft SQL Server Connection** option. @@ -96,6 +96,7 @@ Infisical supports connecting to Microsoft SQL Server using database principals. --data '{ "name": "my-mssql-connection", "method": "username-and-password", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "isPlatformManagedCredentials": true, "credentials": { "host": "123.4.5.6", @@ -115,7 +116,8 @@ Infisical supports connecting to Microsoft SQL Server using database principals. { "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", - "name": "my-pg-connection", + "name": "my-mssql-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/mysql.mdx b/docs/integrations/app-connections/mysql.mdx index 38a8a4e97..6055d77cc 100644 --- a/docs/integrations/app-connections/mysql.mdx +++ b/docs/integrations/app-connections/mysql.mdx @@ -52,7 +52,7 @@ Infisical supports connecting to MySQL using a database role. - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **MySQL Connection** option. @@ -88,6 +88,7 @@ Infisical supports connecting to MySQL using a database role. "name": "my-mysql-connection", "method": "username-and-password", "isPlatformManagedCredentials": true, + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "host": "123.4.5.6", "port": 3306, @@ -107,6 +108,7 @@ Infisical supports connecting to MySQL using a database role. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-mysql-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/netlify.mdx b/docs/integrations/app-connections/netlify.mdx index cd4dfb1b5..d2a62113c 100644 --- a/docs/integrations/app-connections/netlify.mdx +++ b/docs/integrations/app-connections/netlify.mdx @@ -35,7 +35,7 @@ Infisical supports the use of [Personal Access Tokens](https://docs.netlify.com/ - In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -53,7 +53,7 @@ Infisical supports the use of [Personal Access Tokens](https://docs.netlify.com/ ![Netlify Connection Modal](/images/app-connections/netlify/app-connection-form.png) - After submitting the form, your **Netlify Connection** will be successfully created and ready to use with your Infisical projects. + After submitting the form, your **Netlify Connection** will be successfully created and ready to use with your Infisical project. ![Netlify Connection Created](/images/app-connections/netlify/app-connection-generated.png) @@ -72,6 +72,7 @@ Infisical supports the use of [Personal Access Tokens](https://docs.netlify.com/ --data '{ "name": "my-netlify-connection", "method": "access-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "accessToken": "[ACCESS TOKEN]" } @@ -86,6 +87,7 @@ Infisical supports the use of [Personal Access Tokens](https://docs.netlify.com/ "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "name": "my-netlify-connection", "description": null, + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "abcdef12-3456-7890-abcd-ef1234567890", "createdAt": "2025-07-19T10:15:00.000Z", diff --git a/docs/integrations/app-connections/oci.mdx b/docs/integrations/app-connections/oci.mdx index 58fb3c1d3..10d5fabaa 100644 --- a/docs/integrations/app-connections/oci.mdx +++ b/docs/integrations/app-connections/oci.mdx @@ -117,7 +117,7 @@ Infisical supports the use of [API Signing Key Authentication](https://docs.orac - In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -139,7 +139,7 @@ Infisical supports the use of [API Signing Key Authentication](https://docs.orac ![OCI Connection Modal](/images/app-connections/oci/app-connection-modal.png) - After clicking Create, your **OCI Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **OCI Connection** is established and ready to use with your Infisical project. ![OCI Connection Created](/images/app-connections/oci/app-connection-created.png) @@ -157,6 +157,7 @@ Infisical supports the use of [API Signing Key Authentication](https://docs.orac --data '{ "name": "my-oci-connection", "method": "access-key", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "userOcid": "ocid1.user.oc1..aaaaaaaagrp35tbkvvad4y2j7sug7xonua7dl2gfp4at2u5i5xj4ghnitg3a", "tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaotfma465m4zumfe2ua64mj2m5dwmlw2llh4g4dnfttnakiifonta", @@ -174,6 +175,7 @@ Infisical supports the use of [API Signing Key Authentication](https://docs.orac "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-oci-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/okta.mdx b/docs/integrations/app-connections/okta.mdx index 3c1295cf8..cb5edecbd 100644 --- a/docs/integrations/app-connections/okta.mdx +++ b/docs/integrations/app-connections/okta.mdx @@ -31,7 +31,7 @@ Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guide - In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -48,7 +48,7 @@ Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guide ![Connection Modal](/images/app-connections/okta/step-4.png) - After clicking Create, your **Okta Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **Okta Connection** is established and ready to use with your Infisical project. ![Connection Created](/images/app-connections/okta/step-5.png) @@ -66,6 +66,7 @@ Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guide --data '{ "name": "my-okta-connection", "method": "api-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "instanceUrl": "https://example.okta.com", "apiToken": "" @@ -80,6 +81,7 @@ Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guide "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-okta-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/oracledb.mdx b/docs/integrations/app-connections/oracledb.mdx index 8cab6371b..47aa33356 100644 --- a/docs/integrations/app-connections/oracledb.mdx +++ b/docs/integrations/app-connections/oracledb.mdx @@ -62,7 +62,7 @@ Infisical supports connecting to OracleDB using a database user. - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **OracleDB Connection** option. @@ -98,6 +98,7 @@ Infisical supports connecting to OracleDB using a database user. "name": "my-oracledb-connection", "method": "username-and-password", "isPlatformManagedCredentials": true, + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "host": "123.4.5.6", "port": 1521, @@ -117,6 +118,7 @@ Infisical supports connecting to OracleDB using a database user. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-oracledb-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/overview.mdx b/docs/integrations/app-connections/overview.mdx index e698c4302..8b1032e7d 100644 --- a/docs/integrations/app-connections/overview.mdx +++ b/docs/integrations/app-connections/overview.mdx @@ -3,12 +3,16 @@ sidebarTitle: "Overview" description: "Learn how to manage and configure third-party app connections with Infisical." --- -App Connections enable your organization to integrate Infisical with third-party services in a secure and versatile way. +App Connections enable you to integrate your Infisical projects with third-party services in a secure and versatile way. + + + App connections can also be created and managed independently in projects now. + ## Concept -App Connections are an organization-level resource used to establish connections with third-party applications -that can be used across Infisical projects. Example use cases include syncing secrets, generating dynamic secrets, and more. +App Connections can be used to establish connections with third-party applications +that can be used across multiple features. Example use cases include syncing secrets, rotating credentials, scanning repositories for secret leaks, and more.
diff --git a/docs/integrations/app-connections/postgres.mdx b/docs/integrations/app-connections/postgres.mdx index 239608905..dcb6c76d8 100644 --- a/docs/integrations/app-connections/postgres.mdx +++ b/docs/integrations/app-connections/postgres.mdx @@ -60,7 +60,7 @@ Infisical supports connecting to PostgreSQL using a database role. - 1. Navigate to the App Connections tab on the Organization Settings page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **PostgreSQL Connection** option. @@ -95,6 +95,7 @@ Infisical supports connecting to PostgreSQL using a database role. "name": "my-pg-connection", "method": "username-and-password", "isPlatformManagedCredentials": true, + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "host": "123.4.5.6", "port": 5432, @@ -114,6 +115,7 @@ Infisical supports connecting to PostgreSQL using a database role. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-pg-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 1, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/railway.mdx b/docs/integrations/app-connections/railway.mdx index 7b53d02ad..b88b3fbf1 100644 --- a/docs/integrations/app-connections/railway.mdx +++ b/docs/integrations/app-connections/railway.mdx @@ -96,7 +96,7 @@ Infisical supports the use of [API Tokens](https://docs.railway.com/guides/publi - In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -115,7 +115,7 @@ Infisical supports the use of [API Tokens](https://docs.railway.com/guides/publi ![Railway Connection Modal](/images/app-connections/railway/railway-app-connection-form.png)
- After submitting the form, your **Railway Connection** will be successfully created and ready to use with your Infisical projects. + After submitting the form, your **Railway Connection** will be successfully created and ready to use with your Infisical project. ![Railway Connection Created](/images/app-connections/railway/railway-app-connection-generated.png) @@ -134,6 +134,7 @@ Infisical supports the use of [API Tokens](https://docs.railway.com/guides/publi --data '{ "name": "my-railway-connection", "method": "team-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "apiToken": "[TEAM TOKEN]" } @@ -147,6 +148,7 @@ Infisical supports the use of [API Tokens](https://docs.railway.com/guides/publi "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-railway-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/render.mdx b/docs/integrations/app-connections/render.mdx index 580ec953e..78e050135 100644 --- a/docs/integrations/app-connections/render.mdx +++ b/docs/integrations/app-connections/render.mdx @@ -33,8 +33,7 @@ Infisical supports connecting to Render using API keys for secure access to your - Navigate to the **App Connections** tab on the **Organization Settings** - page. ![App Connections + Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/supabase.mdx b/docs/integrations/app-connections/supabase.mdx index 9716b1526..80290cec9 100644 --- a/docs/integrations/app-connections/supabase.mdx +++ b/docs/integrations/app-connections/supabase.mdx @@ -34,7 +34,7 @@ Infisical supports the use of [Personal Access Tokens](https://supabase.com/dash - In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -53,7 +53,7 @@ Infisical supports the use of [Personal Access Tokens](https://supabase.com/dash ![Supabase Connection Modal](/images/app-connections/supabase/app-connection-form.png) - After submitting the form, your **Supabase Connection** will be successfully created and ready to use with your Infisical projects. + After submitting the form, your **Supabase Connection** will be successfully created and ready to use with your Infisical project. ![Supabase Connection Created](/images/app-connections/supabase/app-connection-generated.png) @@ -73,6 +73,7 @@ Infisical supports the use of [Personal Access Tokens](https://supabase.com/dash --data '{ "name": "my-supabase-connection", "method": "access-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "accessToken": "[Access Token]", "instanceUrl": "https://api.supabase.com" @@ -87,6 +88,7 @@ Infisical supports the use of [Personal Access Tokens](https://supabase.com/dash "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-supabase-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/teamcity.mdx b/docs/integrations/app-connections/teamcity.mdx index 889355954..311326f07 100644 --- a/docs/integrations/app-connections/teamcity.mdx +++ b/docs/integrations/app-connections/teamcity.mdx @@ -51,7 +51,7 @@ Infisical supports connecting to TeamCity using Access Tokens. 1. Navigate to App Connections - In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Add Connection @@ -68,7 +68,7 @@ Infisical supports connecting to TeamCity using Access Tokens. ![TeamCity Connection Modal](/images/app-connections/teamcity/teamcity-app-connection-modal.png) 4. Connection Created - After clicking Create, your **TeamCity Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **TeamCity Connection** is established and ready to use with your Infisical project. ![TeamCity Connection Created](/images/app-connections/teamcity/teamcity-app-connection-created.png) @@ -84,6 +84,7 @@ Infisical supports connecting to TeamCity using Access Tokens. --data '{ "name": "my-teamcity-connection", "method": "access-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "accessToken": "...", "instanceUrl": "https://yourcompany.teamcity.com" @@ -98,6 +99,7 @@ Infisical supports connecting to TeamCity using Access Tokens. "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-teamcity-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/docs/integrations/app-connections/terraform-cloud.mdx b/docs/integrations/app-connections/terraform-cloud.mdx index 02deb22cc..bc3da7810 100644 --- a/docs/integrations/app-connections/terraform-cloud.mdx +++ b/docs/integrations/app-connections/terraform-cloud.mdx @@ -30,7 +30,7 @@ Infisical supports connecting to Terraform Cloud using a service user. - 1. Navigate to the **App Connections** tab on the **Organization Settings** page. + 1. Navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Select the **Terraform Cloud Connection** option from the connection options modal. ![Select Terraform Cloud Connection](/images/app-connections/terraform-cloud/terraform-cloud-app-connection-option.png) @@ -52,6 +52,7 @@ Infisical supports connecting to Terraform Cloud using a service user. --data '{ "name": "my-terraform-cloud-connection", "method": "api-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "apiToken": "...", } @@ -65,6 +66,7 @@ Infisical supports connecting to Terraform Cloud using a service user. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-terraform-cloud-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 123, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2023-11-07T05:31:56Z", diff --git a/docs/integrations/app-connections/vercel.mdx b/docs/integrations/app-connections/vercel.mdx index 7ab7bea1b..17973db9b 100644 --- a/docs/integrations/app-connections/vercel.mdx +++ b/docs/integrations/app-connections/vercel.mdx @@ -37,7 +37,7 @@ Infisical supports connecting to Vercel using API Tokens. 1. Navigate to App Connections - In your Infisical dashboard, go to **Organization Settings** and select the **App Connections** tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) 2. Add Connection @@ -52,7 +52,7 @@ Infisical supports connecting to Vercel using API Tokens. ![Vercel Connection Modal](/images/app-connections/vercel/vercel-app-connection-modal.png) 4. Connection Created - After clicking Create, your **Vercel Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **Vercel Connection** is established and ready to use with your Infisical project. ![Vercel Connection Created](/images/app-connections/vercel/vercel-app-connection-created.png) @@ -67,6 +67,7 @@ Infisical supports connecting to Vercel using API Tokens. --header 'Content-Type: application/json' \ --data '{ "name": "my-vercel-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "method": "api-token", "credentials": { "apiToken": "...", @@ -81,6 +82,7 @@ Infisical supports connecting to Vercel using API Tokens. "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-vercel-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 123, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2025-04-01T05:31:56Z", diff --git a/docs/integrations/app-connections/windmill.mdx b/docs/integrations/app-connections/windmill.mdx index 5cab9fa38..d90c83a1b 100644 --- a/docs/integrations/app-connections/windmill.mdx +++ b/docs/integrations/app-connections/windmill.mdx @@ -47,7 +47,8 @@ Ensure the user generating the access token has the required role and permission - In your Infisical dashboard, go to **Organization Settings** and select the **App Connections** tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. + ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -82,6 +83,7 @@ Ensure the user generating the access token has the required role and permission --data '{ "name": "my-windmill-connection", "method": "access-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "token": "...", "instanceUrl": "https://app.windmill.dev" @@ -96,6 +98,7 @@ Ensure the user generating the access token has the required role and permission "appConnection": { "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "name": "my-windmill-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "version": 123, "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "createdAt": "2025-04-01T05:31:56Z", diff --git a/docs/integrations/app-connections/zabbix.mdx b/docs/integrations/app-connections/zabbix.mdx index c4d47b22e..45141e827 100644 --- a/docs/integrations/app-connections/zabbix.mdx +++ b/docs/integrations/app-connections/zabbix.mdx @@ -31,7 +31,7 @@ Infisical supports the use of [API Tokens](https://www.zabbix.com/documentation/ - In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -50,7 +50,7 @@ Infisical supports the use of [API Tokens](https://www.zabbix.com/documentation/ ![Zabbix Connection Modal](/images/app-connections/zabbix/zabbix-app-connection-form.png) - After clicking Create, your **Zabbix Connection** is established and ready to use with your Infisical projects. + After clicking Create, your **Zabbix Connection** is established and ready to use with your Infisical project. ![Zabbix Connection Created](/images/app-connections/zabbix/zabbix-app-connection-generated.png) @@ -68,6 +68,7 @@ Infisical supports the use of [API Tokens](https://www.zabbix.com/documentation/ --data '{ "name": "my-zabbix-connection", "method": "api-token", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "credentials": { "apiToken": "[API TOKEN]", "instanceUrl": "https://zabbix.example.com" @@ -82,6 +83,7 @@ Infisical supports the use of [API Tokens](https://www.zabbix.com/documentation/ "appConnection": { "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "name": "my-zabbix-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", "description": null, "version": 1, "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", diff --git a/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 }) => ( + +
+
+ +
+ App Connections +
+
+ )} + { )} + + {({ isActive }) => ( + +
+
+ +
+ App Connections +
+
+ )} +
{ )} + + {({ isActive }) => ( + +
+
+ +
+ App Connections +
+
+ )} +
{ }, [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) => ( - - )} - -
- } - 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 ( -
+
+
+
+
+

App Connections

+ +
+ + Docs + +
+
+
+

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

+
+ + {(isAllowed) => ( + + )} + +
+ {!isProjectView && ( +
+ + +
+ )} setSearch(e.target.value)} @@ -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"),