feature: add suport for project scoped app connections

This commit is contained in:
Scott Wilson
2025-09-12 17:26:01 -07:00
parent 666ae3bc54
commit 93e5cf4b2b
135 changed files with 2823 additions and 742 deletions

View File

@@ -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<void> {
@@ -13,9 +14,7 @@ export async function up(knex: Knex): Promise<void> {
}
export async function down(knex: Knex): Promise<void> {
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"]);

View File

@@ -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<void> {
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<void> {
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);
}
}
}

View File

@@ -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<typeof AppConnectionsSchema>;

View File

@@ -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<TCreateAppConnectionDTO, "credentials"> & { connectionId: string };
metadata: Omit<TCreateAppConnectionDTO, "credentials" | "projectId"> & { connectionId: string };
}
interface UpdateAppConnectionEvent {
type: EventType.UPDATE_APP_CONNECTION;
metadata: Omit<TUpdateAppConnectionDTO, "credentials"> & { connectionId: string; credentialsUpdated: boolean };
metadata: Omit<TUpdateAppConnectionDTO, "credentials" | "projectId"> & {
connectionId: string;
credentialsUpdated: boolean;
};
}
interface DeleteAppConnectionEvent {
@@ -3697,6 +3716,8 @@ export type Event =
| CreateAppConnectionEvent
| UpdateAppConnectionEvent
| DeleteAppConnectionEvent
| GetAppConnectionUsageEvent
| MigrateAppConnectionEvent
| GetSshHostGroupEvent
| CreateSshHostGroupEvent
| UpdateSshHostGroupEvent

View File

@@ -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;
};

View File

@@ -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<ProjectPermissionSub.SecretEvents> & SecretEventSubjectFields)
]
| [
ProjectPermissionAppConnectionActions,
(
| ProjectPermissionSub.AppConnections
| (ForcedSubject<ProjectPermissionSub.AppConnections> & 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()
})
];

View File

@@ -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 });

View File

