feat: half-way done through integrating with platform

This commit is contained in:
Sheen Capadngan
2025-09-02 03:46:57 +08:00
parent 81dfcb5de1
commit 879361fd7a
20 changed files with 683 additions and 111 deletions
@@ -6,6 +6,7 @@ import {
} from "@app/ee/services/app-connections/oci";
import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee/services/app-connections/oracledb";
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 { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
@@ -213,7 +214,8 @@ export const decryptAppConnectionCredentials = async ({
export const validateAppConnectionCredentials = async (
appConnection: TAppConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
): Promise<TAppConnection["credentials"]> => {
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
@@ -257,7 +259,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService);
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service);
};
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
@@ -5,6 +5,8 @@ import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-c
import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb";
import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGatewayV2DALFactory } from "@app/ee/services/gateway-v2/gateway-v2-dal";
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
OrgPermissionAppConnectionActions,
@@ -109,7 +111,9 @@ export type TAppConnectionServiceFactoryDep = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
gatewayDAL: Pick<TGatewayDALFactory, "find">;
gatewayV2DAL: Pick<TGatewayV2DALFactory, "find">;
};
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
@@ -160,7 +164,9 @@ export const appConnectionServiceFactory = ({
kmsService,
licenseService,
gatewayService,
gatewayDAL
gatewayV2Service,
gatewayDAL,
gatewayV2DAL
}: TAppConnectionServiceFactoryDep) => {
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => {
const { permission } = await permissionService.getOrgPermission(
@@ -264,7 +270,8 @@ export const appConnectionServiceFactory = ({
);
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId });
if (!gateway) {
const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: actor.orgId });
if (!gateway && !gatewayV2) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found for org`
});
@@ -286,7 +293,8 @@ export const appConnectionServiceFactory = ({
orgId: actor.orgId,
gatewayId
} as TAppConnectionConfig,
gatewayService
gatewayService,
gatewayV2Service
);
try {
@@ -319,7 +327,8 @@ export const appConnectionServiceFactory = ({
gatewayId
} as TAppConnectionConfig,
(platformCredentials) => createConnection(platformCredentials),
gatewayService
gatewayService,
gatewayV2Service
);
} else {
connection = await createConnection(validatedCredentials);
@@ -415,7 +424,8 @@ export const appConnectionServiceFactory = ({
method,
gatewayId
} as TAppConnectionConfig,
gatewayService
gatewayService,
gatewayV2Service
);
if (!updatedCredentials)
@@ -456,7 +466,8 @@ export const appConnectionServiceFactory = ({
gatewayId
} as TAppConnectionConfig,
(platformCredentials) => updateConnection(platformCredentials),
gatewayService
gatewayService,
gatewayV2Service
);
} else {
updatedConnection = await updateConnection(updatedCredentials);
@@ -10,6 +10,7 @@ import {
TValidateOracleDBConnectionCredentialsSchema
} from "@app/ee/services/app-connections/oracledb";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
@@ -401,13 +402,15 @@ export type TListAwsConnectionIamUsers = {
export type TAppConnectionCredentialsValidator = (
appConnection: TAppConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
) => Promise<TAppConnection["credentials"]>;
export type TAppConnectionTransitionCredentialsToPlatform = (
appConnection: TAppConnectionConfig,
callback: (credentials: TAppConnection["credentials"]) => Promise<TAppConnectionRaw>,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
) => Promise<TAppConnectionRaw>;
export type TAppConnectionBaseConfig = {
@@ -2,12 +2,14 @@ import knex, { Knex } from "knex";
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
import {
TSqlCredentialsRotationGeneratedCredentials,
TSqlCredentialsRotationWithConnection
} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TAppConnectionRaw, TSqlConnection } from "@app/services/app-connection/app-connection-types";
@@ -104,12 +106,49 @@ export const getSqlConnectionClient = async (appConnection: Pick<TSqlConnection,
export const executeWithPotentialGateway = async <T>(
config: TSqlConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
operation: (client: Knex) => Promise<T>
): Promise<T> => {
const { credentials, app, gatewayId } = config;
if (gatewayId && gatewayService) {
if (gatewayId && gatewayService && gatewayV2Service) {
const [targetHost] = await verifyHostInputValidity(credentials.host, true);
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
gatewayId,
targetHost,
targetPort: credentials.port
});
if (platformConnectionDetails) {
return withGatewayV2Proxy(
async (proxyPort) => {
const client = knex({
client: SQL_CONNECTION_CLIENT_MAP[app],
connection: {
database: credentials.database,
port: proxyPort,
host: "localhost",
user: credentials.username,
password: credentials.password,
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
...getConnectionConfig({ app, credentials })
}
});
try {
return await operation(client);
} finally {
await client.destroy();
}
},
{
protocol: GatewayProxyProtocol.Tcp,
proxyIp: platformConnectionDetails.proxyIp,
gateway: platformConnectionDetails.gateway,
proxy: platformConnectionDetails.proxy
}
);
}
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
@@ -161,10 +200,11 @@ export const executeWithPotentialGateway = async <T>(
export const validateSqlConnectionCredentials = async (
config: TSqlConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
) => {
try {
await executeWithPotentialGateway(config, gatewayService, async (client) => {
await executeWithPotentialGateway(config, gatewayService, gatewayV2Service, async (client) => {
await client.raw(config.app === AppConnection.OracleDB ? `SELECT 1 FROM DUAL` : `Select 1`);
});
return config.credentials;
@@ -191,14 +231,15 @@ export const SQL_CONNECTION_ALTER_LOGIN_STATEMENT: Record<
export const transferSqlConnectionCredentialsToPlatform = async (
config: TSqlConnectionConfig,
callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise<TAppConnectionRaw>,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
) => {
const { credentials, app } = config;
const newPassword = alphaNumericNanoId(32);
try {
return await executeWithPotentialGateway(config, gatewayService, (client) => {
return await executeWithPotentialGateway(config, gatewayService, gatewayV2Service, (client) => {
return client.transaction(async (tx) => {
await tx.raw(
...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword })
@@ -6,6 +6,8 @@ import RE2 from "re2";
import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas";
import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGatewayV2DALFactory } from "@app/ee/services/gateway-v2/gateway-v2-dal";
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
OrgPermissionGatewayActions,
@@ -21,6 +23,7 @@ import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
@@ -54,11 +57,15 @@ type TIdentityKubernetesAuthServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
gatewayService: TGatewayServiceFactory;
gatewayV2Service: TGatewayV2ServiceFactory;
gatewayDAL: Pick<TGatewayDALFactory, "find">;
gatewayV2DAL: Pick<TGatewayV2DALFactory, "find">;
};
export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>;
const GATEWAY_AUTH_DEFAULT_HOST = "https://kubernetes.default.svc.cluster.local";
export const identityKubernetesAuthServiceFactory = ({
identityKubernetesAuthDAL,
identityOrgMembershipDAL,
@@ -66,7 +73,9 @@ export const identityKubernetesAuthServiceFactory = ({
permissionService,
licenseService,
gatewayService,
gatewayV2Service,
gatewayDAL,
gatewayV2DAL,
kmsService
}: TIdentityKubernetesAuthServiceFactoryDep) => {
const $gatewayProxyWrapper = async <T>(
@@ -79,6 +88,42 @@ export const identityKubernetesAuthServiceFactory = ({
},
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
): Promise<T> => {
const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
gatewayId: inputs.gatewayId,
targetHost: inputs.targetHost ?? GATEWAY_AUTH_DEFAULT_HOST,
targetPort: inputs.targetPort ?? 443
});
if (gatewayV2ConnectionDetails) {
let httpsAgent: https.Agent | undefined;
if (!inputs.reviewTokenThroughGateway) {
httpsAgent = new https.Agent({
ca: inputs.caCert,
rejectUnauthorized: Boolean(inputs.caCert)
});
}
const callbackResult = await withGatewayV2Proxy(
async (port) => {
const res = await gatewayCallback(
inputs.reviewTokenThroughGateway ? "http://localhost" : "https://localhost",
port,
httpsAgent
);
return res;
},
{
protocol: inputs.reviewTokenThroughGateway ? GatewayProxyProtocol.Http : GatewayProxyProtocol.Tcp,
proxyIp: gatewayV2ConnectionDetails.proxyIp,
gateway: gatewayV2ConnectionDetails.gateway,
proxy: gatewayV2ConnectionDetails.proxy,
httpsAgent
}
);
return callbackResult;
}
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
@@ -277,7 +322,7 @@ export const identityKubernetesAuthServiceFactory = ({
let data: TCreateTokenReviewResponse | undefined;
if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) {
if (!identityKubernetesAuth.gatewayId) {
if (!identityKubernetesAuth.gatewayId && !identityKubernetesAuth.gatewayV2Id) {
throw new BadRequestError({
message: "Gateway ID is required when token review mode is set to Gateway"
});
@@ -285,7 +330,7 @@ export const identityKubernetesAuthServiceFactory = ({
data = await $gatewayProxyWrapper(
{
gatewayId: identityKubernetesAuth.gatewayId,
gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string,
reviewTokenThroughGateway: true
},
tokenReviewCallbackThroughGateway
@@ -304,17 +349,18 @@ export const identityKubernetesAuthServiceFactory = ({
const [k8sHost, k8sPort] = kubernetesHost.split(":");
data = identityKubernetesAuth.gatewayId
? await $gatewayProxyWrapper(
{
gatewayId: identityKubernetesAuth.gatewayId,
targetHost: k8sHost,
targetPort: k8sPort ? Number(k8sPort) : 443,
reviewTokenThroughGateway: false
},
tokenReviewCallbackRaw
)
: await tokenReviewCallbackRaw();
data =
identityKubernetesAuth.gatewayId || identityKubernetesAuth.gatewayV2Id
? await $gatewayProxyWrapper(
{
gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string,
targetHost: k8sHost,
targetPort: k8sPort ? Number(k8sPort) : 443,
reviewTokenThroughGateway: false
},
tokenReviewCallbackRaw
)
: await tokenReviewCallbackRaw();
} else {
throw new BadRequestError({
message: `Invalid token review mode: ${identityKubernetesAuth.tokenReviewMode}`
@@ -490,14 +536,20 @@ export const identityKubernetesAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
let isGatewayV1 = true;
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId });
if (!gateway) {
const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId });
if (!gateway && !gatewayV2) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
if (!gateway) {
isGatewayV1 = false;
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
@@ -528,7 +580,8 @@ export const identityKubernetesAuthServiceFactory = ({
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
gatewayId,
gatewayId: isGatewayV1 ? gatewayId : null,
gatewayV2Id: isGatewayV1 ? null : gatewayId,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
encryptedKubernetesTokenReviewerJwt: tokenReviewerJwt
? encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob
@@ -608,14 +661,21 @@ export const identityKubernetesAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
let isGatewayV1 = true;
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId });
if (!gateway) {
const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId });
if (!gateway && !gatewayV2) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
if (!gateway) {
isGatewayV1 = false;
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
@@ -629,13 +689,18 @@ export const identityKubernetesAuthServiceFactory = ({
);
}
const shouldUpdateGatewayId = Boolean(gatewayId);
const gatewayIdValue = isGatewayV1 ? gatewayId : null;
const gatewayV2IdValue = isGatewayV1 ? null : gatewayId;
const updateQuery: TIdentityKubernetesAuthsUpdate = {
kubernetesHost,
tokenReviewMode,
allowedNamespaces,
allowedNames,
allowedAudience,
gatewayId,
gatewayId: shouldUpdateGatewayId ? gatewayIdValue : undefined,
gatewayV2Id: shouldUpdateGatewayId ? gatewayV2IdValue : undefined,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
@@ -730,7 +795,13 @@ export const identityKubernetesAuthServiceFactory = ({
}).toString();
}
return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId };
return {
...identityKubernetesAuth,
caCert,
tokenReviewerJwt,
orgId: identityMembershipOrg.orgId,
gatewayId: identityKubernetesAuth.gatewayId ?? identityKubernetesAuth.gatewayV2Id
};
};
const revokeIdentityKubernetesAuth = async ({