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 { Knex } from "knex";
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> { 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> { export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.AppConnection, (t) => { await dropConstraintIfExists(TableName.AppConnection, "app_connections_orgid_name_unique", knex);
t.dropUnique(["orgId", "name"]);
});
await knex.schema.alterTable(TableName.SecretSync, (t) => { await knex.schema.alterTable(TableName.SecretSync, (t) => {
t.dropUnique(["projectId", "name"]); 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(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
isPlatformManagedCredentials: z.boolean().default(false).nullable().optional(), 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>; export type TAppConnections = z.infer<typeof AppConnectionsSchema>;

View File

@@ -392,6 +392,8 @@ export enum EventType {
CREATE_APP_CONNECTION = "create-app-connection", CREATE_APP_CONNECTION = "create-app-connection",
UPDATE_APP_CONNECTION = "update-app-connection", UPDATE_APP_CONNECTION = "update-app-connection",
DELETE_APP_CONNECTION = "delete-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_SHARED_SECRET = "create-shared-secret",
CREATE_SECRET_REQUEST = "create-secret-request", CREATE_SECRET_REQUEST = "create-secret-request",
DELETE_SHARED_SECRET = "delete-shared-secret", 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 { interface CreateAppConnectionEvent {
type: EventType.CREATE_APP_CONNECTION; type: EventType.CREATE_APP_CONNECTION;
metadata: Omit<TCreateAppConnectionDTO, "credentials"> & { connectionId: string }; metadata: Omit<TCreateAppConnectionDTO, "credentials" | "projectId"> & { connectionId: string };
} }
interface UpdateAppConnectionEvent { interface UpdateAppConnectionEvent {
type: EventType.UPDATE_APP_CONNECTION; type: EventType.UPDATE_APP_CONNECTION;
metadata: Omit<TUpdateAppConnectionDTO, "credentials"> & { connectionId: string; credentialsUpdated: boolean }; metadata: Omit<TUpdateAppConnectionDTO, "credentials" | "projectId"> & {
connectionId: string;
credentialsUpdated: boolean;
};
} }
interface DeleteAppConnectionEvent { interface DeleteAppConnectionEvent {
@@ -3697,6 +3716,8 @@ export type Event =
| CreateAppConnectionEvent | CreateAppConnectionEvent
| UpdateAppConnectionEvent | UpdateAppConnectionEvent
| DeleteAppConnectionEvent | DeleteAppConnectionEvent
| GetAppConnectionUsageEvent
| MigrateAppConnectionEvent
| GetSshHostGroupEvent | GetSshHostGroupEvent
| CreateSshHostGroupEvent | CreateSshHostGroupEvent
| UpdateSshHostGroupEvent | UpdateSshHostGroupEvent

View File

@@ -2,6 +2,7 @@ import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionAppConnectionActions,
ProjectPermissionAuditLogsActions, ProjectPermissionAuditLogsActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
@@ -264,6 +265,17 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.SecretEvents ProjectPermissionSub.SecretEvents
); );
can(
[
ProjectPermissionAppConnectionActions.Create,
ProjectPermissionAppConnectionActions.Edit,
ProjectPermissionAppConnectionActions.Delete,
ProjectPermissionAppConnectionActions.Read,
ProjectPermissionAppConnectionActions.Connect
],
ProjectPermissionSub.AppConnections
);
return rules; return rules;
}; };
@@ -477,6 +489,8 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.SecretEvents ProjectPermissionSub.SecretEvents
); );
can(ProjectPermissionAppConnectionActions.Connect, ProjectPermissionSub.AppConnections);
return rules; return rules;
}; };

View File

@@ -147,6 +147,14 @@ export enum ProjectPermissionSecretScanningDataSourceActions {
ReadResources = "read-data-source-resources" 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 { export enum ProjectPermissionSecretScanningFindingActions {
Read = "read-findings", Read = "read-findings",
Update = "update-findings" Update = "update-findings"
@@ -208,7 +216,8 @@ export enum ProjectPermissionSub {
SecretScanningDataSources = "secret-scanning-data-sources", SecretScanningDataSources = "secret-scanning-data-sources",
SecretScanningFindings = "secret-scanning-findings", SecretScanningFindings = "secret-scanning-findings",
SecretScanningConfigs = "secret-scanning-configs", SecretScanningConfigs = "secret-scanning-configs",
SecretEvents = "secret-events" SecretEvents = "secret-events",
AppConnections = "app-connections"
} }
export type SecretSubjectFields = { export type SecretSubjectFields = {
@@ -272,6 +281,10 @@ export type PkiSubscriberSubjectFields = {
// (dangtony98): consider adding [commonName] as a subject field in the future // (dangtony98): consider adding [commonName] as a subject field in the future
}; };
export type AppConnectionSubjectFields = {
connectionId: string;
};
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
@@ -365,6 +378,13 @@ export type ProjectPermissionSet =
| [ | [
ProjectPermissionSecretEventActions, ProjectPermissionSecretEventActions,
ProjectPermissionSub.SecretEvents | (ForcedSubject<ProjectPermissionSub.SecretEvents> & SecretEventSubjectFields) 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 '/'"; const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
@@ -580,6 +600,21 @@ const PkiTemplateConditionSchema = z
}) })
.partial(); .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 = [ const GeneralPermissionSchema = [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), 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( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretScanningConfigActions).describe(
"Describe what action an entity can take." "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({ const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials, credentials: updatedCredentials,
orgId, orgId,
kmsService kmsService,
projectId: connection.projectId
}); });
await appConnectionDAL.updateById(connection.id, { encryptedCredentials }); await appConnectionDAL.updateById(connection.id, { encryptedCredentials });

View File

@@ -52,6 +52,7 @@ const baseSecretRotationV2Query = ({
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"), db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"), 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("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
db db
@@ -106,6 +107,7 @@ const expandSecretRotation = <T extends Awaited<ReturnType<typeof baseSecretRota
connectionUpdatedAt, connectionUpdatedAt,
connectionVersion, connectionVersion,
connectionGatewayId, connectionGatewayId,
connectionProjectId,
connectionIsPlatformManagedCredentials, connectionIsPlatformManagedCredentials,
...el ...el
} = secretRotation; } = secretRotation;
@@ -126,6 +128,7 @@ const expandSecretRotation = <T extends Awaited<ReturnType<typeof baseSecretRota
updatedAt: connectionUpdatedAt, updatedAt: connectionUpdatedAt,
version: connectionVersion, version: connectionVersion,
gatewayId: connectionGatewayId, gatewayId: connectionGatewayId,
projectId: connectionProjectId,
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials isPlatformManagedCredentials: connectionIsPlatformManagedCredentials
}, },
folder: { folder: {

View File

@@ -89,7 +89,7 @@ import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
export type TSecretRotationV2ServiceFactoryDep = { export type TSecretRotationV2ServiceFactoryDep = {
secretRotationV2DAL: TSecretRotationV2DALFactory; secretRotationV2DAL: TSecretRotationV2DALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">; appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
@@ -459,7 +459,11 @@ export const secretRotationV2ServiceFactory = ({
const typeApp = SECRET_ROTATION_CONNECTION_MAP[payload.type]; const typeApp = SECRET_ROTATION_CONNECTION_MAP[payload.type];
// validates permission to connect and app is valid for rotation 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]( 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("description").withSchema(TableName.AppConnection).as("connectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"), 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("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
db db
@@ -84,6 +85,7 @@ const expandSecretScanningDataSource = <
connectionVersion, connectionVersion,
connectionIsPlatformManagedCredentials, connectionIsPlatformManagedCredentials,
connectionGatewayId, connectionGatewayId,
connectionProjectId,
...el ...el
} = dataSource; } = dataSource;
@@ -103,7 +105,8 @@ const expandSecretScanningDataSource = <
updatedAt: connectionUpdatedAt, updatedAt: connectionUpdatedAt,
version: connectionVersion, version: connectionVersion,
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials, isPlatformManagedCredentials: connectionIsPlatformManagedCredentials,
gatewayId: connectionGatewayId gatewayId: connectionGatewayId,
projectId: connectionProjectId
} }
: undefined : undefined
}; };

View File