@@ -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 = <T extends Awaited<ReturnType<typeof baseSecretRota
connectionUpdatedAt,
connectionVersion,
connectionGatewayId,
connectionProjectId,
connectionIsPlatformManagedCredentials,
...el
} = secretRotation;
@@ -126,6 +128,7 @@ const expandSecretRotation = <T extends Awaited<ReturnType<typeof baseSecretRota
updatedAt: connectionUpdatedAt,
version: connectionVersion,
gatewayId: connectionGatewayId,
projectId: connectionProjectId,
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials
},
folder: {

View File

@@ -89,7 +89,7 @@ import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
export type TSecretRotationV2ServiceFactoryDep = {
secretRotationV2DAL: TSecretRotationV2DALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
@@ -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](
{

View File

@@ -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
};

View File

@@ -60,7 +60,7 @@ import { TSecretScanningV2QueueServiceFactory } from "./secret-scanning-v2-queue
export type TSecretScanningV2ServiceFactoryDep = {
secretScanningV2DAL: TSecretScanningV2DALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -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
);
}

View File

@@ -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) => {

View File

@@ -1837,7 +1837,8 @@ export const registerRoutes = async (
gatewayService,
gatewayV2Service,
gatewayDAL,
gatewayV2DAL
gatewayV2DAL,
projectDAL
});
const secretSyncService = secretSyncServiceFactory({

View File

@@ -26,6 +26,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
description?: string | null;
isPlatformManagedCredentials?: boolean;
gatewayId?: string | null;
projectId?: string;
}>;
updateSchema: z.ZodType<{
name?: string;
@@ -47,18 +48,27 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
schema: {
hide: false,
tags: [ApiDocsTags.AppConnections],
description: `List the ${appName} Connections for the current organization.`,
description: `List the ${appName} Connections for the current organization or project.`,
querystring: z.object({
projectId: z.string().optional().describe(AppConnections.LIST(app).projectId)
}),
response: {
200: z.object({ appConnections: sanitizedResponseSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
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 = <T extends TAppConnection, I exten
schema: {
hide: false,
tags: [ApiDocsTags.AppConnections],
description: `List the ${appName} Connections the current user has permission to establish connections with.`,
description: `List the ${appName} Connections the current user has permission to establish connections within this project.`,
querystring: z.object({
projectId: z.string().describe(AppConnections.LIST(app).projectId)
}),
response: {
200: z.object({
appConnections: z
.object({
app: z.literal(app),
name: z.string(),
id: z.string().uuid()
id: z.string().uuid(),
projectId: z.string().nullish(),
orgId: z.string()
})
.array()
})
@@ -97,14 +112,17 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
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 = <T extends TAppConnection, I exten
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: appConnection.projectId ?? undefined,
event: {
type: EventType.GET_APP_CONNECTION,
metadata: {
@@ -178,6 +197,9 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
.min(1, "Connection name required")
.describe(AppConnections.GET_BY_NAME(app).connectionName)
}),
querystring: z.object({
projectId: z.string().trim().optional().describe(AppConnections.GET_BY_NAME(app).projectId)
}),
response: {
200: z.object({ appConnection: sanitizedResponseSchema })
}
@@ -185,16 +207,21 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
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 = <T extends TAppConnection, I exten
schema: {
hide: false,
tags: [ApiDocsTags.AppConnections],
description: `Create ${
startsWithVowel(appName) ? "an" : "a"
} ${appName} Connection for the current organization.`,
description: `Create ${startsWithVowel(appName) ? "an" : "a"} ${appName} Connection.`,
body: createSchema,
response: {
200: z.object({ appConnection: sanitizedResponseSchema })
@@ -226,16 +251,17 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
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 = <T extends TAppConnection, I exten
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: appConnection.projectId ?? undefined,
event: {
type: EventType.UPDATE_APP_CONNECTION,
metadata: {
@@ -329,6 +356,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: appConnection.projectId ?? undefined,
event: {
type: EventType.DELETE_APP_CONNECTION,
metadata: {
@@ -340,4 +368,81 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
return { appConnection };
}
});
// scott: we will need this once we have individual app connection page and may want to expose to API
// server.route({
// method: "GET",
// url: `/:connectionId/usage`,
// config: {
// rateLimit: readLimit
// },
// schema: {
// hide: true, // scott: we could expose this in the future but just for UI right now
// tags: [ApiDocsTags.AppConnections],
// params: z.object({
// connectionId: z.string().uuid()
// }),
// response: {
// 200: z.object({
// projects: z
// .object({
// id: z.string(),
// name: z.string(),
// type: z.nativeEnum(ProjectType),
// slug: z.string(),
// resources: z.object({
// secretSyncs: z
// .object({
// id: z.string(),
// name: z.string()
// })
// .array(),
// secretRotations: z
// .object({
// id: z.string(),
// name: z.string()
// })
// .array(),
// externalCas: z
// .object({
// id: z.string(),
// name: z.string()
// })
// .array(),
// dataSources: z
// .object({
// id: z.string(),
// name: z.string()
// })
// .array()
// })
// })
// .array()
// })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async (req) => {
// 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 };
// }
// });
};

View File

@@ -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: {

View File

@@ -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<typeof appConnectionDALFactory>;
type AppConnectionFindFilter = Parameters<typeof buildFindFilter<TAppConnections>>[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 };
};

View File

@@ -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<string, ProjectWithResources>();
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());
};

View File

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

View File

@@ -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<TPermissionServiceFactory, "getOrgPermission">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
gatewayDAL: Pick<TGatewayDALFactory, "find">;
gatewayV2DAL: Pick<TGatewayV2DALFactory, "find">;
projectDAL: Pick<TProjectDALFactory, "findProjectById">;
};
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
@@ -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<TAppConnection, "credentials">[];
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<TAppConnection, "credentials">[];
};
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),

View File

@@ -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<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app" | "projectId">> & {
connectionId: string;
};
export type TGetAppConnectionByNameDTO = {
connectionName: string;
projectId?: string;
};
export type TValidateAppConnectionUsageByIdDTO = {
connectionId: string;
projectId: string;
};
export type TAppConnectionConfig =
| TAwsConnectionConfig
| TGitHubConnectionConfig

View File

@@ -51,7 +51,7 @@ const authorizeAuth0Connection = async ({
};
export const getAuth0ConnectionAccessToken = async (
{ id, orgId, credentials }: TAuth0Connection,
{ id, orgId, credentials, projectId }: TAuth0Connection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
@@ -72,7 +72,8 @@ export const getAuth0ConnectionAccessToken = async (
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials,
orgId,
kmsService
kmsService,
projectId
});
await appConnectionDAL.updateById(id, { encryptedCredentials });

View File

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

View File

@@ -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
});

View File

@@ -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 });

View File

@@ -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<ExchangeCodeAzureResponse>(
@@ -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 });

