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
@@ -1,3 +1,4 @@
import { ProjectType } from "@app/db/schemas";
import { TAppConnections } from "@app/db/schemas/app-connections";
import {
getOCIConnectionListItem,
@@ -8,6 +9,8 @@ import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { SECRET_ROTATION_CONNECTION_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-maps";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { APP_CONNECTION_NAME_MAP, APP_CONNECTION_PLAN_MAP } from "@app/services/app-connection/app-connection-maps";
@@ -16,6 +19,7 @@ import {
validateSqlConnectionCredentials
} from "@app/services/app-connection/shared/sql";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps";
import {
getOnePassConnectionListItem,
@@ -133,7 +137,19 @@ import {
} from "./windmill";
import { getZabbixConnectionListItem, validateZabbixConnectionCredentials, ZabbixConnectionMethod } from "./zabbix";
export const listAppConnectionOptions = () => {
const SECRET_SYNC_APP_CONNECTION_MAP = Object.fromEntries(
Object.entries(SECRET_SYNC_CONNECTION_MAP).map(([key, value]) => [value, key])
);
const SECRET_ROTATION_APP_CONNECTION_MAP = Object.fromEntries(
Object.entries(SECRET_ROTATION_CONNECTION_MAP).map(([key, value]) => [value, key])
);
const SECRET_SCANNING_APP_CONNECTION_MAP = Object.fromEntries(
Object.entries(SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP).map(([key, value]) => [value, key])
);
export const listAppConnectionOptions = (projectType?: ProjectType) => {
return [
getAwsConnectionListItem(),
getGitHubConnectionListItem(),
@@ -173,22 +189,55 @@ export const listAppConnectionOptions = () => {
getDigitalOceanConnectionListItem(),
getNetlifyConnectionListItem(),
getOktaConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
]
.filter((option) => {
switch (projectType) {
case ProjectType.SecretManager:
return (
Boolean(SECRET_SYNC_APP_CONNECTION_MAP[option.app]) ||
Boolean(SECRET_ROTATION_APP_CONNECTION_MAP[option.app])
);
case ProjectType.SecretScanning:
return Boolean(SECRET_SCANNING_APP_CONNECTION_MAP[option.app]);
case ProjectType.CertificateManager:
return (
option.app === AppConnection.AWS ||
option.app === AppConnection.Cloudflare ||
option.app === AppConnection.AzureADCS
);
case ProjectType.KMS:
return false;
case ProjectType.SSH:
return false;
default:
return true;
}
})
.sort((a, b) => a.name.localeCompare(b.name));
};
export const encryptAppConnectionCredentials = async ({
orgId,
credentials,
kmsService
kmsService,
projectId
}: {
orgId: string;
credentials: TAppConnection["credentials"];
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
projectId: string | null | undefined;
}) => {
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
});
const { encryptor } = await kmsService.createCipherPairWithDataKey(
projectId
? {
type: KmsDataKey.SecretManager,
projectId
}
: {
type: KmsDataKey.Organization,
orgId
}
);
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
plainText: Buffer.from(JSON.stringify(credentials))
@@ -200,16 +249,22 @@ export const encryptAppConnectionCredentials = async ({
export const decryptAppConnectionCredentials = async ({
orgId,
encryptedCredentials,
kmsService
kmsService,
projectId
}: {
orgId: string;
encryptedCredentials: Buffer;
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
projectId: string | null | undefined;
}) => {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
});
const { decryptor } = await kmsService.createCipherPairWithDataKey(
projectId
? { type: KmsDataKey.SecretManager, projectId }
: {
type: KmsDataKey.Organization,
orgId
}
);
const decryptedPlainTextBlob = decryptor({
cipherTextBlob: encryptedCredentials
@@ -343,6 +398,7 @@ export const decryptAppConnection = async (
credentials: await decryptAppConnectionCredentials({
encryptedCredentials: appConnection.encryptedCredentials,
orgId: appConnection.orgId,
projectId: appConnection.projectId,
kmsService
}),
credentialsHash: crypto.nativeCrypto.createHash("sha256").update(appConnection.encryptedCredentials).digest("hex")
@@ -413,3 +469,73 @@ export const enterpriseAppCheck = async (
});
}
};
type Resource = {
name: string;
id: string;
projectId: string;
projectName: string;
projectSlug: string;
projectType: string;
};
type UsageData = {
secretSyncs: Resource[];
secretRotations: Resource[];
dataSources: Resource[];
externalCas: Resource[];
};
type ResourceSummary = {
name: string;
id: string;
};
type ProjectWithResources = {
id: string;
name: string;
slug: string;
type: ProjectType;
resources: {
secretSyncs: ResourceSummary[];
secretRotations: ResourceSummary[];
dataSources: ResourceSummary[];
externalCas: (ResourceSummary & { appConnectionId?: string; dnsAppConnectionId?: string })[];
};
};
export const transformUsageToProjects = (data: UsageData): ProjectWithResources[] => {
const projectMap = new Map<string, ProjectWithResources>();
Object.entries(data).forEach(([resourceType, resources]) => {
resources.forEach((resource) => {
const { projectId, projectName, projectSlug, projectType, name, id, ...rest } = resource;
const projectKey = projectId;
if (!projectMap.has(projectKey)) {
projectMap.set(projectKey, {
id: projectId,
name: projectName,
slug: projectSlug,
type: projectType as ProjectType,
resources: {
secretSyncs: [],
secretRotations: [],
dataSources: [],
externalCas: []
}
});
}
const project = projectMap.get(projectKey)!;
project.resources[resourceType as keyof ProjectWithResources["resources"]].push({
name,
id,
...rest
});
});
});
return Array.from(projectMap.values());
};