@@ -60,7 +60,7 @@ import { TSecretScanningV2QueueServiceFactory } from "./secret-scanning-v2-queue
export type TSecretScanningV2ServiceFactoryDep = { export type TSecretScanningV2ServiceFactoryDep = {
secretScanningV2DAL: TSecretScanningV2DALFactory; secretScanningV2DAL: TSecretScanningV2DALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">; appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">; appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -252,9 +252,9 @@ export const secretScanningV2ServiceFactory = ({
let connection: TAppConnection | null = null; let connection: TAppConnection | null = null;
if (payload.connectionId) { if (payload.connectionId) {
// validates permission to connect and app is valid for data source // 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], SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[payload.type],
payload.connectionId, { connectionId: payload.connectionId, projectId: payload.projectId },
actor actor
); );
} }
@@ -373,9 +373,9 @@ export const secretScanningV2ServiceFactory = ({
let connection: TAppConnection | null = null; let connection: TAppConnection | null = null;
if (dataSource.connectionId) { if (dataSource.connectionId) {
// validates permission to connect and app is valid for data source // 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], SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSource.type],
dataSource.connectionId, { connectionId: dataSource.connectionId, projectId: dataSource.projectId },
actor actor
); );
} }

View File

@@ -2185,11 +2185,15 @@ export const CertificateAuthorities = {
}; };
export const AppConnections = { 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) => ({ GET_BY_ID: (app: AppConnection) => ({
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.` connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
}), }),
GET_BY_NAME: (app: AppConnection) => ({ 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) => { CREATE: (app: AppConnection) => {
const appName = APP_CONNECTION_NAME_MAP[app]; const appName = APP_CONNECTION_NAME_MAP[app];
@@ -2198,7 +2202,8 @@ export const AppConnections = {
description: `An optional description for the ${appName} Connection.`, description: `An optional description for the ${appName} Connection.`,
credentials: `The credentials used to connect with ${appName}.`, credentials: `The credentials used to connect with ${appName}.`,
method: `The method used to authenticate 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) => { UPDATE: (app: AppConnection) => {

View File

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

View File

@@ -26,6 +26,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
description?: string | null; description?: string | null;
isPlatformManagedCredentials?: boolean; isPlatformManagedCredentials?: boolean;
gatewayId?: string | null; gatewayId?: string | null;
projectId?: string;
}>; }>;
updateSchema: z.ZodType<{ updateSchema: z.ZodType<{
name?: string; name?: string;
@@ -47,18 +48,27 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AppConnections], 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: { response: {
200: z.object({ appConnections: sanitizedResponseSchema.array() }) 200: z.object({ appConnections: sanitizedResponseSchema.array() })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId,
event: { event: {
type: EventType.GET_APP_CONNECTIONS, type: EventType.GET_APP_CONNECTIONS,
metadata: { metadata: {
@@ -82,14 +92,19 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AppConnections], 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: { response: {
200: z.object({ 200: z.object({
appConnections: z appConnections: z
.object({ .object({
app: z.literal(app), app: z.literal(app),
name: z.string(), name: z.string(),
id: z.string().uuid() id: z.string().uuid(),
projectId: z.string().nullish(),
orgId: z.string()
}) })
.array() .array()
}) })
@@ -97,14 +112,17 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { projectId } = req.query;
const appConnections = await server.services.appConnection.listAvailableAppConnectionsForUser( const appConnections = await server.services.appConnection.listAvailableAppConnectionsForUser(
app, app,
req.permission req.permission,
projectId
); );
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId,
event: { event: {
type: EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS, type: EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS,
metadata: { metadata: {
@@ -149,6 +167,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId: appConnection.projectId ?? undefined,
event: { event: {
type: EventType.GET_APP_CONNECTION, type: EventType.GET_APP_CONNECTION,
metadata: { metadata: {
@@ -178,6 +197,9 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
.min(1, "Connection name required") .min(1, "Connection name required")
.describe(AppConnections.GET_BY_NAME(app).connectionName) .describe(AppConnections.GET_BY_NAME(app).connectionName)
}), }),
querystring: z.object({
projectId: z.string().trim().optional().describe(AppConnections.GET_BY_NAME(app).projectId)
}),
response: { response: {
200: z.object({ appConnection: sanitizedResponseSchema }) 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]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { connectionName } = req.params; const { connectionName } = req.params;
const { projectId } = req.query;
const appConnection = (await server.services.appConnection.findAppConnectionByName( const appConnection = (await server.services.appConnection.findAppConnectionByName(
app, app,
connectionName, {
connectionName,
projectId
},
req.permission req.permission
)) as T; )) as T;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId: appConnection.projectId ?? undefined,
event: { event: {
type: EventType.GET_APP_CONNECTION, type: EventType.GET_APP_CONNECTION,
metadata: { metadata: {
@@ -216,9 +243,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AppConnections], tags: [ApiDocsTags.AppConnections],
description: `Create ${ description: `Create ${startsWithVowel(appName) ? "an" : "a"} ${appName} Connection.`,
startsWithVowel(appName) ? "an" : "a"
} ${appName} Connection for the current organization.`,
body: createSchema, body: createSchema,
response: { response: {
200: z.object({ appConnection: sanitizedResponseSchema }) 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]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { 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( const appConnection = (await server.services.appConnection.createAppConnection(
{ name, method, app, credentials, description, isPlatformManagedCredentials, gatewayId }, { name, method, app, credentials, description, isPlatformManagedCredentials, gatewayId, projectId },
req.permission req.permission
)) as T; )) as T;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId,
event: { event: {
type: EventType.CREATE_APP_CONNECTION, type: EventType.CREATE_APP_CONNECTION,
metadata: { metadata: {
@@ -283,6 +309,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId: appConnection.projectId ?? undefined,
event: { event: {
type: EventType.UPDATE_APP_CONNECTION, type: EventType.UPDATE_APP_CONNECTION,
metadata: { metadata: {
@@ -329,6 +356,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId: appConnection.projectId ?? undefined,
event: { event: {
type: EventType.DELETE_APP_CONNECTION, type: EventType.DELETE_APP_CONNECTION,
metadata: { metadata: {
@@ -340,4 +368,81 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
return { appConnection }; 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 { z } from "zod";
import { ProjectType } from "@app/db/schemas";
import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/ee/services/app-connections/oci"; import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/ee/services/app-connections/oci";
import { import {
OracleDBConnectionListItemSchema, OracleDBConnectionListItemSchema,
SanitizedOracleDBConnectionSchema SanitizedOracleDBConnectionSchema
} from "@app/ee/services/app-connections/oracledb"; } from "@app/ee/services/app-connections/oracledb";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; 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 { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { import {
@@ -210,6 +211,9 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
hide: false, hide: false,
tags: [ApiDocsTags.AppConnections], tags: [ApiDocsTags.AppConnections],
description: "List the available App Connection Options.", description: "List the available App Connection Options.",
querystring: z.object({
projectType: z.nativeEnum(ProjectType).optional()
}),
response: { response: {
200: z.object({ 200: z.object({
appConnectionOptions: AppConnectionOptionsSchema.array() appConnectionOptions: AppConnectionOptionsSchema.array()
@@ -217,8 +221,8 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: () => { handler: (req) => {
const appConnectionOptions = server.services.appConnection.listAppConnectionOptions(); const appConnectionOptions = server.services.appConnection.listAppConnectionOptions(req.query.projectType);
return { appConnectionOptions }; return { appConnectionOptions };
} }
}); });
@@ -232,18 +236,27 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AppConnections], 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: { response: {
200: z.object({ appConnections: SanitizedAppConnectionSchema.array() }) 200: z.object({ appConnections: SanitizedAppConnectionSchema.array() })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId,
event: { event: {
type: EventType.GET_APP_CONNECTIONS, type: EventType.GET_APP_CONNECTIONS,
metadata: { metadata: {

View File

@@ -1,11 +1,115 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName, TAppConnections } from "@app/db/schemas";
import { ormify } from "@app/lib/knex"; 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>; export type TAppConnectionDALFactory = ReturnType<typeof appConnectionDALFactory>;
type AppConnectionFindFilter = Parameters<typeof buildFindFilter<TAppConnections>>[0];
export const appConnectionDALFactory = (db: TDbClient) => { export const appConnectionDALFactory = (db: TDbClient) => {
const appConnectionOrm = ormify(db, TableName.AppConnection); 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 { TAppConnections } from "@app/db/schemas/app-connections";
import { import {
getOCIConnectionListItem, getOCIConnectionListItem,
@@ -8,6 +9,8 @@ import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-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 { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { APP_CONNECTION_NAME_MAP, APP_CONNECTION_PLAN_MAP } from "@app/services/app-connection/app-connection-maps"; import { APP_CONNECTION_NAME_MAP, APP_CONNECTION_PLAN_MAP } from "@app/services/app-connection/app-connection-maps";
@@ -16,6 +19,7 @@ import {
validateSqlConnectionCredentials validateSqlConnectionCredentials
} from "@app/services/app-connection/shared/sql"; } from "@app/services/app-connection/shared/sql";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { import {
getOnePassConnectionListItem, getOnePassConnectionListItem,
@@ -133,7 +137,19 @@ import {
} from "./windmill"; } from "./windmill";
import { getZabbixConnectionListItem, validateZabbixConnectionCredentials, ZabbixConnectionMethod } from "./zabbix"; 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 [ return [
getAwsConnectionListItem(), getAwsConnectionListItem(),
getGitHubConnectionListItem(), getGitHubConnectionListItem(),
@@ -173,22 +189,55 @@ export const listAppConnectionOptions = () => {
getDigitalOceanConnectionListItem(), getDigitalOceanConnectionListItem(),
getNetlifyConnectionListItem(), getNetlifyConnectionListItem(),
getOktaConnectionListItem() 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 ({ export const encryptAppConnectionCredentials = async ({
orgId, orgId,
credentials, credentials,
kmsService kmsService,
projectId
}: { }: {
orgId: string; orgId: string;
credentials: TAppConnection["credentials"]; credentials: TAppConnection["credentials"];
kmsService: TAppConnectionServiceFactoryDep["kmsService"]; kmsService: TAppConnectionServiceFactoryDep["kmsService"];
projectId: string | null | undefined;
}) => { }) => {
const { encryptor } = await kmsService.createCipherPairWithDataKey({ const { encryptor } = await kmsService.createCipherPairWithDataKey(
type: KmsDataKey.Organization, projectId
orgId ? {
}); type: KmsDataKey.SecretManager,
projectId
}
: {
type: KmsDataKey.Organization,
orgId
}
);
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({ const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
plainText: Buffer.from(JSON.stringify(credentials)) plainText: Buffer.from(JSON.stringify(credentials))
@@ -200,16 +249,22 @@ export const encryptAppConnectionCredentials = async ({
export const decryptAppConnectionCredentials = async ({ export const decryptAppConnectionCredentials = async ({
orgId, orgId,
encryptedCredentials, encryptedCredentials,
kmsService kmsService,
projectId
}: { }: {
orgId: string; orgId: string;
encryptedCredentials: Buffer; encryptedCredentials: Buffer;
kmsService: TAppConnectionServiceFactoryDep["kmsService"]; kmsService: TAppConnectionServiceFactoryDep["kmsService"];
projectId: string | null | undefined;
}) => { }) => {
const { decryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor } = await kmsService.createCipherPairWithDataKey(
type: KmsDataKey.Organization, projectId
orgId ? { type: KmsDataKey.SecretManager, projectId }
}); : {
type: KmsDataKey.Organization,
orgId
}
);
const decryptedPlainTextBlob = decryptor({ const decryptedPlainTextBlob = decryptor({
cipherTextBlob: encryptedCredentials cipherTextBlob: encryptedCredentials
@@ -343,6 +398,7 @@ export const decryptAppConnection = async (
credentials: await decryptAppConnectionCredentials({ credentials: await decryptAppConnectionCredentials({
encryptedCredentials: appConnection.encryptedCredentials, encryptedCredentials: appConnection.encryptedCredentials,
orgId: appConnection.orgId, orgId: appConnection.orgId,
projectId: appConnection.projectId,
kmsService kmsService
}), }),
credentialsHash: crypto.nativeCrypto.createHash("sha256").update(appConnection.encryptedCredentials).digest("hex") 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, app: true,
method: true method: true
}).extend({ }).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 = ( export const GenericCreateAppConnectionFieldsSchema = (
@@ -28,6 +36,7 @@ export const GenericCreateAppConnectionFieldsSchema = (
.max(256, "Description cannot exceed 256 characters") .max(256, "Description cannot exceed 256 characters")
.nullish() .nullish()
.describe(AppConnections.CREATE(app).description), .describe(AppConnections.CREATE(app).description),
projectId: z.string().optional().describe(AppConnections.CREATE(app).projectId),
isPlatformManagedCredentials: supportsPlatformManagedCredentials isPlatformManagedCredentials: supportsPlatformManagedCredentials
? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials) ? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials)
: z : z

View File

@@ -1,5 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, TAppConnections } from "@app/db/schemas";
import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci"; import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci";
import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service"; import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service";
import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb";
@@ -14,6 +15,10 @@ import {
OrgPermissionSubjects OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission"; } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; 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 { crypto } from "@app/lib/crypto/cryptography";
import { DatabaseErrorCode } from "@app/lib/error-codes"; import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
@@ -27,9 +32,8 @@ import {
TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM, TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM,
validateAppConnectionCredentials validateAppConnectionCredentials
} from "@app/services/app-connection/app-connection-fns"; } 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 { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { ValidateOnePassConnectionCredentialsSchema } from "./1password"; import { ValidateOnePassConnectionCredentialsSchema } from "./1password";
import { onePassConnectionService } from "./1password/1password-connection-service"; import { onePassConnectionService } from "./1password/1password-connection-service";
@@ -41,10 +45,13 @@ import {
TAppConnectionConfig, TAppConnectionConfig,
TAppConnectionRaw, TAppConnectionRaw,
TCreateAppConnectionDTO, TCreateAppConnectionDTO,
TGetAppConnectionByNameDTO,
TUpdateAppConnectionDTO, TUpdateAppConnectionDTO,
TValidateAppConnectionCredentialsSchema TValidateAppConnectionCredentialsSchema,
TValidateAppConnectionUsageByIdDTO
} from "./app-connection-types"; } from "./app-connection-types";
import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0"; import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0";
import { auth0ConnectionService } from "./auth0/auth0-connection-service";
import { ValidateAwsConnectionCredentialsSchema } from "./aws"; import { ValidateAwsConnectionCredentialsSchema } from "./aws";
import { awsConnectionService } from "./aws/aws-connection-service"; import { awsConnectionService } from "./aws/aws-connection-service";
import { ValidateAzureADCSConnectionCredentialsSchema } from "./azure-adcs/azure-adcs-connection-schemas"; 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 { ValidateGitHubConnectionCredentialsSchema } from "./github";
import { githubConnectionService } from "./github/github-connection-service"; import { githubConnectionService } from "./github/github-connection-service";
import { ValidateGitHubRadarConnectionCredentialsSchema } from "./github-radar"; import { ValidateGitHubRadarConnectionCredentialsSchema } from "./github-radar";
import { githubRadarConnectionService } from "./github-radar/github-radar-connection-service";
import { ValidateGitLabConnectionCredentialsSchema } from "./gitlab"; import { ValidateGitLabConnectionCredentialsSchema } from "./gitlab";
import { gitlabConnectionService } from "./gitlab/gitlab-connection-service"; import { gitlabConnectionService } from "./gitlab/gitlab-connection-service";
import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault"; import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault";
@@ -108,13 +116,14 @@ import { zabbixConnectionService } from "./zabbix/zabbix-connection-service";
export type TAppConnectionServiceFactoryDep = { export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory; appConnectionDAL: TAppConnectionDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">; gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">; gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
gatewayDAL: Pick<TGatewayDALFactory, "find">; gatewayDAL: Pick<TGatewayDALFactory, "find">;
gatewayV2DAL: Pick<TGatewayV2DALFactory, "find">; gatewayV2DAL: Pick<TGatewayV2DALFactory, "find">;
projectDAL: Pick<TProjectDALFactory, "findProjectById">;
}; };
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>; export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
@@ -168,29 +177,64 @@ export const appConnectionServiceFactory = ({
gatewayService, gatewayService,
gatewayV2Service, gatewayV2Service,
gatewayDAL, gatewayDAL,
gatewayV2DAL gatewayV2DAL,
projectDAL
}: TAppConnectionServiceFactoryDep) => { }: TAppConnectionServiceFactoryDep) => {
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => { const listAppConnections = async (actor: OrgServiceActor, app?: AppConnection, projectId?: string) => {
const { permission } = await permissionService.getOrgPermission( let appConnections: TAppConnections[];
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan( if (projectId) {
OrgPermissionAppConnectionActions.Read, const { permission } = await permissionService.getProjectPermission({
OrgPermissionSubjects.AppConnections actor: actor.type,
); actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.Any
});
const appConnections = await appConnectionDAL.find( ForbiddenError.from(permission).throwUnlessCan(
app ProjectPermissionAppConnectionActions.Read,
? { orgId: actor.orgId, app } ProjectPermissionSub.AppConnections
: { );
orgId: actor.orgId
} 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( return Promise.all(
appConnections appConnections
@@ -204,18 +248,34 @@ export const appConnectionServiceFactory = ({
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
const { permission } = await permissionService.getOrgPermission( if (appConnection.projectId) {
actor.type, const { permission } = await permissionService.getProjectPermission({
actor.id, actor: actor.type,
actor.orgId, actorId: actor.id,
actor.authMethod, projectId: appConnection.projectId,
appConnection.orgId actorAuthMethod: actor.authMethod,
); actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Read, ProjectPermissionAppConnectionActions.Read,
OrgPermissionSubjects.AppConnections 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) if (appConnection.app !== app)
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for 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); return decryptAppConnection(appConnection, kmsService);
}; };
const findAppConnectionByName = async (app: AppConnection, connectionName: string, actor: OrgServiceActor) => { const findAppConnectionByName = async (
const appConnection = await appConnectionDAL.findOne({ name: connectionName, orgId: actor.orgId }); app: AppConnection,
{ connectionName, projectId }: TGetAppConnectionByNameDTO,
actor: OrgServiceActor
) => {
const appConnection = await appConnectionDAL.findOne({
name: connectionName,
...(projectId ? { projectId } : { orgId: actor.orgId, projectId: null })
});
if (!appConnection) 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( if (appConnection.projectId) {
actor.type, const { permission } = await permissionService.getProjectPermission({
actor.id, actor: actor.type,
actor.orgId, actorId: actor.id,
actor.authMethod, projectId: appConnection.projectId,
appConnection.orgId actorAuthMethod: actor.authMethod,
); actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Read, ProjectPermissionAppConnectionActions.Read,
OrgPermissionSubjects.AppConnections 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) if (appConnection.app !== app)
throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for 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 ( const createAppConnection = async (
{ method, app, credentials, gatewayId, ...params }: TCreateAppConnectionDTO, { method, app, credentials, gatewayId, projectId, ...params }: TCreateAppConnectionDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const { permission } = await permissionService.getOrgPermission( const { permission: orgPermission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
actor.orgId, actor.orgId,
@@ -260,13 +345,33 @@ export const appConnectionServiceFactory = ({
actor.orgId actor.orgId
); );
ForbiddenError.from(permission).throwUnlessCan( if (projectId) {
OrgPermissionAppConnectionActions.Create, const project = await projectDAL.findProjectById(projectId);
OrgPermissionSubjects.AppConnections
); 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) { if (gatewayId) {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways, OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway OrgPermissionSubjects.Gateway
); );
@@ -304,7 +409,8 @@ export const appConnectionServiceFactory = ({
const encryptedCredentials = await encryptAppConnectionCredentials({ const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: connectionCredentials, credentials: connectionCredentials,
orgId: actor.orgId, orgId: actor.orgId,
kmsService kmsService,
projectId
}); });
return appConnectionDAL.create({ return appConnectionDAL.create({
@@ -313,6 +419,7 @@ export const appConnectionServiceFactory = ({
method, method,
app, app,
gatewayId, gatewayId,
projectId,
...params ...params
}); });
}; };
@@ -365,7 +472,7 @@ export const appConnectionServiceFactory = ({
"Failed to update app connection due to plan restriction. Upgrade plan to access enterprise app connections." "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.type,
actor.id, actor.id,
actor.orgId, actor.orgId,
@@ -373,13 +480,29 @@ export const appConnectionServiceFactory = ({
appConnection.orgId appConnection.orgId
); );
ForbiddenError.from(permission).throwUnlessCan( if (appConnection.projectId) {
OrgPermissionAppConnectionActions.Edit, const { permission } = await permissionService.getProjectPermission({
OrgPermissionSubjects.AppConnections 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( 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, OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway OrgPermissionSubjects.Gateway
); );
@@ -441,7 +564,8 @@ export const appConnectionServiceFactory = ({
? await encryptAppConnectionCredentials({ ? await encryptAppConnectionCredentials({
credentials: connectionCredentials, credentials: connectionCredentials,
orgId: actor.orgId, orgId: actor.orgId,
kmsService kmsService,
projectId: appConnection.projectId
}) })
: undefined; : undefined;
@@ -491,18 +615,34 @@ export const appConnectionServiceFactory = ({
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
const { permission } = await permissionService.getOrgPermission( if (appConnection.projectId) {
actor.type, const { permission } = await permissionService.getProjectPermission({
actor.id, actor: actor.type,
actor.orgId, actorId: actor.id,
actor.authMethod, projectId: appConnection.projectId,
appConnection.orgId actorAuthMethod: actor.authMethod,
); actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Delete, ProjectPermissionAppConnectionActions.Delete,
OrgPermissionSubjects.AppConnections 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) if (appConnection.app !== app)
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for 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." "Failed to connect app due to plan restriction. Upgrade plan to access enterprise app connections."
); );
const { permission: orgPermission } = await permissionService.getOrgPermission( if (appConnection.projectId) {
actor.type, const { permission } = await permissionService.getProjectPermission({
actor.id, actor: actor.type,
appConnection.orgId, actorId: actor.id,
actor.authMethod, projectId: appConnection.projectId,
actor.orgId actorAuthMethod: actor.authMethod,
); actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(orgPermission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Connect, ProjectPermissionAppConnectionActions.Connect,
subject(OrgPermissionSubjects.AppConnections, { connectionId: appConnection.id }) 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) if (appConnection.app !== app)
throw new BadRequestError({ throw new BadRequestError({
@@ -569,7 +725,41 @@ export const appConnectionServiceFactory = ({
return connection as T; 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( const { permission: orgPermission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
@@ -578,28 +768,67 @@ export const appConnectionServiceFactory = ({
actor.orgId 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( orgPermission.can(
OrgPermissionAppConnectionActions.Connect, OrgPermissionAppConnectionActions.Connect,
subject(OrgPermissionSubjects.AppConnections, { connectionId: connection.id }) 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 { return {
listAppConnectionOptions, listAppConnectionOptions,
listAppConnectionsByOrg, listAppConnections,
findAppConnectionById, findAppConnectionById,
findAppConnectionByName, findAppConnectionByName,
createAppConnection, createAppConnection,
updateAppConnection, updateAppConnection,
deleteAppConnection, deleteAppConnection,
connectAppConnectionById, connectAppConnectionById,
validateAppConnectionUsageById,
listAvailableAppConnectionsForUser, listAvailableAppConnectionsForUser,
findAppConnectionUsageById,
github: githubConnectionService(connectAppConnectionById, gatewayService, gatewayV2Service), github: githubConnectionService(connectAppConnectionById, gatewayService, gatewayV2Service),
githubRadar: githubRadarConnectionService(connectAppConnectionById), githubRadar: githubRadarConnectionService(connectAppConnectionById),
gcp: gcpConnectionService(connectAppConnectionById), gcp: gcpConnectionService(connectAppConnectionById),

View File

@@ -316,13 +316,23 @@ export type TSqlConnectionInput =
export type TCreateAppConnectionDTO = Pick< export type TCreateAppConnectionDTO = Pick<
TAppConnectionInput, 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; connectionId: string;
}; };
export type TGetAppConnectionByNameDTO = {
connectionName: string;
projectId?: string;
};
export type TValidateAppConnectionUsageByIdDTO = {
connectionId: string;
projectId: string;
};
export type TAppConnectionConfig = export type TAppConnectionConfig =
| TAwsConnectionConfig | TAwsConnectionConfig
| TGitHubConnectionConfig | TGitHubConnectionConfig

View File

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

View File

@@ -352,7 +352,8 @@ export const getAzureADCSConnectionCredentials = async (
const credentials = (await decryptAppConnectionCredentials({ const credentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService, kmsService,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials,
projectId: appConnection.projectId
})) as { })) as {
username: string; username: string;
password: string; password: string;

View File

@@ -57,6 +57,7 @@ export const getAzureConnectionAccessToken = async (
const credentials = (await decryptAppConnectionCredentials({ const credentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService, kmsService,
projectId: appConnection.projectId,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials
})) as TAzureClientSecretsConnectionCredentials; })) as TAzureClientSecretsConnectionCredentials;
@@ -93,6 +94,7 @@ export const getAzureConnectionAccessToken = async (
const encryptedCredentials = await encryptAppConnectionCredentials({ const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials, credentials: updatedCredentials,
orgId: appConnection.orgId, orgId: appConnection.orgId,
projectId: appConnection.projectId,
kmsService kmsService
}); });
@@ -102,6 +104,7 @@ export const getAzureConnectionAccessToken = async (
case AzureClientSecretsConnectionMethod.ClientSecret: case AzureClientSecretsConnectionMethod.ClientSecret:
const accessTokenCredentials = (await decryptAppConnectionCredentials({ const accessTokenCredentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
projectId: appConnection.projectId,
kmsService, kmsService,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials
})) as TAzureClientSecretsConnectionClientSecretCredentials; })) as TAzureClientSecretsConnectionClientSecretCredentials;
@@ -129,6 +132,7 @@ export const getAzureConnectionAccessToken = async (
const encryptedClientCredentials = await encryptAppConnectionCredentials({ const encryptedClientCredentials = await encryptAppConnectionCredentials({
credentials: updatedClientCredentials, credentials: updatedClientCredentials,
orgId: appConnection.orgId, orgId: appConnection.orgId,
projectId: appConnection.projectId,
kmsService kmsService
}); });

View File

@@ -70,7 +70,8 @@ export const getAzureDevopsConnection = async (
const oauthCredentials = (await decryptAppConnectionCredentials({ const oauthCredentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService, kmsService,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials,
projectId: appConnection.projectId
})) as TAzureDevOpsConnectionCredentials; })) as TAzureDevOpsConnectionCredentials;
if (!("refreshToken" in oauthCredentials)) { if (!("refreshToken" in oauthCredentials)) {
@@ -100,7 +101,8 @@ export const getAzureDevopsConnection = async (
const encryptedOAuthCredentials = await encryptAppConnectionCredentials({ const encryptedOAuthCredentials = await encryptAppConnectionCredentials({
credentials: updatedOAuthCredentials, credentials: updatedOAuthCredentials,
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService kmsService,
projectId: appConnection.projectId
}); });
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials }); await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials });
@@ -111,7 +113,8 @@ export const getAzureDevopsConnection = async (
const accessTokenCredentials = (await decryptAppConnectionCredentials({ const accessTokenCredentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService, kmsService,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials,
projectId: appConnection.projectId
})) as { accessToken: string }; })) as { accessToken: string };
if (!("accessToken" in accessTokenCredentials)) { if (!("accessToken" in accessTokenCredentials)) {
@@ -124,7 +127,8 @@ export const getAzureDevopsConnection = async (
const clientSecretCredentials = (await decryptAppConnectionCredentials({ const clientSecretCredentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService, kmsService,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials,
projectId: appConnection.projectId
})) as TAzureDevOpsConnectionClientSecretCredentials; })) as TAzureDevOpsConnectionClientSecretCredentials;
const { accessToken, expiresAt, clientId, clientSecret, tenantId: clientTenantId } = clientSecretCredentials; const { accessToken, expiresAt, clientId, clientSecret, tenantId: clientTenantId } = clientSecretCredentials;
@@ -153,7 +157,8 @@ export const getAzureDevopsConnection = async (
const encryptedClientCredentials = await encryptAppConnectionCredentials({ const encryptedClientCredentials = await encryptAppConnectionCredentials({
credentials: updatedClientCredentials, credentials: updatedClientCredentials,
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService kmsService,
projectId: appConnection.projectId
}); });
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials }); await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials });

View File

@@ -58,7 +58,8 @@ export const getAzureConnectionAccessToken = async (
const oauthCredentials = (await decryptAppConnectionCredentials({ const oauthCredentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService, kmsService,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials,
projectId: appConnection.projectId
})) as TAzureKeyVaultConnectionCredentials; })) as TAzureKeyVaultConnectionCredentials;
const { data } = await request.post<ExchangeCodeAzureResponse>( const { data } = await request.post<ExchangeCodeAzureResponse>(
@@ -82,7 +83,8 @@ export const getAzureConnectionAccessToken = async (
const encryptedOAuthCredentials = await encryptAppConnectionCredentials({ const encryptedOAuthCredentials = await encryptAppConnectionCredentials({
credentials: updatedOAuthCredentials, credentials: updatedOAuthCredentials,
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService kmsService,
projectId: appConnection.projectId
}); });
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials }); await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials });
@@ -95,7 +97,8 @@ export const getAzureConnectionAccessToken = async (
const clientSecretCredentials = (await decryptAppConnectionCredentials({ const clientSecretCredentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService, kmsService,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials,
projectId: appConnection.projectId
})) as TAzureKeyVaultConnectionClientSecretCredentials; })) as TAzureKeyVaultConnectionClientSecretCredentials;
const { accessToken, expiresAt, clientId, clientSecret, tenantId } = clientSecretCredentials; const { accessToken, expiresAt, clientId, clientSecret, tenantId } = clientSecretCredentials;
@@ -124,7 +127,8 @@ export const getAzureConnectionAccessToken = async (
const encryptedClientCredentials = await encryptAppConnectionCredentials({ const encryptedClientCredentials = await encryptAppConnectionCredentials({
credentials: updatedClientCredentials, credentials: updatedClientCredentials,
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService kmsService,
projectId: appConnection.projectId
}); });
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials }); await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials });

View File

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

View File

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

View File

@@ -64,6 +64,7 @@ export const refreshGitLabToken = async (
refreshToken: string, refreshToken: string,
appId: string, appId: string,
orgId: string, orgId: string,
projectId: string | undefined | null,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">, appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">, kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">,
instanceUrl?: string instanceUrl?: string
@@ -105,7 +106,8 @@ export const refreshGitLabToken = async (
expiresAt expiresAt
}, },
orgId, orgId,
kmsService kmsService,
projectId
}); });
await appConnectionDAL.updateById(appId, { encryptedCredentials }); await appConnectionDAL.updateById(appId, { encryptedCredentials });
@@ -238,6 +240,7 @@ export const getGitLabConnectionClient = async (
appConnection.credentials.refreshToken, appConnection.credentials.refreshToken,
appConnection.id, appConnection.id,
appConnection.orgId, appConnection.orgId,
appConnection.projectId,
appConnectionDAL, appConnectionDAL,
kmsService, kmsService,
appConnection.credentials.instanceUrl appConnection.credentials.instanceUrl
@@ -273,6 +276,7 @@ export const listGitLabProjects = async ({
appConnection.credentials.refreshToken, appConnection.credentials.refreshToken,
appConnection.id, appConnection.id,
appConnection.orgId, appConnection.orgId,
appConnection.projectId,
appConnectionDAL, appConnectionDAL,
kmsService, kmsService,
appConnection.credentials.instanceUrl appConnection.credentials.instanceUrl
@@ -341,6 +345,7 @@ export const listGitLabGroups = async ({
appConnection.credentials.refreshToken, appConnection.credentials.refreshToken,
appConnection.id, appConnection.id,
appConnection.orgId, appConnection.orgId,
appConnection.projectId,
appConnectionDAL, appConnectionDAL,
kmsService, kmsService,
appConnection.credentials.instanceUrl appConnection.credentials.instanceUrl

View File

@@ -36,6 +36,7 @@ export const refreshHerokuToken = async (
refreshToken: string, refreshToken: string,
appId: string, appId: string,
orgId: string, orgId: string,
projectId: string | null | undefined,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">, appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey"> kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<string> => { ): Promise<string> => {
@@ -64,7 +65,8 @@ export const refreshHerokuToken = async (
expiresAt: new Date(Date.now() + data.expires_in * 1000 - 60000) expiresAt: new Date(Date.now() + data.expires_in * 1000 - 60000)
}, },
orgId, orgId,
kmsService kmsService,
projectId
}); });
await appConnectionDAL.updateById(appId, { encryptedCredentials }); await appConnectionDAL.updateById(appId, { encryptedCredentials });
@@ -186,6 +188,7 @@ export const listHerokuApps = async ({
appConnection.credentials.refreshToken, appConnection.credentials.refreshToken,
appConnection.id, appConnection.id,
appConnection.orgId, appConnection.orgId,
appConnection.projectId,
appConnectionDAL, appConnectionDAL,
kmsService kmsService
); );

View File

@@ -42,10 +42,10 @@ import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/
type TAcmeCertificateAuthorityFnsDeps = { type TAcmeCertificateAuthorityFnsDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">; appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">; appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
certificateAuthorityDAL: Pick< certificateAuthorityDAL: Pick<
TCertificateAuthorityDALFactory, TCertificateAuthorityDALFactory,
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" "create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById"
>; >;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">; externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">; certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
@@ -152,7 +152,11 @@ export const AcmeCertificateAuthorityFns = ({
} }
// validates permission to connect // 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) => { const caEntity = await certificateAuthorityDAL.transaction(async (tx) => {
try { 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 // validates permission to connect
await appConnectionService.connectAppConnectionById( await appConnectionService.validateAppConnectionUsageById(
appConnection.app as AppConnection, appConnection.app as AppConnection,
dnsAppConnectionId, { connectionId: dnsAppConnectionId, projectId: ca.projectId },
actor actor
); );

View File

@@ -41,10 +41,10 @@ import {
type TAzureAdCsCertificateAuthorityFnsDeps = { type TAzureAdCsCertificateAuthorityFnsDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">; appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">; appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
certificateAuthorityDAL: Pick< certificateAuthorityDAL: Pick<
TCertificateAuthorityDALFactory, TCertificateAuthorityDALFactory,
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" "create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById"
>; >;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">; externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">; certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
@@ -621,9 +621,9 @@ export const AzureAdCsCertificateAuthorityFns = ({
}); });
} }
await appConnectionService.connectAppConnectionById( await appConnectionService.validateAppConnectionUsageById(
appConnection.app as AppConnection, appConnection.app as AppConnection,
azureAdcsConnectionId, { connectionId: azureAdcsConnectionId, projectId },
actor 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, appConnection.app as AppConnection,
azureAdcsConnectionId, { connectionId: azureAdcsConnectionId, projectId: ca.projectId },
actor actor
); );

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,7 @@ const getValidAuthToken = async (
connection.credentials.refreshToken, connection.credentials.refreshToken,
connection.id, connection.id,
connection.orgId, connection.orgId,
connection.projectId,
appConnectionDAL, appConnectionDAL,
kmsService 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("description").withSchema(TableName.AppConnection).as("connectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"), 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("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
db db
@@ -67,6 +68,7 @@ const expandSecretSync = (
connectionVersion, connectionVersion,
connectionIsPlatformManagedCredentials, connectionIsPlatformManagedCredentials,
connectionGatewayId, connectionGatewayId,
connectionProjectId,
...el ...el
} = secretSync; } = secretSync;
@@ -86,7 +88,8 @@ const expandSecretSync = (
updatedAt: connectionUpdatedAt, updatedAt: connectionUpdatedAt,
version: connectionVersion, version: connectionVersion,
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials, isPlatformManagedCredentials: connectionIsPlatformManagedCredentials,
gatewayId: connectionGatewayId gatewayId: connectionGatewayId,
projectId: connectionProjectId
}, },
folder: folder folder: folder
? { ? {

View File

@@ -484,13 +484,14 @@ export const secretSyncQueueFactory = ({
try { try {
const { const {
connection: { orgId, encryptedCredentials } connection: { orgId, encryptedCredentials, projectId }
} = secretSync; } = secretSync;
const credentials = await decryptAppConnectionCredentials({ const credentials = await decryptAppConnectionCredentials({
orgId, orgId,
encryptedCredentials, encryptedCredentials,
kmsService kmsService,
projectId
}); });
const secretSyncWithCredentials = { const secretSyncWithCredentials = {
@@ -624,13 +625,14 @@ export const secretSyncQueueFactory = ({
try { try {
const { const {
connection: { orgId, encryptedCredentials } connection: { orgId, encryptedCredentials, projectId }
} = secretSync; } = secretSync;
const credentials = await decryptAppConnectionCredentials({ const credentials = await decryptAppConnectionCredentials({
orgId, orgId,
encryptedCredentials, encryptedCredentials,
kmsService kmsService,
projectId
}); });
await $importSecrets( await $importSecrets(
@@ -744,13 +746,14 @@ export const secretSyncQueueFactory = ({
try { try {
const { const {
connection: { orgId, encryptedCredentials } connection: { orgId, encryptedCredentials, projectId }
} = secretSync; } = secretSync;
const credentials = await decryptAppConnectionCredentials({ const credentials = await decryptAppConnectionCredentials({
orgId, orgId,
encryptedCredentials, encryptedCredentials,
kmsService kmsService,
projectId
}); });
const secretMap = await $getInfisicalSecrets(secretSync); const secretMap = await $getInfisicalSecrets(secretSync);

View File

@@ -41,7 +41,7 @@ import { TSecretSyncQueueFactory } from "./secret-sync-queue";
type TSecretSyncServiceFactoryDep = { type TSecretSyncServiceFactoryDep = {
secretSyncDAL: TSecretSyncDALFactory; secretSyncDAL: TSecretSyncDALFactory;
secretImportDAL: TSecretImportDALFactory; secretImportDAL: TSecretImportDALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">; appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">;
@@ -267,7 +267,11 @@ export const secretSyncServiceFactory = ({
const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination]; const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination];
// validates permission to connect and app is valid for sync 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 { try {
const secretSync = await secretSyncDAL.create({ const secretSync = await secretSyncDAL.create({
@@ -362,7 +366,11 @@ export const secretSyncServiceFactory = ({
const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync]; const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync];
// validates permission to connect and app is valid for sync 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: secretSync.projectId },
actor
);
} }
if ( 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"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </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) ![1Password Connection Modal](/images/app-connections/1password/app-connection-modal.png)
</Step> </Step>
<Step title="Connection Created"> <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) ![1Password Connection Created](/images/app-connections/1password/app-connection-created.png)
</Step> </Step>
@@ -90,6 +90,7 @@ Infisical supports the use of [Service Accounts](https://developer.1password.com
--data '{ --data '{
"name": "my-1password-connection", "name": "my-1password-connection",
"method": "api-token", "method": "api-token",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"instanceUrl": "https://1pass.example.com", "instanceUrl": "https://1pass.example.com",
"apiToken": "<YOUR-API-TOKEN>" "apiToken": "<YOUR-API-TOKEN>"
@@ -104,6 +105,7 @@ Infisical supports the use of [Service Accounts](https://developer.1password.com
"appConnection": { "appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-1password-connection", "name": "my-1password-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"description": null, "description": null,
"version": 1, "version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", "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> <Tabs>
<Tab title="Infisical UI"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Select the **Auth0 Connection** option. 2. Select the **Auth0 Connection** option.
@@ -67,6 +67,7 @@ Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-st
--data '{ --data '{
"name": "my-auth0-connection", "name": "my-auth0-connection",
"method": "client-credentials", "method": "client-credentials",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"domain": "xxx-xxxxxxxxx.us.auth0.com", "domain": "xxx-xxxxxxxxx.us.auth0.com",
"clientId": "...", "clientId": "...",
@@ -83,6 +84,7 @@ Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-st
"appConnection": { "appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-auth0-connection", "name": "my-auth0-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"version": 1, "version": 1,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z", "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"> <Step title="Setup AWS Connection in Infisical">
<Tabs> <Tabs>
<Tab title="Infisical UI"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Select the **AWS Connection** option. 2. Select the **AWS Connection** option.
@@ -209,6 +209,7 @@ Infisical supports two methods for connecting to AWS.
--data '{ --data '{
"name": "my-aws-connection", "name": "my-aws-connection",
"method": "assume-role", "method": "assume-role",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"roleArn": "...", "roleArn": "...",
} }
@@ -222,6 +223,7 @@ Infisical supports two methods for connecting to AWS.
"appConnection": { "appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-aws-connection", "name": "my-aws-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"version": 123, "version": 123,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z", "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"> <Step title="Setup AWS Connection in Infisical">
<Tabs> <Tabs>
<Tab title="Infisical UI"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Select the **AWS Connection** option. 2. Select the **AWS Connection** option.
@@ -386,6 +388,7 @@ Infisical supports two methods for connecting to AWS.
--data '{ --data '{
"name": "my-aws-connection", "name": "my-aws-connection",
"method": "access-key", "method": "access-key",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"accessKeyId": "...", "accessKeyId": "...",
"secretKey": "..." "secretKey": "..."
@@ -400,6 +403,7 @@ Infisical supports two methods for connecting to AWS.
"appConnection": { "appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-aws-connection", "name": "my-aws-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"version": 123, "version": 123,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z", "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> <Steps>
<Step title="Navigate to App Connections"> <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) Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <Step title="Add Connection">

View File

@@ -94,7 +94,7 @@ Infisical currently only supports two methods for connecting to Azure, which are
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <Step title="Add Connection">

View File

@@ -117,7 +117,7 @@ Infisical currently supports three methods for connecting to Azure DevOps, which
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <Step title="Add Connection">

View File

@@ -83,7 +83,7 @@ Infisical currently only supports two methods for connecting to Azure, which are
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <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"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </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) ![Bitbucket Connection Modal](/images/app-connections/bitbucket/step-6.png)
</Step> </Step>
<Step title="Connection Created"> <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) ![Bitbucket Connection Created](/images/app-connections/bitbucket/step-7.png)
</Step> </Step>
@@ -113,6 +113,7 @@ Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbuck
--data '{ --data '{
"name": "my-bitbucket-connection", "name": "my-bitbucket-connection",
"method": "api-token", "method": "api-token",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"email": "user@example.com", "email": "user@example.com",
"apiToken": "<YOUR-API-TOKEN>" "apiToken": "<YOUR-API-TOKEN>"
@@ -127,6 +128,7 @@ Infisical supports the use of [API Tokens](https://support.atlassian.com/bitbuck
"appConnection": { "appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-bitbucket-connection", "name": "my-bitbucket-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"description": null, "description": null,
"version": 1, "version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c",

View File

@@ -50,8 +50,7 @@ Infisical supports connecting to Camunda APIs using [client credentials](https:/
<Steps> <Steps>
<Step title="Navigate to App Connections"> <Step title="Navigate to App Connections">
Navigate to the **App Connections** tab on the **Organization Settings** Navigate to the **App Connections** page in the desired project. ![App Connections
page. ![App Connections
Tab](/images/app-connections/general/add-connection.png) Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <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"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </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) ![Checkly Connection Modal](/images/app-connections/checkly/checkly-app-connection-form.png)
</Step> </Step>
<Step title="Connection created"> <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) ![Checkly Connection Created](/images/app-connections/checkly/checkly-app-connection-generated.png)
</Step> </Step>
@@ -75,6 +75,7 @@ Infisical supports the use of [API Keys](https://app.checklyhq.com/settings/user
--data '{ --data '{
"name": "my-checkly-connection", "name": "my-checkly-connection",
"method": "api-key", "method": "api-key",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"apiKey": "[API KEY]" "apiKey": "[API KEY]"
} }
@@ -88,6 +89,7 @@ Infisical supports the use of [API Keys](https://app.checklyhq.com/settings/user
"appConnection": { "appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-checkly-connection", "name": "my-checkly-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"description": null, "description": null,
"version": 1, "version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", "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> <Steps>
<Step title="Navigate to App Connections"> <Step title="Navigate to App Connections">
Navigate to the **App Connections** tab on the **Organization Settings** Navigate to the **App Connections** page in the desired project. ![App Connections
page. ![App Connections
Tab](/images/app-connections/general/add-connection.png) Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <Step title="Add Connection">

View File

@@ -43,8 +43,7 @@ Infisical supports the use of [service principals](https://docs.databricks.com/e
<Steps> <Steps>
<Step title="Navigate to App Connections"> <Step title="Navigate to App Connections">
Navigate to the **App Connections** tab on the **Organization Settings** Navigate to the **App Connections** page in the desired project. ![App Connections
page. ![App Connections
Tab](/images/app-connections/general/add-connection.png) Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <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"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </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) ![DigitalOcean Connection Modal](/images/app-connections/digital-ocean/app-connection-form.png)
</Step> </Step>
<Step title="Connection created"> <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) ![DigitalOcean Connection Created](/images/app-connections/digital-ocean/app-connection-generated.png)
</Step> </Step>
@@ -82,6 +82,7 @@ Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/accoun
--data '{ --data '{
"name": "my-digitalocean-connection", "name": "my-digitalocean-connection",
"method": "api-token", "method": "api-token",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"apiToken": "[API TOKEN]" "apiToken": "[API TOKEN]"
} }
@@ -95,6 +96,7 @@ Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/accoun
"appConnection": { "appConnection": {
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"name": "my-digitalocean-connection", "name": "my-digitalocean-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"description": null, "description": null,
"version": 1, "version": 1,
"orgId": "abcdef12-3456-7890-abcd-ef1234567890", "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"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </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) ![Fly.io Connection Modal](/images/app-connections/flyio/app-connection-modal.png)
</Step> </Step>
<Step title="Connection Created"> <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) ![Fly.io Connection Created](/images/app-connections/flyio/app-connection-created.png)
</Step> </Step>
@@ -66,6 +66,7 @@ Infisical supports the use of [Access Tokens](https://fly.io/docs/security/token
--data '{ --data '{
"name": "my-flyio-connection", "name": "my-flyio-connection",
"method": "access-token", "method": "access-token",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"accessToken": "[PRIVATE TOKEN]" "accessToken": "[PRIVATE TOKEN]"
} }
@@ -79,6 +80,7 @@ Infisical supports the use of [Access Tokens](https://fly.io/docs/security/token
"appConnection": { "appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-flyio-connection", "name": "my-flyio-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"description": null, "description": null,
"version": 1, "version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c",

View File

@@ -82,8 +82,7 @@ Infisical supports [service account impersonation](https://cloud.google.com/iam/
<Steps> <Steps>
<Step title="Navigate to App Connections"> <Step title="Navigate to App Connections">
Navigate to the **App Connections** tab on the **Organization Settings** Navigate to the **App Connections** page in the desired project. ![App Connections
page. ![App Connections
Tab](/images/app-connections/general/add-connection.png) Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <Step title="Add Connection">

View File

@@ -97,7 +97,7 @@ Infisical supports GitHub App installation for creating a GitHub Radar Connectio
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <Step title="Add Connection">

View File

@@ -85,7 +85,7 @@ Infisical supports two methods for connecting to GitHub.
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <Step title="Add Connection">
@@ -156,7 +156,7 @@ Infisical supports two methods for connecting to GitHub.
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <Step title="Add Connection">

View File

@@ -70,7 +70,7 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <Step title="Add Connection">
@@ -193,7 +193,7 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <Step title="Add Connection">

View File

@@ -131,7 +131,7 @@ Infisical supports two methods for connecting to Hashicorp Vault.
<Tab title="Infisical UI"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
@@ -184,6 +184,7 @@ Infisical supports two methods for connecting to Hashicorp Vault.
--data '{ --data '{
"name": "my-vault-connection", "name": "my-vault-connection",
"method": "app-role", "method": "app-role",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"instanceUrl": "https://vault.example.com", "instanceUrl": "https://vault.example.com",
"roleId": "4797c4fa-7794-71f0-c8b1-7c87759df5bf", "roleId": "4797c4fa-7794-71f0-c8b1-7c87759df5bf",
@@ -199,6 +200,7 @@ Infisical supports two methods for connecting to Hashicorp Vault.
"appConnection": { "appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-vault-connection", "name": "my-vault-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"version": 1, "version": 1,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2025-04-01T05:31:56Z", "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> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <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) ![Heroku API Token](/images/app-connections/heroku/heroku-api-token.png)
</Step> </Step>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <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) ![Humanitec Connection Created](/images/app-connections/humanitec/humanitec-user-added.png)
</Step> </Step>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <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> <Tabs>
<Tab title="Infisical UI"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Select the **LDAP Connection** option. 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 '{ --data '{
"name": "my-ldap-connection", "name": "my-ldap-connection",
"method": "simple-bind", "method": "simple-bind",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"provider": "active-directory", "provider": "active-directory",
"url": "ldaps://domain-or-ip:636", "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": { "appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-ldap-connection", "name": "my-ldap-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"version": 1, "version": 1,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z", "createdAt": "2023-11-07T05:31:56Z",

View File

@@ -62,7 +62,7 @@ Infisical supports connecting to Microsoft SQL Server using database principals.
<Tabs> <Tabs>
<Tab title="Infisical UI"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Select the **Microsoft SQL Server Connection** option. 2. Select the **Microsoft SQL Server Connection** option.
@@ -96,6 +96,7 @@ Infisical supports connecting to Microsoft SQL Server using database principals.
--data '{ --data '{
"name": "my-mssql-connection", "name": "my-mssql-connection",
"method": "username-and-password", "method": "username-and-password",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"isPlatformManagedCredentials": true, "isPlatformManagedCredentials": true,
"credentials": { "credentials": {
"host": "123.4.5.6", "host": "123.4.5.6",
@@ -115,7 +116,8 @@ Infisical supports connecting to Microsoft SQL Server using database principals.
{ {
"appConnection": { "appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-pg-connection", "name": "my-mssql-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"version": 1, "version": 1,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z", "createdAt": "2023-11-07T05:31:56Z",

View File

@@ -52,7 +52,7 @@ Infisical supports connecting to MySQL using a database role.
<Tabs> <Tabs>
<Tab title="Infisical UI"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Select the **MySQL Connection** option. 2. Select the **MySQL Connection** option.
@@ -88,6 +88,7 @@ Infisical supports connecting to MySQL using a database role.
"name": "my-mysql-connection", "name": "my-mysql-connection",
"method": "username-and-password", "method": "username-and-password",
"isPlatformManagedCredentials": true, "isPlatformManagedCredentials": true,
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"host": "123.4.5.6", "host": "123.4.5.6",
"port": 3306, "port": 3306,
@@ -107,6 +108,7 @@ Infisical supports connecting to MySQL using a database role.
"appConnection": { "appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-mysql-connection", "name": "my-mysql-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"version": 1, "version": 1,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z", "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"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </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) ![Netlify Connection Modal](/images/app-connections/netlify/app-connection-form.png)
</Step> </Step>
<Step title="Connection created"> <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) ![Netlify Connection Created](/images/app-connections/netlify/app-connection-generated.png)
</Step> </Step>
@@ -72,6 +72,7 @@ Infisical supports the use of [Personal Access Tokens](https://docs.netlify.com/
--data '{ --data '{
"name": "my-netlify-connection", "name": "my-netlify-connection",
"method": "access-token", "method": "access-token",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"accessToken": "[ACCESS TOKEN]" "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", "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"name": "my-netlify-connection", "name": "my-netlify-connection",
"description": null, "description": null,
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"version": 1, "version": 1,
"orgId": "abcdef12-3456-7890-abcd-ef1234567890", "orgId": "abcdef12-3456-7890-abcd-ef1234567890",
"createdAt": "2025-07-19T10:15:00.000Z", "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"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </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) ![OCI Connection Modal](/images/app-connections/oci/app-connection-modal.png)
</Step> </Step>
<Step title="Connection Created"> <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) ![OCI Connection Created](/images/app-connections/oci/app-connection-created.png)
</Step> </Step>
@@ -157,6 +157,7 @@ Infisical supports the use of [API Signing Key Authentication](https://docs.orac
--data '{ --data '{
"name": "my-oci-connection", "name": "my-oci-connection",
"method": "access-key", "method": "access-key",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"userOcid": "ocid1.user.oc1..aaaaaaaagrp35tbkvvad4y2j7sug7xonua7dl2gfp4at2u5i5xj4ghnitg3a", "userOcid": "ocid1.user.oc1..aaaaaaaagrp35tbkvvad4y2j7sug7xonua7dl2gfp4at2u5i5xj4ghnitg3a",
"tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaotfma465m4zumfe2ua64mj2m5dwmlw2llh4g4dnfttnakiifonta", "tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaotfma465m4zumfe2ua64mj2m5dwmlw2llh4g4dnfttnakiifonta",
@@ -174,6 +175,7 @@ Infisical supports the use of [API Signing Key Authentication](https://docs.orac
"appConnection": { "appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-oci-connection", "name": "my-oci-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"description": null, "description": null,
"version": 1, "version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", "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"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </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) ![Connection Modal](/images/app-connections/okta/step-4.png)
</Step> </Step>
<Step title="Connection Created"> <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) ![Connection Created](/images/app-connections/okta/step-5.png)
</Step> </Step>
@@ -66,6 +66,7 @@ Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guide
--data '{ --data '{
"name": "my-okta-connection", "name": "my-okta-connection",
"method": "api-token", "method": "api-token",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"instanceUrl": "https://example.okta.com", "instanceUrl": "https://example.okta.com",
"apiToken": "<YOUR-API-TOKEN>" "apiToken": "<YOUR-API-TOKEN>"
@@ -80,6 +81,7 @@ Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guide
"appConnection": { "appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-okta-connection", "name": "my-okta-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"description": null, "description": null,
"version": 1, "version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c",

View File

@@ -62,7 +62,7 @@ Infisical supports connecting to OracleDB using a database user.
<Tabs> <Tabs>
<Tab title="Infisical UI"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Select the **OracleDB Connection** option. 2. Select the **OracleDB Connection** option.
@@ -98,6 +98,7 @@ Infisical supports connecting to OracleDB using a database user.
"name": "my-oracledb-connection", "name": "my-oracledb-connection",
"method": "username-and-password", "method": "username-and-password",
"isPlatformManagedCredentials": true, "isPlatformManagedCredentials": true,
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"host": "123.4.5.6", "host": "123.4.5.6",
"port": 1521, "port": 1521,
@@ -117,6 +118,7 @@ Infisical supports connecting to OracleDB using a database user.
"appConnection": { "appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-oracledb-connection", "name": "my-oracledb-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"version": 1, "version": 1,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z", "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." 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 ## Concept
App Connections are an organization-level resource used to establish connections with third-party applications App Connections can be 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. that can be used across multiple features. Example use cases include syncing secrets, rotating credentials, scanning repositories for secret leaks, and more.
<br /> <br />

View File

@@ -60,7 +60,7 @@ Infisical supports connecting to PostgreSQL using a database role.
<Tabs> <Tabs>
<Tab title="Infisical UI"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Select the **PostgreSQL Connection** option. 2. Select the **PostgreSQL Connection** option.
@@ -95,6 +95,7 @@ Infisical supports connecting to PostgreSQL using a database role.
"name": "my-pg-connection", "name": "my-pg-connection",
"method": "username-and-password", "method": "username-and-password",
"isPlatformManagedCredentials": true, "isPlatformManagedCredentials": true,
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"host": "123.4.5.6", "host": "123.4.5.6",
"port": 5432, "port": 5432,
@@ -114,6 +115,7 @@ Infisical supports connecting to PostgreSQL using a database role.
"appConnection": { "appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-pg-connection", "name": "my-pg-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"version": 1, "version": 1,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z", "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"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </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) ![Railway Connection Modal](/images/app-connections/railway/railway-app-connection-form.png)
</Step> </Step>
<Step title="Connection created"> <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) ![Railway Connection Created](/images/app-connections/railway/railway-app-connection-generated.png)
</Step> </Step>
@@ -134,6 +134,7 @@ Infisical supports the use of [API Tokens](https://docs.railway.com/guides/publi
--data '{ --data '{
"name": "my-railway-connection", "name": "my-railway-connection",
"method": "team-token", "method": "team-token",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"apiToken": "[TEAM TOKEN]" "apiToken": "[TEAM TOKEN]"
} }
@@ -147,6 +148,7 @@ Infisical supports the use of [API Tokens](https://docs.railway.com/guides/publi
"appConnection": { "appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-railway-connection", "name": "my-railway-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"description": null, "description": null,
"version": 1, "version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", "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> <Steps>
<Step title="Navigate to App Connections"> <Step title="Navigate to App Connections">
Navigate to the **App Connections** tab on the **Organization Settings** Navigate to the **App Connections** page in the desired project. ![App Connections
page. ![App Connections
Tab](/images/app-connections/general/add-connection.png) Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <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"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </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) ![Supabase Connection Modal](/images/app-connections/supabase/app-connection-form.png)
</Step> </Step>
<Step title="Connection created"> <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) ![Supabase Connection Created](/images/app-connections/supabase/app-connection-generated.png)
</Step> </Step>
@@ -73,6 +73,7 @@ Infisical supports the use of [Personal Access Tokens](https://supabase.com/dash
--data '{ --data '{
"name": "my-supabase-connection", "name": "my-supabase-connection",
"method": "access-token", "method": "access-token",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"accessToken": "[Access Token]", "accessToken": "[Access Token]",
"instanceUrl": "https://api.supabase.com" "instanceUrl": "https://api.supabase.com"
@@ -87,6 +88,7 @@ Infisical supports the use of [Personal Access Tokens](https://supabase.com/dash
"appConnection": { "appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-supabase-connection", "name": "my-supabase-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"description": null, "description": null,
"version": 1, "version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c",

View File

@@ -51,7 +51,7 @@ Infisical supports connecting to TeamCity using Access Tokens.
<Tab title="Infisical UI"> <Tab title="Infisical UI">
1. Navigate to App Connections 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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Add Connection 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) ![TeamCity Connection Modal](/images/app-connections/teamcity/teamcity-app-connection-modal.png)
4. Connection Created 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) ![TeamCity Connection Created](/images/app-connections/teamcity/teamcity-app-connection-created.png)
</Tab> </Tab>
<Tab title="API"> <Tab title="API">
@@ -84,6 +84,7 @@ Infisical supports connecting to TeamCity using Access Tokens.
--data '{ --data '{
"name": "my-teamcity-connection", "name": "my-teamcity-connection",
"method": "access-token", "method": "access-token",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"accessToken": "...", "accessToken": "...",
"instanceUrl": "https://yourcompany.teamcity.com" "instanceUrl": "https://yourcompany.teamcity.com"
@@ -98,6 +99,7 @@ Infisical supports connecting to TeamCity using Access Tokens.
"appConnection": { "appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-teamcity-connection", "name": "my-teamcity-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"description": null, "description": null,
"version": 1, "version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", "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"> <Step title="Add Terraform Cloud Connection in Infisical">
<Tabs> <Tabs>
<Tab title="Infisical UI"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Select the **Terraform Cloud Connection** option from the connection options modal. 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) ![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 '{ --data '{
"name": "my-terraform-cloud-connection", "name": "my-terraform-cloud-connection",
"method": "api-token", "method": "api-token",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"apiToken": "...", "apiToken": "...",
} }
@@ -65,6 +66,7 @@ Infisical supports connecting to Terraform Cloud using a service user.
"appConnection": { "appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-terraform-cloud-connection", "name": "my-terraform-cloud-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"version": 123, "version": 123,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z", "createdAt": "2023-11-07T05:31:56Z",

View File

@@ -37,7 +37,7 @@ Infisical supports connecting to Vercel using API Tokens.
<Tab title="Infisical UI"> <Tab title="Infisical UI">
1. Navigate to App Connections 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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Add Connection 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) ![Vercel Connection Modal](/images/app-connections/vercel/vercel-app-connection-modal.png)
4. Connection Created 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) ![Vercel Connection Created](/images/app-connections/vercel/vercel-app-connection-created.png)
</Tab> </Tab>
<Tab title="API"> <Tab title="API">
@@ -67,6 +67,7 @@ Infisical supports connecting to Vercel using API Tokens.
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \
--data '{ --data '{
"name": "my-vercel-connection", "name": "my-vercel-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"method": "api-token", "method": "api-token",
"credentials": { "credentials": {
"apiToken": "...", "apiToken": "...",
@@ -81,6 +82,7 @@ Infisical supports connecting to Vercel using API Tokens.
"appConnection": { "appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-vercel-connection", "name": "my-vercel-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"version": 123, "version": 123,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2025-04-01T05:31:56Z", "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"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </Step>
<Step title="Add Connection"> <Step title="Add Connection">
@@ -82,6 +83,7 @@ Ensure the user generating the access token has the required role and permission
--data '{ --data '{
"name": "my-windmill-connection", "name": "my-windmill-connection",
"method": "access-token", "method": "access-token",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"token": "...", "token": "...",
"instanceUrl": "https://app.windmill.dev" "instanceUrl": "https://app.windmill.dev"
@@ -96,6 +98,7 @@ Ensure the user generating the access token has the required role and permission
"appConnection": { "appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-windmill-connection", "name": "my-windmill-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"version": 123, "version": 123,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2025-04-01T05:31:56Z", "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"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Navigate to App Connections"> <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) ![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step> </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) ![Zabbix Connection Modal](/images/app-connections/zabbix/zabbix-app-connection-form.png)
</Step> </Step>
<Step title="Connection Created"> <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) ![Zabbix Connection Created](/images/app-connections/zabbix/zabbix-app-connection-generated.png)
</Step> </Step>
@@ -68,6 +68,7 @@ Infisical supports the use of [API Tokens](https://www.zabbix.com/documentation/
--data '{ --data '{
"name": "my-zabbix-connection", "name": "my-zabbix-connection",
"method": "api-token", "method": "api-token",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"credentials": { "credentials": {
"apiToken": "[API TOKEN]", "apiToken": "[API TOKEN]",
"instanceUrl": "https://zabbix.example.com" "instanceUrl": "https://zabbix.example.com"
@@ -82,6 +83,7 @@ Infisical supports the use of [API Tokens](https://www.zabbix.com/documentation/
"appConnection": { "appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-zabbix-connection", "name": "my-zabbix-connection",
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
"description": null, "description": null,
"version": 1, "version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", "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 { 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 { 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"; import { AccessRestrictedBanner, Tooltip } from "../v2";
@@ -14,7 +16,7 @@ export const OrgPermissionGuardBanner = () => {
); );
}; };
type Props = { type Props<T extends AbilityTuple> = {
label?: ReactNode; label?: ReactNode;
// this prop is used when there exist already a tooltip as helper text for users // 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 // so when permission is allowed same tooltip will be reused to show helpertext
@@ -22,9 +24,18 @@ type Props = {
allowedLabel?: string; allowedLabel?: string;
renderGuardBanner?: boolean; renderGuardBanner?: boolean;
tooltipProps?: Omit<TooltipProps, "children">; 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", label = "Access restricted",
children, children,
passThrough = true, passThrough = true,
@@ -41,9 +52,7 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
{(isAllowed, ability) => { {(isAllowed, ability) => {
// akhilmhdh: This is set as type due to error in casl react type. // akhilmhdh: This is set as type due to error in casl react type.
const finalChild = const finalChild =
typeof children === "function" typeof children === "function" ? children(isAllowed, ability as any) : children;
? children(isAllowed, ability as TOrgPermission)
: children;
if (!isAllowed && passThrough) { if (!isAllowed && passThrough) {
return ( 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 { OrgPermissionCan } from "./OrgPermissionCan";
export { PermissionDeniedBanner } from "./PermissionDeniedBanner"; export { PermissionDeniedBanner } from "./PermissionDeniedBanner";
export { ProjectPermissionCan } from "./ProjectPermissionCan"; 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 { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { SecretRotationV2Form } from "@app/components/secret-rotations-v2/forms"; 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 { SecretRotationV2ModalHeader } from "@app/components/secret-rotations-v2/SecretRotationV2ModalHeader";
import { SecretRotationV2Select } from "@app/components/secret-rotations-v2/SecretRotationV2Select"; import { SecretRotationV2Select } from "@app/components/secret-rotations-v2/SecretRotationV2Select";
import { Modal, ModalContent } from "@app/components/v2"; import { Modal, ModalContent } from "@app/components/v2";
@@ -24,14 +26,21 @@ type ContentProps = {
onComplete: (secretRotation: TSecretRotationV2) => void; onComplete: (secretRotation: TSecretRotationV2) => void;
selectedRotation: SecretRotation | null; selectedRotation: SecretRotation | null;
setSelectedRotation: (selectedRotation: SecretRotation | null) => void; setSelectedRotation: (selectedRotation: SecretRotation | null) => void;
initialFormData?: Partial<TSecretRotationV2Form>;
} & SharedProps; } & SharedProps;
const Content = ({ setSelectedRotation, selectedRotation, ...props }: ContentProps) => { const Content = ({
setSelectedRotation,
selectedRotation,
initialFormData,
...props
}: ContentProps) => {
if (selectedRotation) { if (selectedRotation) {
return ( return (
<SecretRotationV2Form <SecretRotationV2Form
onCancel={() => setSelectedRotation(null)} onCancel={() => setSelectedRotation(null)}
type={selectedRotation} type={selectedRotation}
initialFormData={initialFormData}
{...props} {...props}
/> />
); );
@@ -42,12 +51,56 @@ const Content = ({ setSelectedRotation, selectedRotation, ...props }: ContentPro
export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }: Props) => { export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }: Props) => {
const [selectedRotation, setSelectedRotation] = useState<SecretRotation | null>(null); 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 ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) setSelectedRotation(null); if (!open) {
setSelectedRotation(null);
setInitialFormData(undefined);
}
onOpenChange(open); onOpenChange(open);
}} }}
> >
@@ -87,6 +140,7 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
setSelectedRotation(null); setSelectedRotation(null);
onOpenChange(false); onOpenChange(false);
}} }}
initialFormData={initialFormData}
selectedRotation={selectedRotation} selectedRotation={selectedRotation}
setSelectedRotation={setSelectedRotation} setSelectedRotation={setSelectedRotation}
{...props} {...props}

View File

@@ -1,14 +1,17 @@
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { SingleValue } from "react-select";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 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 { FilterableSelect, FormControl } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission } from "@app/context"; import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types"; import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_ROTATION_CONNECTION_MAP } from "@app/helpers/secretRotationsV2"; import { SECRET_ROTATION_CONNECTION_MAP } from "@app/helpers/secretRotationsV2";
import { usePopUp } from "@app/hooks";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections"; import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
import { TSecretRotationV2Form } from "./schemas"; import { TSecretRotationV2Form } from "./schemas";
@@ -18,19 +21,26 @@ type Props = {
}; };
export const SecretRotationV2ConnectionField = ({ onChange: callback, isUpdate }: Props) => { export const SecretRotationV2ConnectionField = ({ onChange: callback, isUpdate }: Props) => {
const { permission } = useOrgPermission(); const { permission } = useProjectPermission();
const { control, watch } = useFormContext<TSecretRotationV2Form>(); const { control, watch, setValue } = useFormContext<TSecretRotationV2Form>();
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const);
const rotationType = watch("type"); const rotationType = watch("type");
const app = SECRET_ROTATION_CONNECTION_MAP[rotationType]; 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 connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can( const canCreateConnection = permission.can(
OrgPermissionAppConnectionActions.Create, ProjectPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections ProjectPermissionSub.AppConnections
); );
const appName = APP_CONNECTION_MAP[app].name; const appName = APP_CONNECTION_MAP[app].name;
@@ -66,37 +76,56 @@ export const SecretRotationV2ConnectionField = ({ onChange: callback, isUpdate }
<FilterableSelect <FilterableSelect
value={value} value={value}
onChange={(newValue) => { 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); onChange(newValue);
if (callback) callback(); if (callback) callback();
}} }}
isLoading={isPending} isLoading={isPending}
options={availableConnections} options={[
...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []),
...(availableConnections ?? [])
]}
isDisabled={isUpdate} isDisabled={isUpdate}
placeholder="Select connection..." placeholder="Select connection..."
getOptionLabel={(option) => option.name} getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id} getOptionValue={(option) => option.id}
components={{ Option: AppConnectionOption }}
/> />
</FormControl> </FormControl>
)} )}
control={control} control={control}
name="connection" name="connection"
/> />
{!isUpdate && availableConnections?.length === 0 && ( {!isUpdate && !isPending && !availableConnections?.length && !canCreateConnection && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow"> <p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} /> <FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? ( You do not have access to any ${appName} Connections. Contact an admin to create one.
<>
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.`
)}
</p> </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; environment?: string;
environments?: WorkspaceEnv[]; environments?: WorkspaceEnv[];
secretRotation?: TSecretRotationV2; secretRotation?: TSecretRotationV2;
initialFormData?: Partial<TSecretRotationV2Form>;
}; };
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretRotationV2Form)[] }[] = [ const FORM_TABS: { name: string; key: string; fields: (keyof TSecretRotationV2Form)[] }[] = [
@@ -64,7 +65,8 @@ export const SecretRotationV2Form = ({
environment: envSlug, environment: envSlug,
secretPath, secretPath,
secretRotation, secretRotation,
environments environments,
initialFormData
}: Props) => { }: Props) => {
const createSecretRotation = useCreateSecretRotationV2(); const createSecretRotation = useCreateSecretRotationV2();
const updateSecretRotation = useUpdateSecretRotationV2(); const updateSecretRotation = useUpdateSecretRotationV2();
@@ -93,7 +95,8 @@ export const SecretRotationV2Form = ({
}, },
environment: currentWorkspace?.environments.find((env) => env.slug === envSlug), environment: currentWorkspace?.environments.find((env) => env.slug === envSlug),
secretPath, 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" 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 { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 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 { Modal, ModalContent } from "@app/components/v2";
import { import {
SecretScanningDataSource, SecretScanningDataSource,
@@ -21,6 +23,7 @@ type ContentProps = {
onComplete: (dataSource: TSecretScanningDataSource) => void; onComplete: (dataSource: TSecretScanningDataSource) => void;
selectedDataSource: SecretScanningDataSource | null; selectedDataSource: SecretScanningDataSource | null;
setSelectedDataSource: (selectedDataSource: SecretScanningDataSource | null) => void; setSelectedDataSource: (selectedDataSource: SecretScanningDataSource | null) => void;
initialFormData?: Partial<TSecretScanningDataSourceForm>;
}; };
const Content = ({ setSelectedDataSource, selectedDataSource, ...props }: ContentProps) => { const Content = ({ setSelectedDataSource, selectedDataSource, ...props }: ContentProps) => {
@@ -41,6 +44,47 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
const [selectedDataSource, setSelectedDataSource] = useState<SecretScanningDataSource | null>( const [selectedDataSource, setSelectedDataSource] = useState<SecretScanningDataSource | null>(
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 ( return (
<Modal <Modal
@@ -88,6 +132,7 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
}} }}
selectedDataSource={selectedDataSource} selectedDataSource={selectedDataSource}
setSelectedDataSource={setSelectedDataSource} setSelectedDataSource={setSelectedDataSource}
initialFormData={initialFormData}
{...props} {...props}
/> />
</ModalContent> </ModalContent>

View File

@@ -1,14 +1,17 @@
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { SingleValue } from "react-select";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 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 { FilterableSelect, FormControl } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission } from "@app/context"; import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types"; import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/helpers/secretScanningV2"; import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/helpers/secretScanningV2";
import { usePopUp } from "@app/hooks";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections"; import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
import { TSecretScanningDataSourceForm } from "./schemas"; import { TSecretScanningDataSourceForm } from "./schemas";
@@ -21,19 +24,26 @@ export const SecretScanningDataSourceConnectionField = ({
onChange: callback, onChange: callback,
isUpdate isUpdate
}: Props) => { }: Props) => {
const { permission } = useOrgPermission(); const { permission } = useProjectPermission();
const { control, watch } = useFormContext<TSecretScanningDataSourceForm>(); const { control, watch, setValue } = useFormContext<TSecretScanningDataSourceForm>();
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const);
const dataSourceType = watch("type"); const dataSourceType = watch("type");
const app = SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSourceType]; 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 connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can( const canCreateConnection = permission.can(
OrgPermissionAppConnectionActions.Create, ProjectPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections ProjectPermissionSub.AppConnections
); );
return ( return (
@@ -67,37 +77,57 @@ export const SecretScanningDataSourceConnectionField = ({
<FilterableSelect <FilterableSelect
value={value} value={value}
onChange={(newValue) => { 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); onChange(newValue);
if (callback) callback(); if (callback) callback();
}} }}
isLoading={isPending} isLoading={isPending}
options={availableConnections} options={[
...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []),
...(availableConnections ?? [])
]}
isDisabled={isUpdate} isDisabled={isUpdate}
placeholder="Select connection..." placeholder="Select connection..."
getOptionLabel={(option) => option.name} getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id} getOptionValue={(option) => option.id}
components={{ Option: AppConnectionOption }}
/> />
</FormControl> </FormControl>
)} )}
control={control} control={control}
name="connection" name="connection"
/> />
{!isUpdate && availableConnections?.length === 0 && ( {!isUpdate && !isPending && !availableConnections?.length && !canCreateConnection && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow"> <p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} /> <FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? ( You do not have access to any ${connectionName} Connections. Contact an admin to create
<> one.
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.`
)}
</p> </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; type: SecretScanningDataSource;
onCancel: () => void; onCancel: () => void;
dataSource?: TSecretScanningDataSource; dataSource?: TSecretScanningDataSource;
initialFormData?: Partial<TSecretScanningDataSourceForm>;
}; };
const FORM_TABS: { name: string; key: string; fields: (keyof 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: [] } { 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 createDataSource = useCreateSecretScanningDataSource();
const updateDataSource = useUpdateSecretScanningDataSource(); const updateDataSource = useUpdateSecretScanningDataSource();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
@@ -48,7 +55,8 @@ export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataS
resolver: zodResolver(SecretScanningDataSourceSchema), resolver: zodResolver(SecretScanningDataSourceSchema),
defaultValues: dataSource ?? { defaultValues: dataSource ?? {
type, 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" reValidateMode: "onChange"
}); });

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Modal, ModalContent } from "@app/components/v2"; import { Modal, ModalContent } from "@app/components/v2";
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
@@ -11,18 +12,21 @@ type Props = {
isOpen: boolean; isOpen: boolean;
onOpenChange: (isOpen: boolean) => void; onOpenChange: (isOpen: boolean) => void;
selectSync?: SecretSync | null; selectSync?: SecretSync | null;
initialFormData?: Partial<TSecretSyncForm>;
}; };
type ContentProps = { type ContentProps = {
onComplete: (secretSync: TSecretSync) => void; onComplete: (secretSync: TSecretSync) => void;
selectedSync: SecretSync | null; selectedSync: SecretSync | null;
setSelectedSync: (selectedSync: SecretSync | null) => void; setSelectedSync: (selectedSync: SecretSync | null) => void;
initialFormData?: Partial<TSecretSyncForm>;
}; };
const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) => { const Content = ({ onComplete, setSelectedSync, selectedSync, initialFormData }: ContentProps) => {
if (selectedSync) { if (selectedSync) {
return ( return (
<CreateSecretSyncForm <CreateSecretSyncForm
initialFormData={initialFormData}
onComplete={onComplete} onComplete={onComplete}
onCancel={() => setSelectedSync(null)} onCancel={() => setSelectedSync(null)}
destination={selectedSync} destination={selectedSync}
@@ -33,7 +37,12 @@ const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) =>
return <SecretSyncSelect onSelect={setSelectedSync} />; 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); const [selectedSync, setSelectedSync] = useState<SecretSync | null>(selectSync);
useEffect(() => { useEffect(() => {
@@ -67,6 +76,7 @@ export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...prop
}} }}
selectedSync={selectedSync} selectedSync={selectedSync}
setSelectedSync={setSelectedSync} setSelectedSync={setSelectedSync}
initialFormData={initialFormData}
/> />
</ModalContent> </ModalContent>
</Modal> </Modal>

View File

@@ -29,6 +29,7 @@ type Props = {
onComplete: (secretSync: TSecretSync) => void; onComplete: (secretSync: TSecretSync) => void;
destination: SecretSync; destination: SecretSync;
onCancel: () => void; onCancel: () => void;
initialFormData?: Partial<TSecretSyncForm>;
}; };
const FORM_TABS: { name: string; key: string; fields: (keyof 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: [] } { name: "Review", key: "review", fields: [] }
]; ];
export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Props) => { export const CreateSecretSyncForm = ({
destination,
onComplete,
onCancel,
initialFormData
}: Props) => {
const createSecretSync = useCreateSecretSync(); const createSecretSync = useCreateSecretSync();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { name: destinationName } = SECRET_SYNC_MAP[destination]; const { name: destinationName } = SECRET_SYNC_MAP[destination];
const [showConfirmation, setShowConfirmation] = useState(false); 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); const { syncOption } = useSecretSyncOption(destination);
@@ -59,7 +66,8 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
initialSyncBehavior: syncOption?.canImportSecrets initialSyncBehavior: syncOption?.canImportSecrets
? undefined ? undefined
: SecretSyncInitialSyncBehavior.OverwriteDestination : SecretSyncInitialSyncBehavior.OverwriteDestination
} },
...initialFormData
} as Partial<TSecretSyncForm>, } as Partial<TSecretSyncForm>,
reValidateMode: "onChange" reValidateMode: "onChange"
}); });

View File

@@ -1,14 +1,17 @@
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { SingleValue } from "react-select";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 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 { FilterableSelect, FormControl } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission } from "@app/context"; import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types"; import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_SYNC_CONNECTION_MAP } from "@app/helpers/secretSyncs"; import { SECRET_SYNC_CONNECTION_MAP } from "@app/helpers/secretSyncs";
import { usePopUp } from "@app/hooks";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections"; import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
import { TSecretSyncForm } from "./schemas"; import { TSecretSyncForm } from "./schemas";
@@ -17,19 +20,26 @@ type Props = {
}; };
export const SecretSyncConnectionField = ({ onChange: callback }: Props) => { export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
const { permission } = useOrgPermission(); const { permission } = useProjectPermission();
const { control, watch } = useFormContext<TSecretSyncForm>(); const { control, watch, setValue } = useFormContext<TSecretSyncForm>();
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const);
const destination = watch("destination"); const destination = watch("destination");
const app = SECRET_SYNC_CONNECTION_MAP[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 connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can( const canCreateConnection = permission.can(
OrgPermissionAppConnectionActions.Create, ProjectPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections ProjectPermissionSub.AppConnections
); );
const appName = APP_CONNECTION_MAP[SECRET_SYNC_CONNECTION_MAP[destination]].name; const appName = APP_CONNECTION_MAP[SECRET_SYNC_CONNECTION_MAP[destination]].name;
@@ -51,36 +61,55 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
<FilterableSelect <FilterableSelect
value={value} value={value}
onChange={(newValue) => { 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); onChange(newValue);
if (callback) callback(); if (callback) callback();
}} }}
isLoading={isPending} isLoading={isPending}
options={availableConnections} options={[
...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []),
...(availableConnections ?? [])
]}
placeholder="Select connection..." placeholder="Select connection..."
getOptionLabel={(option) => option.name} getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id} getOptionValue={(option) => option.id}
components={{ Option: AppConnectionOption }}
/> />
</FormControl> </FormControl>
)} )}
control={control} control={control}
name="connection" name="connection"
/> />
{availableConnections?.length === 0 && ( {!isPending && !availableConnections?.length && !canCreateConnection && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow"> <p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} /> <FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? ( You do not have access to any ${appName} Connections. Contact an admin to create one.
<>
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.`
)}
</p> </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 { export enum OrgPermissionActions {
Read = "read", Read = "read",
@@ -126,7 +126,6 @@ export type OrgPermissionSet =
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity] | [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip] | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [ | [
@@ -134,14 +133,13 @@ export type OrgPermissionSet =
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
] ]
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway] | [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway]
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]; | [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]
// TODO(scott): add back once org UI refactored | [
// | [ OrgPermissionAppConnectionActions,
// OrgPermissionAppConnectionActions, (
// ( | OrgPermissionSubjects.AppConnections
// | OrgPermissionSubjects.AppConnections | (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
// | (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields) )
// ) ];
// ];
export type TOrgPermission = MongoAbility<OrgPermissionSet>; export type TOrgPermission = MongoAbility<OrgPermissionSet>;

View File

@@ -154,6 +154,14 @@ export enum ProjectPermissionAuditLogsActions {
Read = "read" 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 { export enum PermissionConditionOperators {
$IN = "$in", $IN = "$in",
$ALL = "$all", $ALL = "$all",
@@ -173,6 +181,10 @@ export type IdentityManagementSubjectFields = {
identityId: string; identityId: string;
}; };
export type AppConnectionSubjectFields = {
connectionId: string;
};
export type ConditionalProjectPermissionSubject = export type ConditionalProjectPermissionSubject =
| ProjectPermissionSub.SecretSyncs | ProjectPermissionSub.SecretSyncs
| ProjectPermissionSub.Secrets | ProjectPermissionSub.Secrets
@@ -184,7 +196,8 @@ export type ConditionalProjectPermissionSubject =
| ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports | ProjectPermissionSub.SecretImports
| ProjectPermissionSub.SecretRotation | ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.SecretEvents; | ProjectPermissionSub.SecretEvents
| ProjectPermissionSub.AppConnections;
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = { export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
[PermissionConditionOperators.$EQ]: "equal to", [PermissionConditionOperators.$EQ]: "equal to",
@@ -263,7 +276,8 @@ export enum ProjectPermissionSub {
SecretScanningDataSources = "secret-scanning-data-sources", SecretScanningDataSources = "secret-scanning-data-sources",
SecretScanningFindings = "secret-scanning-findings", SecretScanningFindings = "secret-scanning-findings",
SecretScanningConfigs = "secret-scanning-configs", SecretScanningConfigs = "secret-scanning-configs",
SecretEvents = "secret-events" SecretEvents = "secret-events",
AppConnections = "app-connections"
} }
export type SecretSubjectFields = { export type SecretSubjectFields = {
@@ -431,6 +445,13 @@ export type ProjectPermissionSet =
| ProjectPermissionSub.SecretEvents | ProjectPermissionSub.SecretEvents
| (ForcedSubject<ProjectPermissionSub.SecretEvents> & SecretEventSubjectFields) | (ForcedSubject<ProjectPermissionSub.SecretEvents> & SecretEventSubjectFields)
) )
]
| [
ProjectPermissionAppConnectionActions,
(
| ProjectPermissionSub.AppConnections
| (ForcedSubject<ProjectPermissionSub.AppConnections> & AppConnectionSubjectFields)
)
]; ];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>; export type TProjectPermission = MongoAbility<ProjectPermissionSet>;

View File

@@ -8,6 +8,7 @@ import {
faServer, faServer,
faUser faUser
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { useRouterState } from "@tanstack/react-router";
import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { import {
@@ -221,3 +222,11 @@ export const AWS_REGIONS = [
{ name: "AWS GovCloud (US-East)", slug: "us-gov-east-1" }, { name: "AWS GovCloud (US-East)", slug: "us-gov-east-1" },
{ name: "AWS GovCloud (US-West)", slug: "us-gov-west-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, TAppConnectionResponse,
TCreateAppConnectionDTO, TCreateAppConnectionDTO,
TDeleteAppConnectionDTO, TDeleteAppConnectionDTO,
TMigrateAppConnectionDTO,
TUpdateAppConnectionDTO TUpdateAppConnectionDTO
} from "@app/hooks/api/appConnections/types"; } from "@app/hooks/api/appConnections/types";
@@ -20,7 +21,10 @@ export const useCreateAppConnection = () => {
return data.appConnection; 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; return data.appConnection;
}, },
onSuccess: (_, { connectionId, app }) => { onSuccess: ({ projectId, app }) => {
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() }); queryClient.invalidateQueries({ queryKey: appConnectionKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) }); queryClient.invalidateQueries({ queryKey: appConnectionKeys.listAvailable(app, projectId) });
// queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) });
} }
}); });
}; };
@@ -50,9 +55,28 @@ export const useDeleteAppConnection = () => {
return data; return data;
}, },
onSuccess: (_, { connectionId, app }) => { onSuccess: ({ projectId, app }) => {
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() }); queryClient.invalidateQueries({ queryKey: appConnectionKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) }); 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 { AppConnection } from "@app/hooks/api/appConnections/enums";
import { import {
TAppConnection, TAppConnection,
TAppConnectionMap,
TAppConnectionOptions, TAppConnectionOptions,
TAvailableAppConnection, TAvailableAppConnection,
TAvailableAppConnectionsResponse, TAvailableAppConnectionsResponse,
TGetAppConnection,
TListAppConnections TListAppConnections
} from "@app/hooks/api/appConnections/types"; } from "@app/hooks/api/appConnections/types";
import { import {
TAppConnectionOption, TAppConnectionOption,
TAppConnectionOptionMap TAppConnectionOptionMap
} from "@app/hooks/api/appConnections/types/app-options"; } from "@app/hooks/api/appConnections/types/app-options";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const appConnectionKeys = { export const appConnectionKeys = {
all: ["app-connection"] as const, all: ["app-connection"] as const,
options: () => [...appConnectionKeys.all, "options"] as const, options: (projectType?: ProjectType) =>
list: () => [...appConnectionKeys.all, "list"] as const, [...appConnectionKeys.all, "options", ...(projectType ? [projectType] : [])] as const,
listAvailable: (app: AppConnection) => [...appConnectionKeys.all, app, "list-available"] as const, list: (projectId?: string | null) =>
listByApp: (app: AppConnection) => [...appConnectionKeys.list(), app], [...appConnectionKeys.all, "list", ...(projectId ? [projectId] : [])] as const,
byId: (app: AppConnection, connectionId: string) => listAvailable: (app: AppConnection, projectId?: string | null) =>
[...appConnectionKeys.all, app, "by-id", connectionId] as const [...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 = ( export const useAppConnectionOptions = (
projectType?: ProjectType,
options?: Omit< options?: Omit<
UseQueryOptions< UseQueryOptions<
TAppConnectionOption[], TAppConnectionOption[],
@@ -39,10 +46,11 @@ export const useAppConnectionOptions = (
> >
) => { ) => {
return useQuery({ return useQuery({
queryKey: appConnectionKeys.options(), queryKey: appConnectionKeys.options(projectType),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<TAppConnectionOptions>( const { data } = await apiRequest.get<TAppConnectionOptions>(
"/api/v1/app-connections/options" "/api/v1/app-connections/options",
{ params: { projectType } }
); );
return data.appConnectionOptions; return data.appConnectionOptions;
@@ -64,6 +72,7 @@ export const useGetAppConnectionOption = <T extends AppConnection>(app: T) => {
}; };
export const useListAppConnections = ( export const useListAppConnections = (
projectId?: string,
options?: Omit< options?: Omit<
UseQueryOptions< UseQueryOptions<
TAppConnection[], TAppConnection[],
@@ -75,10 +84,12 @@ export const useListAppConnections = (
> >
) => { ) => {
return useQuery({ return useQuery({
queryKey: appConnectionKeys.list(), queryKey: appConnectionKeys.list(projectId),
queryFn: async () => { queryFn: async () => {
const { data } = const { data } = await apiRequest.get<TListAppConnections<TAppConnection>>(
await apiRequest.get<TListAppConnections<TAppConnection>>("/api/v1/app-connections"); "/api/v1/app-connections",
{ params: { projectId } }
);
return data.appConnections; return data.appConnections;
}, },
@@ -88,6 +99,7 @@ export const useListAppConnections = (
export const useListAvailableAppConnections = ( export const useListAvailableAppConnections = (
app: AppConnection, app: AppConnection,
projectId: string,
options?: Omit< options?: Omit<
UseQueryOptions< UseQueryOptions<
TAvailableAppConnection[], TAvailableAppConnection[],
@@ -99,10 +111,11 @@ export const useListAvailableAppConnections = (
> >
) => { ) => {
return useQuery({ return useQuery({
queryKey: appConnectionKeys.listAvailable(app), queryKey: appConnectionKeys.listAvailable(app, projectId),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<TAvailableAppConnectionsResponse>( const { data } = await apiRequest.get<TAvailableAppConnectionsResponse>(
`/api/v1/app-connections/${app}/available` `/api/v1/app-connections/${app}/available`,
{ params: { projectId } }
); );
return data.appConnections; return data.appConnections;
@@ -111,53 +124,82 @@ export const useListAvailableAppConnections = (
}); });
}; };
export const useListAppConnectionsByApp = <T extends AppConnection>( // scott: may need these in the future but not using now
app: T, // export const useGetAppConnectionUsageById = (
options?: Omit< // app: AppConnection,
UseQueryOptions< // connectionId: string,
TAppConnectionMap[T][], // options?: Omit<
unknown, // UseQueryOptions<
TAppConnectionMap[T][], // AppConnectionUsage,
ReturnType<typeof appConnectionKeys.listByApp> // unknown,
>, // AppConnectionUsage,
"queryKey" | "queryFn" // ReturnType<typeof appConnectionKeys.getUsage>
> // >,
) => { // "queryKey" | "queryFn"
return useQuery({ // >
queryKey: appConnectionKeys.listByApp(app), // ) => {
queryFn: async () => { // return useQuery({
const { data } = await apiRequest.get<TListAppConnections<TAppConnectionMap[T]>>( // queryKey: appConnectionKeys.getUsage(app, connectionId),
`/api/v1/app-connections/${app}` // queryFn: async () => {
); // const { data } = await apiRequest.get<AppConnectionUsage>(
// `/api/v1/app-connections/${app}/${connectionId}/usage`
// );
//
// return data;
// },
// ...options
// });
// };
return data.appConnections; // scott: may need these in the future but not using now
}, // export const useListAppConnectionsByApp = <T extends AppConnection>(
...options // 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>( // scott: we will need this once we have individual app connection page
app: T, // export const useGetAppConnectionById = <T extends AppConnection>(
connectionId: string, // app: T,
options?: Omit< // connectionId: string,
UseQueryOptions< // options?: Omit<
TAppConnectionMap[T], // UseQueryOptions<
unknown, // TAppConnectionMap[T],
TAppConnectionMap[T], // unknown,
ReturnType<typeof appConnectionKeys.byId> // TAppConnectionMap[T],
>, // ReturnType<typeof appConnectionKeys.byId>
"queryKey" | "queryFn" // >,
> // "queryKey" | "queryFn"
) => { // >
return useQuery({ // ) => {
queryKey: appConnectionKeys.byId(app, connectionId), // return useQuery({
queryFn: async () => { // queryKey: appConnectionKeys.byId(app, connectionId),
const { data } = await apiRequest.get<TGetAppConnection<TAppConnectionMap[T]>>( // queryFn: async () => {
`/api/v1/app-connections/${app}/${connectionId}` // const { data } = await apiRequest.get<TGetAppConnection<TAppConnectionMap[T]>>(
); // `/api/v1/app-connections/${app}/${connectionId}`
// );
return data.appConnection; //
}, // return data.appConnection;
...options // },
}); // ...options
}; // });
// };

View File

@@ -1,3 +1,5 @@
import { ProjectType } from "@app/hooks/api/workspace/types";
import { AppConnection } from "../enums"; import { AppConnection } from "../enums";
import { TOnePassConnection } from "./1password-connection"; import { TOnePassConnection } from "./1password-connection";
import { TAppConnectionOption } from "./app-options"; import { TAppConnectionOption } from "./app-options";
@@ -116,10 +118,11 @@ export type TAppConnection =
| TNetlifyConnection | TNetlifyConnection
| TOktaConnection; | 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 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 TAppConnectionOptions = { appConnectionOptions: TAppConnectionOption[] };
export type TAppConnectionResponse = { appConnection: TAppConnection }; export type TAppConnectionResponse = { appConnection: TAppConnection };
export type TAvailableAppConnectionsResponse = { appConnections: TAvailableAppConnection[] }; export type TAvailableAppConnectionsResponse = { appConnections: TAvailableAppConnection[] };
@@ -133,6 +136,7 @@ export type TCreateAppConnectionDTO = Pick<
| "description" | "description"
| "isPlatformManagedCredentials" | "isPlatformManagedCredentials"
| "gatewayId" | "gatewayId"
| "projectId"
>; >;
export type TUpdateAppConnectionDTO = Partial< export type TUpdateAppConnectionDTO = Partial<
@@ -150,43 +154,64 @@ export type TDeleteAppConnectionDTO = {
connectionId: string; connectionId: string;
}; };
export type TAppConnectionMap = { // scott: we will need this once we have individual app connection page
[AppConnection.AWS]: TAwsConnection; // export type TAppConnectionMap = {
[AppConnection.GitHub]: TGitHubConnection; // [AppConnection.AWS]: TAwsConnection;
[AppConnection.GitHubRadar]: TGitHubRadarConnection; // [AppConnection.GitHub]: TGitHubConnection;
[AppConnection.GCP]: TGcpConnection; // [AppConnection.GitHubRadar]: TGitHubRadarConnection;
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnection; // [AppConnection.GCP]: TGcpConnection;
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection; // [AppConnection.AzureKeyVault]: TAzureKeyVaultConnection;
[AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection; // [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
[AppConnection.AzureDevOps]: TAzureDevOpsConnection; // [AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection;
[AppConnection.AzureADCS]: TAzureADCSConnection; // [AppConnection.AzureDevOps]: TAzureDevOpsConnection;
[AppConnection.Databricks]: TDatabricksConnection; // [AppConnection.AzureADCS]: TAzureADCSConnection;
[AppConnection.Humanitec]: THumanitecConnection; // [AppConnection.Databricks]: TDatabricksConnection;
[AppConnection.TerraformCloud]: TTerraformCloudConnection; // [AppConnection.Humanitec]: THumanitecConnection;
[AppConnection.Vercel]: TVercelConnection; // [AppConnection.TerraformCloud]: TTerraformCloudConnection;
[AppConnection.Postgres]: TPostgresConnection; // [AppConnection.Vercel]: TVercelConnection;
[AppConnection.MsSql]: TMsSqlConnection; // [AppConnection.Postgres]: TPostgresConnection;
[AppConnection.MySql]: TMySqlConnection; // [AppConnection.MsSql]: TMsSqlConnection;
[AppConnection.OracleDB]: TOracleDBConnection; // [AppConnection.MySql]: TMySqlConnection;
[AppConnection.Camunda]: TCamundaConnection; // [AppConnection.OracleDB]: TOracleDBConnection;
[AppConnection.Windmill]: TWindmillConnection; // [AppConnection.Camunda]: TCamundaConnection;
[AppConnection.Auth0]: TAuth0Connection; // [AppConnection.Windmill]: TWindmillConnection;
[AppConnection.HCVault]: THCVaultConnection; // [AppConnection.Auth0]: TAuth0Connection;
[AppConnection.LDAP]: TLdapConnection; // [AppConnection.HCVault]: THCVaultConnection;
[AppConnection.TeamCity]: TTeamCityConnection; // [AppConnection.LDAP]: TLdapConnection;
[AppConnection.OCI]: TOCIConnection; // [AppConnection.TeamCity]: TTeamCityConnection;
[AppConnection.OnePass]: TOnePassConnection; // [AppConnection.OCI]: TOCIConnection;
[AppConnection.Heroku]: THerokuConnection; // [AppConnection.OnePass]: TOnePassConnection;
[AppConnection.Render]: TRenderConnection; // [AppConnection.Heroku]: THerokuConnection;
[AppConnection.Flyio]: TFlyioConnection; // [AppConnection.Render]: TRenderConnection;
[AppConnection.GitLab]: TGitLabConnection; // [AppConnection.Flyio]: TFlyioConnection;
[AppConnection.Cloudflare]: TCloudflareConnection; // [AppConnection.GitLab]: TGitLabConnection;
[AppConnection.Bitbucket]: TBitbucketConnection; // [AppConnection.Cloudflare]: TCloudflareConnection;
[AppConnection.Zabbix]: TZabbixConnection; // [AppConnection.Bitbucket]: TBitbucketConnection;
[AppConnection.Railway]: TRailwayConnection; // [AppConnection.Zabbix]: TZabbixConnection;
[AppConnection.Checkly]: TChecklyConnection; // [AppConnection.Railway]: TRailwayConnection;
[AppConnection.Supabase]: TSupabaseConnection; // [AppConnection.Checkly]: TChecklyConnection;
[AppConnection.DigitalOcean]: TDigitalOceanConnection; // [AppConnection.Supabase]: TSupabaseConnection;
[AppConnection.Netlify]: TNetlifyConnection; // [AppConnection.DigitalOcean]: TDigitalOceanConnection;
[AppConnection.Okta]: TOktaConnection; // [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 = { export type TRootAppConnection = {
id: string; id: string;
name: string; name: string;
@@ -8,4 +10,11 @@ export type TRootAppConnection = {
updatedAt: string; updatedAt: string;
isPlatformManagedCredentials?: boolean; isPlatformManagedCredentials?: boolean;
gatewayId?: string | null; 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.CREATE_APP_CONNECTION]: "Create App Connection",
[EventType.UPDATE_APP_CONNECTION]: "Update App Connection", [EventType.UPDATE_APP_CONNECTION]: "Update App Connection",
[EventType.DELETE_APP_CONNECTION]: "Delete 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_SYNCS]: "List secret syncs",
[EventType.GET_SECRET_SYNC]: "Get Secret Sync", [EventType.GET_SECRET_SYNC]: "Get Secret Sync",
[EventType.CREATE_SECRET_SYNC]: "Create Secret Sync", [EventType.CREATE_SECRET_SYNC]: "Create Secret Sync",

View File

@@ -140,6 +140,8 @@ export enum EventType {
CREATE_APP_CONNECTION = "create-app-connection", CREATE_APP_CONNECTION = "create-app-connection",
UPDATE_APP_CONNECTION = "update-app-connection", UPDATE_APP_CONNECTION = "update-app-connection",
DELETE_APP_CONNECTION = "delete-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_SYNCS = "get-secret-syncs",
GET_SECRET_SYNC = "get-secret-sync", GET_SECRET_SYNC = "get-secret-sync",
CREATE_SECRET_SYNC = "create-secret-sync", CREATE_SECRET_SYNC = "create-secret-sync",

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