View File

@@ -40,7 +40,7 @@ const authorizeCamundaConnection = async ({
};
export const getCamundaConnectionAccessToken = async (
{ id, orgId, credentials }: TCamundaConnection,
{ id, orgId, credentials, projectId }: TCamundaConnection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
@@ -61,7 +61,8 @@ export const getCamundaConnectionAccessToken = async (
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials,
orgId,
kmsService
kmsService,
projectId
});
await appConnectionDAL.updateById(id, { encryptedCredentials });

View File

@@ -47,7 +47,7 @@ const authorizeDatabricksConnection = async ({
};
export const getDatabricksConnectionAccessToken = async (
{ id, orgId, credentials }: TDatabricksConnection,
{ id, orgId, credentials, projectId }: TDatabricksConnection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
@@ -68,7 +68,8 @@ export const getDatabricksConnectionAccessToken = async (
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials,
orgId,
kmsService
kmsService,
projectId
});
await appConnectionDAL.updateById(id, { encryptedCredentials });

View File

@@ -64,6 +64,7 @@ export const refreshGitLabToken = async (
refreshToken: string,
appId: string,
orgId: string,
projectId: string | undefined | null,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">,
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

View File

@@ -36,6 +36,7 @@ export const refreshHerokuToken = async (
refreshToken: string,
appId: string,
orgId: string,
projectId: string | null | undefined,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<string> => {
@@ -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
);

View File

@@ -42,10 +42,10 @@ import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/
type TAcmeCertificateAuthorityFnsDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
certificateAuthorityDAL: Pick<
TCertificateAuthorityDALFactory,
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa"
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById"
>;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
@@ -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
);

View File

@@ -41,10 +41,10 @@ import {
type TAzureAdCsCertificateAuthorityFnsDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
certificateAuthorityDAL: Pick<
TCertificateAuthorityDALFactory,
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa"
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById"
>;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
@@ -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
);

View File

@@ -35,7 +35,7 @@ import {
type TCertificateAuthorityQueueFactoryDep = {
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
certificateAuthorityCrlDAL: TCertificateAuthorityCrlDALFactory;

View File

@@ -43,7 +43,7 @@ import { TCreateInternalCertificateAuthorityDTO } from "./internal/internal-cert
type TCertificateAuthorityServiceFactoryDep = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
certificateAuthorityDAL: Pick<
TCertificateAuthorityDALFactory,
| "transaction"

View File

@@ -53,6 +53,7 @@ const getValidAccessToken = async (
connection.credentials.refreshToken,
connection.id,
connection.orgId,
connection.projectId,
appConnectionDAL,
kmsService,
connection.credentials.instanceUrl

View File

@@ -32,6 +32,7 @@ const getValidAuthToken = async (
connection.credentials.refreshToken,
connection.id,
connection.orgId,
connection.projectId,
appConnectionDAL,
kmsService
);

View File

@@ -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
? {

View File

@@ -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);

View File

@@ -41,7 +41,7 @@ import { TSecretSyncQueueFactory } from "./secret-sync-queue";
type TSecretSyncServiceFactoryDep = {
secretSyncDAL: TSecretSyncDALFactory;
secretImportDAL: TSecretImportDALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">;
@@ -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 (

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 930 KiB

View File

@@ -53,7 +53,7 @@ Infisical supports the use of [Service Accounts](https://developer.1password.com
<Tab title="Infisical UI">
<Steps>
<Step title="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)
</Step>
@@ -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)
</Step>
<Step title="Connection Created">
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)
</Step>
@@ -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": "<YOUR-API-TOKEN>"
@@ -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",

View File

@@ -42,7 +42,7 @@ Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-st
<Tabs>
<Tab title="Infisical UI">
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",

View File

@@ -184,7 +184,7 @@ Infisical supports two methods for connecting to AWS.
<Step title="Setup AWS Connection in Infisical">
<Tabs>
<Tab title="Infisical UI">
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.
<Step title="Setup AWS Connection in Infisical">
<Tabs>
<Tab title="Infisical UI">
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",

View File

@@ -83,7 +83,7 @@ Infisical currently only supports two methods for connecting to Azure, which are
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -94,7 +94,7 @@ Infisical currently only supports two methods for connecting to Azure, which are
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -117,7 +117,7 @@ Infisical currently supports three methods for connecting to Azure DevOps, which
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -83,7 +83,7 @@ Infisical currently only supports two methods for connecting to Azure, which are
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -78,7 +78,7 @@ Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbuck
<Tab title="Infisical UI">
<Steps>
<Step title="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)
</Step>
@@ -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)
</Step>
<Step title="Connection Created">
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)
</Step>
@@ -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": "<YOUR-API-TOKEN>"
@@ -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",

View File

@@ -50,8 +50,7 @@ Infisical supports connecting to Camunda APIs using [client credentials](https:/
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -37,7 +37,7 @@ Infisical supports the use of [API Keys](https://app.checklyhq.com/settings/user
<Tab title="Infisical UI">
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
@@ -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)
</Step>
<Step title="Connection created">
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)
</Step>
@@ -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",

View File

@@ -88,8 +88,7 @@ Infisical supports connecting to Cloudflare using API tokens and Account ID for
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -43,8 +43,7 @@ Infisical supports the use of [service principals](https://docs.databricks.com/e
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -45,7 +45,7 @@ Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/accoun
<Tab title="Infisical UI">
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
@@ -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)
</Step>
<Step title="Connection created">
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)
</Step>
@@ -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",

View File

@@ -30,7 +30,7 @@ Infisical supports the use of [Access Tokens](https://fly.io/docs/security/token
<Tab title="Infisical UI">
<Steps>
<Step title="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)
</Step>
@@ -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)
</Step>
<Step title="Connection Created">
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)
</Step>
@@ -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",

View File

@@ -82,8 +82,7 @@ Infisical supports [service account impersonation](https://cloud.google.com/iam/
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -97,7 +97,7 @@ Infisical supports GitHub App installation for creating a GitHub Radar Connectio
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -85,7 +85,7 @@ Infisical supports two methods for connecting to GitHub.
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">
@@ -156,7 +156,7 @@ Infisical supports two methods for connecting to GitHub.
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -70,7 +70,7 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">
@@ -193,7 +193,7 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -131,7 +131,7 @@ Infisical supports two methods for connecting to Hashicorp Vault.
<Tab title="Infisical UI">
<Steps>
<Step title="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)
</Step>
@@ -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",

View File

@@ -51,7 +51,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">
@@ -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)
</Step>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -53,7 +53,7 @@ Infisical supports connecting to Humanitec using a service user.
![Humanitec Connection Created](/images/app-connections/humanitec/humanitec-user-added.png)
</Step>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -33,7 +33,7 @@ Depending on how you intend to use your LDAP connection, there may be additional
<Tabs>
<Tab title="Infisical UI">
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",

View File

@@ -62,7 +62,7 @@ Infisical supports connecting to Microsoft SQL Server using database principals.
<Tabs>
<Tab title="Infisical UI">
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",

View File

@@ -52,7 +52,7 @@ Infisical supports connecting to MySQL using a database role.
<Tabs>
<Tab title="Infisical UI">
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",

View File

@@ -35,7 +35,7 @@ Infisical supports the use of [Personal Access Tokens](https://docs.netlify.com/
<Tab title="Infisical UI">
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
@@ -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)
</Step>
<Step title="Connection created">
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)
</Step>
@@ -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",

View File

@@ -117,7 +117,7 @@ Infisical supports the use of [API Signing Key Authentication](https://docs.orac
<Tab title="Infisical UI">
<Steps>
<Step title="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)
</Step>
@@ -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)
</Step>
<Step title="Connection Created">
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)
</Step>
@@ -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",

View File

@@ -31,7 +31,7 @@ Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guide
<Tab title="Infisical UI">
<Steps>
<Step title="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)
</Step>
@@ -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)
</Step>
<Step title="Connection Created">
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)
</Step>
@@ -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": "<YOUR-API-TOKEN>"
@@ -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",

View File

@@ -62,7 +62,7 @@ Infisical supports connecting to OracleDB using a database user.
<Tabs>
<Tab title="Infisical UI">
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",

View File

@@ -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.
<Note>
App connections can also be created and managed independently in projects now.
</Note>
## 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.
<br />

View File

@@ -60,7 +60,7 @@ Infisical supports connecting to PostgreSQL using a database role.
<Tabs>
<Tab title="Infisical UI">
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",

View File

@@ -96,7 +96,7 @@ Infisical supports the use of [API Tokens](https://docs.railway.com/guides/publi
<Tab title="Infisical UI">
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
@@ -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)
</Step>
<Step title="Connection created">
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)
</Step>
@@ -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",

View File

@@ -33,8 +33,7 @@ Infisical supports connecting to Render using API keys for secure access to your
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Add Connection">

View File

@@ -34,7 +34,7 @@ Infisical supports the use of [Personal Access Tokens](https://supabase.com/dash
<Tab title="Infisical UI">
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
@@ -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)
</Step>
<Step title="Connection created">
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)
</Step>
@@ -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",

View File

@@ -51,7 +51,7 @@ Infisical supports connecting to TeamCity using Access Tokens.
<Tab title="Infisical UI">
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)
</Tab>
<Tab title="API">
@@ -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",

View File

@@ -30,7 +30,7 @@ Infisical supports connecting to Terraform Cloud using a service user.
<Step title="Add Terraform Cloud Connection in Infisical">
<Tabs>
<Tab title="Infisical UI">
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",

View File

@@ -37,7 +37,7 @@ Infisical supports connecting to Vercel using API Tokens.
<Tab title="Infisical UI">
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)
</Tab>
<Tab title="API">
@@ -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",

View File

@@ -47,7 +47,8 @@ Ensure the user generating the access token has the required role and permission
<Tab title="Infisical UI">
<Steps>
<Step title="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)
</Step>
<Step title="Add Connection">
@@ -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",

View File

@@ -31,7 +31,7 @@ Infisical supports the use of [API Tokens](https://www.zabbix.com/documentation/
<Tab title="Infisical UI">
<Steps>
<Step title="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)
</Step>
@@ -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)
</Step>
<Step title="Connection Created">
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)
</Step>
@@ -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",

View File

@@ -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<TAvailableAppConnection>) => {
const isCreateOption = props.data.id === "_create";
return (
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
{isCreateOption ? (
<div className="flex items-center gap-x-1 text-mineshaft-400">
<FontAwesomeIcon icon={faPlus} size="sm" />
<span className="mr-auto">Create New Connection</span>
</div>
) : (
<>
<p className="truncate">{children}</p>
{!props.data.projectId && (
<Tooltip content="This connection belongs to your organization.">
<div className="ml-2 mr-auto">
<Badge className="flex h-5 w-min items-center gap-1 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300 hover:text-bunker-300">
<FontAwesomeIcon icon={faBuilding} size="sm" />
Organization
</Badge>
</div>
</Tooltip>
)}
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</>
)}
</div>
</components.Option>
);
};

View File

@@ -0,0 +1 @@
export * from "./AppConnectionOption";

View File

@@ -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<T extends AbilityTuple> = {
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<TooltipProps, "children">;
} & BoundCanProps<TOrgPermission>;
I: T[0];
ability?: MongoAbility<T>;
children: ReactNode | ((isAllowed: boolean, ability: T) => ReactNode);
passThrough?: boolean;
} & (
| { an: T[1] }
| {
a: T[1];
}
);
export const OrgPermissionCan: FunctionComponent<Props> = ({
export const OrgPermissionCan: FunctionComponent<Props<OrgPermissionSet>> = ({
label = "Access restricted",
children,
passThrough = true,
@@ -41,9 +52,7 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
{(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 (

View File

@@ -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 <ProjectPermissionCan {...props}>{children}</ProjectPermissionCan>;
}
return <OrgPermissionCan {...props}>{children}</OrgPermissionCan>;
};

View File

@@ -3,3 +3,4 @@ export { GlobPermissionInfo } from "./GlobPermissionInfo";
export { OrgPermissionCan } from "./OrgPermissionCan";
export { PermissionDeniedBanner } from "./PermissionDeniedBanner";
export { ProjectPermissionCan } from "./ProjectPermissionCan";
export * from "./VariablePermissionCan";

View File

@@ -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<TSecretRotationV2Form>;
} & SharedProps;
const Content = ({ setSelectedRotation, selectedRotation, ...props }: ContentProps) => {
const Content = ({
setSelectedRotation,
selectedRotation,
initialFormData,
...props
}: ContentProps) => {
if (selectedRotation) {
return (
<SecretRotationV2Form
onCancel={() => 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<SecretRotation | null>(null);
const [initialFormData, setInitialFormData] = useState<Partial<TSecretRotationV2Form>>();
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<TSecretRotationV2Form> = {};
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 (
<Modal
isOpen={isOpen}
onOpenChange={(open) => {
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}

View File

@@ -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<TSecretRotationV2Form>();
const { permission } = useProjectPermission();
const { control, watch, setValue } = useFormContext<TSecretRotationV2Form>();
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 }
<FilterableSelect
value={value}
onChange={(newValue) => {
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 }}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{!isUpdate && availableConnections?.length === 0 && (
{!isUpdate && !isPending && !availableConnections?.length && !canCreateConnection && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {appName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
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.
</p>
)}
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => {
// 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);
}
}}
/>
</>
);
};

View File

@@ -34,6 +34,7 @@ type Props = {
environment?: string;
environments?: WorkspaceEnv[];
secretRotation?: TSecretRotationV2;
initialFormData?: Partial<TSecretRotationV2Form>;
};
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"
});

View File

@@ -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<TSecretScanningDataSourceForm>;
};
const Content = ({ setSelectedDataSource, selectedDataSource, ...props }: ContentProps) => {
@@ -41,6 +44,47 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
const [selectedDataSource, setSelectedDataSource] = useState<SecretScanningDataSource | null>(
null
);
const [initialFormData, setInitialFormData] = useState<Partial<TSecretScanningDataSourceForm>>();
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<TSecretScanningDataSourceForm> = {};
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 (
<Modal
@@ -88,6 +132,7 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
}}
selectedDataSource={selectedDataSource}
setSelectedDataSource={setSelectedDataSource}
initialFormData={initialFormData}
{...props}
/>
</ModalContent>

View File

@@ -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<TSecretScanningDataSourceForm>();
const { permission } = useProjectPermission();
const { control, watch, setValue } = useFormContext<TSecretScanningDataSourceForm>();
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 = ({
<FilterableSelect
value={value}
onChange={(newValue) => {
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 }}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{!isUpdate && availableConnections?.length === 0 && (
{!isUpdate && !isPending && !availableConnections?.length && !canCreateConnection && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {connectionName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
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.
</p>
)}
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => {
// 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);
}
}}
/>
</>
);
};

View File

@@ -27,6 +27,7 @@ type Props = {
type: SecretScanningDataSource;
onCancel: () => void;
dataSource?: TSecretScanningDataSource;
initialFormData?: Partial<TSecretScanningDataSourceForm>;
};
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"
});

View File

@@ -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<TSecretSyncForm>;
};
type ContentProps = {
onComplete: (secretSync: TSecretSync) => void;
selectedSync: SecretSync | null;
setSelectedSync: (selectedSync: SecretSync | null) => void;
initialFormData?: Partial<TSecretSyncForm>;
};
const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) => {
const Content = ({ onComplete, setSelectedSync, selectedSync, initialFormData }: ContentProps) => {
if (selectedSync) {
return (
<CreateSecretSyncForm
initialFormData={initialFormData}
onComplete={onComplete}
onCancel={() => setSelectedSync(null)}
destination={selectedSync}
@@ -33,7 +37,12 @@ const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) =>
return <SecretSyncSelect onSelect={setSelectedSync} />;
};
export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...props }: Props) => {
export const CreateSecretSyncModal = ({
onOpenChange,
selectSync = null,
initialFormData,
...props
}: Props) => {
const [selectedSync, setSelectedSync] = useState<SecretSync | null>(selectSync);
useEffect(() => {
@@ -67,6 +76,7 @@ export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...prop
}}
selectedSync={selectedSync}
setSelectedSync={setSelectedSync}
initialFormData={initialFormData}
/>
</ModalContent>
</Modal>

View File

@@ -29,6 +29,7 @@ type Props = {
onComplete: (secretSync: TSecretSync) => void;
destination: SecretSync;
onCancel: () => void;
initialFormData?: Partial<TSecretSyncForm>;
};
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<TSecretSyncForm>,
reValidateMode: "onChange"
});

View File

@@ -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<TSecretSyncForm>();
const { permission } = useProjectPermission();
const { control, watch, setValue } = useFormContext<TSecretSyncForm>();
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) => {
<FilterableSelect
value={value}
onChange={(newValue) => {
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 }}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{availableConnections?.length === 0 && (
{!isPending && !availableConnections?.length && !canCreateConnection && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {appName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
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.
</p>
)}
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => {
// 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);
}
}}
/>
</>
);
};

View File

@@ -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<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
// )
// ];
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]
| [
OrgPermissionAppConnectionActions,
(
| OrgPermissionSubjects.AppConnections
| (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
)
];
export type TOrgPermission = MongoAbility<OrgPermissionSet>;

View File

@@ -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<ProjectPermissionSub.SecretEvents> & SecretEventSubjectFields)
)
]
| [
ProjectPermissionAppConnectionActions,
(
| ProjectPermissionSub.AppConnections
| (ForcedSubject<ProjectPermissionSub.AppConnections> & AppConnectionSubjectFields)
)
];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;

View File

@@ -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;
};

View File

@@ -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) });
}
});
};

View File

@@ -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<TAppConnectionOptions>(
"/api/v1/app-connections/options"
"/api/v1/app-connections/options",
{ params: { projectType } }
);
return data.appConnectionOptions;
@@ -64,6 +72,7 @@ export const useGetAppConnectionOption = <T extends AppConnection>(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<TListAppConnections<TAppConnection>>("/api/v1/app-connections");
const { data } = await apiRequest.get<TListAppConnections<TAppConnection>>(
"/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<TAvailableAppConnectionsResponse>(
`/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 = <T extends AppConnection>(
app: T,
options?: Omit<
UseQueryOptions<
TAppConnectionMap[T][],
unknown,
TAppConnectionMap[T][],
ReturnType<typeof appConnectionKeys.listByApp>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: appConnectionKeys.listByApp(app),
queryFn: async () => {
const { data } = await apiRequest.get<TListAppConnections<TAppConnectionMap[T]>>(
`/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<typeof appConnectionKeys.getUsage>
// >,
// "queryKey" | "queryFn"
// >
// ) => {
// return useQuery({
// queryKey: appConnectionKeys.getUsage(app, connectionId),
// queryFn: async () => {
// const { data } = await apiRequest.get<AppConnectionUsage>(
// `/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 = <T extends AppConnection>(
// app: T,
// options?: Omit<
// UseQueryOptions<
// TAppConnectionMap[T][],
// unknown,
// TAppConnectionMap[T][],
// ReturnType<typeof appConnectionKeys.listByApp>
// >,
// "queryKey" | "queryFn"
// >
// ) => {
// return useQuery({
// queryKey: appConnectionKeys.listByApp(app),
// queryFn: async () => {
// const { data } = await apiRequest.get<TListAppConnections<TAppConnectionMap[T]>>(
// `/api/v1/app-connections/${app}`
// );
//
// return data.appConnections;
// },
// ...options
// });
// };
export const useGetAppConnectionById = <T extends AppConnection>(
app: T,
connectionId: string,
options?: Omit<
UseQueryOptions<
TAppConnectionMap[T],
unknown,
TAppConnectionMap[T],
ReturnType<typeof appConnectionKeys.byId>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: appConnectionKeys.byId(app, connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TGetAppConnection<TAppConnectionMap[T]>>(
`/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 = <T extends AppConnection>(
// app: T,
// connectionId: string,
// options?: Omit<
// UseQueryOptions<
// TAppConnectionMap[T],
// unknown,
// TAppConnectionMap[T],
// ReturnType<typeof appConnectionKeys.byId>
// >,
// "queryKey" | "queryFn"
// >
// ) => {
// return useQuery({
// queryKey: appConnectionKeys.byId(app, connectionId),
// queryFn: async () => {
// const { data } = await apiRequest.get<TGetAppConnection<TAppConnectionMap[T]>>(
// `/api/v1/app-connections/${app}/${connectionId}`
// );
//
// return data.appConnection;
// },
// ...options
// });
// };

View File

@@ -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<TAppConnection, "name" | "id">;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;
export type TListAppConnections<T extends TAppConnection> = { appConnections: T[] };
export type TGetAppConnection<T extends TAppConnection> = { appConnection: T };
// scott: we will need this once we have individual app connection page
// export type TGetAppConnection<T extends TAppConnection> = { 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 }>;
};
}>;
};

View File

@@ -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;
};

View File

@@ -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",

View File

@@ -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",

Some files were not shown because too many files have changed in this diff Show More