mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 11:38:55 +00:00
Merge branch 'main' into feat/azureClientSecretsRotation
This commit is contained in:
@@ -13,7 +13,9 @@ export enum AppConnection {
|
||||
MsSql = "mssql",
|
||||
Camunda = "camunda",
|
||||
Windmill = "windmill",
|
||||
Auth0 = "auth0"
|
||||
Auth0 = "auth0",
|
||||
LDAP = "ldap",
|
||||
TeamCity = "teamcity"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -46,8 +46,14 @@ import {
|
||||
HumanitecConnectionMethod,
|
||||
validateHumanitecConnectionCredentials
|
||||
} from "./humanitec";
|
||||
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
|
||||
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||
import {
|
||||
getTeamCityConnectionListItem,
|
||||
TeamCityConnectionMethod,
|
||||
validateTeamCityConnectionCredentials
|
||||
} from "./teamcity";
|
||||
import {
|
||||
getTerraformCloudConnectionListItem,
|
||||
TerraformCloudConnectionMethod,
|
||||
@@ -77,7 +83,9 @@ export const listAppConnectionOptions = () => {
|
||||
getCamundaConnectionListItem(),
|
||||
getAzureClientSecretsConnectionListItem(),
|
||||
getWindmillConnectionListItem(),
|
||||
getAuth0ConnectionListItem()
|
||||
getAuth0ConnectionListItem(),
|
||||
getLdapConnectionListItem(),
|
||||
getTeamCityConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@@ -143,7 +151,9 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
@@ -176,9 +186,12 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
return "Username & Password";
|
||||
case WindmillConnectionMethod.AccessToken:
|
||||
case TeamCityConnectionMethod.AccessToken:
|
||||
return "Access Token";
|
||||
case Auth0ConnectionMethod.ClientCredentials:
|
||||
return "Client Credentials";
|
||||
case LdapConnectionMethod.SimpleBind:
|
||||
return "Simple Bind";
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
@@ -224,5 +237,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureClientSecrets]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
|
||||
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
@@ -15,5 +15,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.MsSql]: "Microsoft SQL Server",
|
||||
[AppConnection.Camunda]: "Camunda",
|
||||
[AppConnection.Windmill]: "Windmill",
|
||||
[AppConnection.Auth0]: "Auth0"
|
||||
[AppConnection.Auth0]: "Auth0",
|
||||
[AppConnection.LDAP]: "LDAP",
|
||||
[AppConnection.TeamCity]: "TeamCity"
|
||||
};
|
||||
|
||||
@@ -45,8 +45,11 @@ import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||
import { githubConnectionService } from "./github/github-connection-service";
|
||||
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
|
||||
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
|
||||
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
|
||||
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
|
||||
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
|
||||
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
|
||||
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
|
||||
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
|
||||
@@ -77,7 +80,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
|
||||
[AppConnection.AzureClientSecrets]: ValidateAzureClientSecretsConnectionCredentialsSchema,
|
||||
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
|
||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
|
||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
|
||||
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
|
||||
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -454,6 +459,7 @@ export const appConnectionServiceFactory = ({
|
||||
vercel: vercelConnectionService(connectAppConnectionById),
|
||||
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
windmill: windmillConnectionService(connectAppConnectionById)
|
||||
windmill: windmillConnectionService(connectAppConnectionById),
|
||||
teamcity: teamcityConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -63,12 +63,24 @@ import {
|
||||
THumanitecConnectionInput,
|
||||
TValidateHumanitecConnectionCredentialsSchema
|
||||
} from "./humanitec";
|
||||
import {
|
||||
TLdapConnection,
|
||||
TLdapConnectionConfig,
|
||||
TLdapConnectionInput,
|
||||
TValidateLdapConnectionCredentialsSchema
|
||||
} from "./ldap";
|
||||
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import {
|
||||
TPostgresConnection,
|
||||
TPostgresConnectionInput,
|
||||
TValidatePostgresConnectionCredentialsSchema
|
||||
} from "./postgres";
|
||||
import {
|
||||
TTeamCityConnection,
|
||||
TTeamCityConnectionConfig,
|
||||
TTeamCityConnectionInput,
|
||||
TValidateTeamCityConnectionCredentialsSchema
|
||||
} from "./teamcity";
|
||||
import {
|
||||
TTerraformCloudConnection,
|
||||
TTerraformCloudConnectionConfig,
|
||||
@@ -104,6 +116,8 @@ export type TAppConnection = { id: string } & (
|
||||
| TAzureClientSecretsConnection
|
||||
| TWindmillConnection
|
||||
| TAuth0Connection
|
||||
| TLdapConnection
|
||||
| TTeamCityConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -126,6 +140,8 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TAzureClientSecretsConnectionInput
|
||||
| TWindmillConnectionInput
|
||||
| TAuth0ConnectionInput
|
||||
| TLdapConnectionInput
|
||||
| TTeamCityConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
|
||||
@@ -153,7 +169,9 @@ export type TAppConnectionConfig =
|
||||
| TCamundaConnectionConfig
|
||||
| TVercelConnectionConfig
|
||||
| TWindmillConnectionConfig
|
||||
| TAuth0ConnectionConfig;
|
||||
| TAuth0ConnectionConfig
|
||||
| TLdapConnectionConfig
|
||||
| TTeamCityConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -170,7 +188,9 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateVercelConnectionCredentialsSchema
|
||||
| TValidateTerraformCloudConnectionCredentialsSchema
|
||||
| TValidateWindmillConnectionCredentialsSchema
|
||||
| TValidateAuth0ConnectionCredentialsSchema;
|
||||
| TValidateAuth0ConnectionCredentialsSchema
|
||||
| TValidateLdapConnectionCredentialsSchema
|
||||
| TValidateTeamCityConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
@@ -178,6 +198,10 @@ export type TListAwsConnectionKmsKeys = {
|
||||
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||
};
|
||||
|
||||
export type TListAwsConnectionIamUsers = {
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
export type TAppConnectionCredentialsValidator = (
|
||||
appConnection: TAppConnectionConfig
|
||||
) => Promise<TAppConnection["credentials"]>;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
import AWS from "aws-sdk";
|
||||
import { AxiosError } from "axios";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { AwsConnectionMethod } from "./aws-connection-enums";
|
||||
@@ -90,9 +92,20 @@ export const validateAwsConnectionCredentials = async (appConnection: TAwsConnec
|
||||
const sts = new AWS.STS(awsConfig);
|
||||
|
||||
resp = await sts.getCallerIdentity().promise();
|
||||
} catch (e: unknown) {
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Error validating AWS connection credentials");
|
||||
|
||||
let message: string;
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message = (error.response?.data?.message as string) || error.message || "verify credentials";
|
||||
} else {
|
||||
message = (error as Error)?.message || "verify credentials";
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
message: `Unable to validate connection: ${message}`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ import AWS from "aws-sdk";
|
||||
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TListAwsConnectionKmsKeys } from "@app/services/app-connection/app-connection-types";
|
||||
import {
|
||||
TListAwsConnectionIamUsers,
|
||||
TListAwsConnectionKmsKeys
|
||||
} from "@app/services/app-connection/app-connection-types";
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@@ -70,6 +73,23 @@ const listAwsKmsKeys = async (
|
||||
return kmsKeys;
|
||||
};
|
||||
|
||||
const listAwsIamUsers = async (appConnection: TAwsConnection) => {
|
||||
const { credentials } = await getAwsConnectionConfig(appConnection);
|
||||
|
||||
const iam = new AWS.IAM({ credentials });
|
||||
|
||||
const userEntries: AWS.IAM.User[] = [];
|
||||
let userMarker: string | undefined;
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await iam.listUsers({ MaxItems: 100, Marker: userMarker }).promise();
|
||||
userEntries.push(...(response.Users || []));
|
||||
userMarker = response.Marker;
|
||||
} while (userMarker);
|
||||
|
||||
return userEntries;
|
||||
};
|
||||
|
||||
export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listKmsKeys = async (
|
||||
{ connectionId, region, destination }: TListAwsConnectionKmsKeys,
|
||||
@@ -82,7 +102,16 @@ export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
|
||||
return kmsKeys;
|
||||
};
|
||||
|
||||
const listIamUsers = async ({ connectionId }: TListAwsConnectionIamUsers, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.AWS, connectionId, actor);
|
||||
|
||||
const iamUsers = await listAwsIamUsers(appConnection);
|
||||
|
||||
return iamUsers;
|
||||
};
|
||||
|
||||
return {
|
||||
listKmsKeys
|
||||
listKmsKeys,
|
||||
listIamUsers
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./ldap-connection-enums";
|
||||
export * from "./ldap-connection-fns";
|
||||
export * from "./ldap-connection-schemas";
|
||||
export * from "./ldap-connection-types";
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum LdapConnectionMethod {
|
||||
SimpleBind = "simple-bind"
|
||||
}
|
||||
|
||||
export enum LdapProvider {
|
||||
ActiveDirectory = "active-directory"
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import ldap from "ldapjs";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { LdapConnectionMethod } from "./ldap-connection-enums";
|
||||
import { TLdapConnectionConfig } from "./ldap-connection-types";
|
||||
|
||||
export const getLdapConnectionListItem = () => {
|
||||
return {
|
||||
name: "LDAP" as const,
|
||||
app: AppConnection.LDAP as const,
|
||||
methods: Object.values(LdapConnectionMethod) as [LdapConnectionMethod.SimpleBind]
|
||||
};
|
||||
};
|
||||
|
||||
const LDAP_TIMEOUT = 15_000;
|
||||
|
||||
export const getLdapConnectionClient = async ({
|
||||
url,
|
||||
dn,
|
||||
password,
|
||||
sslCertificate,
|
||||
sslRejectUnauthorized = true
|
||||
}: TLdapConnectionConfig["credentials"]) => {
|
||||
await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
const isSSL = url.startsWith("ldaps");
|
||||
|
||||
return new Promise<ldap.Client>((resolve, reject) => {
|
||||
const client = ldap.createClient({
|
||||
url,
|
||||
timeout: LDAP_TIMEOUT,
|
||||
connectTimeout: LDAP_TIMEOUT,
|
||||
tlsOptions: isSSL
|
||||
? {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
ca: sslCertificate ? [sslCertificate] : undefined
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
|
||||
client.on("error", (err: Error) => {
|
||||
logger.error(err, "LDAP Error");
|
||||
client.destroy();
|
||||
reject(new Error(`Provider Error - ${err.message}`));
|
||||
});
|
||||
|
||||
client.on("connectError", (err: Error) => {
|
||||
logger.error(err, "LDAP Connection Error");
|
||||
client.destroy();
|
||||
reject(new Error(`Provider Connect Error - ${err.message}`));
|
||||
});
|
||||
|
||||
client.on("connectRefused", (err: Error) => {
|
||||
logger.error(err, "LDAP Connection Refused");
|
||||
client.destroy();
|
||||
reject(new Error(`Provider Connection Refused - ${err.message}`));
|
||||
});
|
||||
|
||||
client.on("connectTimeout", (err: Error) => {
|
||||
logger.error(err, "LDAP Connection Timeout");
|
||||
client.destroy();
|
||||
reject(new Error(`Provider Connection Timeout - ${err.message}`));
|
||||
});
|
||||
|
||||
client.on("connect", () => {
|
||||
client.bind(dn, password, (err) => {
|
||||
if (err) {
|
||||
logger.error(err, "LDAP Bind Error");
|
||||
reject(new Error(`Bind Error: ${err.message}`));
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
resolve(client);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => {
|
||||
let client: ldap.Client | undefined;
|
||||
|
||||
try {
|
||||
client = await getLdapConnectionClient(credentials);
|
||||
|
||||
// this shouldn't occur as handle connection error events in client but here as fallback
|
||||
if (!client.connected) {
|
||||
throw new BadRequestError({ message: "Unable to connect to LDAP server" });
|
||||
}
|
||||
|
||||
return credentials;
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection: ${(e as Error).message || "verify credentials"}`
|
||||
});
|
||||
} finally {
|
||||
client?.destroy();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { DistinguishedNameRegex } from "@app/lib/regex";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { LdapConnectionMethod, LdapProvider } from "./ldap-connection-enums";
|
||||
|
||||
export const LdapConnectionSimpleBindCredentialsSchema = z.object({
|
||||
provider: z.nativeEnum(LdapProvider).describe(AppConnections.CREDENTIALS.LDAP.provider),
|
||||
url: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "URL required")
|
||||
.regex(new RE2(/^ldaps?:\/\//))
|
||||
.describe(AppConnections.CREDENTIALS.LDAP.url),
|
||||
dn: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
|
||||
.min(1, "Distinguished Name (DN) required")
|
||||
.describe(AppConnections.CREDENTIALS.LDAP.dn),
|
||||
password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.LDAP.password),
|
||||
sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.LDAP.sslRejectUnauthorized),
|
||||
sslCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value) => value || undefined)
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.LDAP.sslCertificate)
|
||||
});
|
||||
|
||||
const BaseLdapConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.LDAP)
|
||||
});
|
||||
|
||||
export const LdapConnectionSchema = z.intersection(
|
||||
BaseLdapConnectionSchema,
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(LdapConnectionMethod.SimpleBind),
|
||||
credentials: LdapConnectionSimpleBindCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
export const SanitizedLdapConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseLdapConnectionSchema.extend({
|
||||
method: z.literal(LdapConnectionMethod.SimpleBind),
|
||||
credentials: LdapConnectionSimpleBindCredentialsSchema.pick({
|
||||
provider: true,
|
||||
url: true,
|
||||
dn: true,
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(LdapConnectionMethod.SimpleBind).describe(AppConnections.CREATE(AppConnection.LDAP).method),
|
||||
credentials: LdapConnectionSimpleBindCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.LDAP).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateLdapConnectionSchema = ValidateLdapConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP)
|
||||
);
|
||||
|
||||
export const UpdateLdapConnectionSchema = z
|
||||
.object({
|
||||
credentials: LdapConnectionSimpleBindCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.LDAP).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LDAP));
|
||||
|
||||
export const LdapConnectionListItemSchema = z.object({
|
||||
name: z.literal("LDAP"),
|
||||
app: z.literal(AppConnection.LDAP),
|
||||
// the below is preferable but currently breaks with our zod to json schema parser
|
||||
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
|
||||
methods: z.nativeEnum(LdapConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import {
|
||||
CreateLdapConnectionSchema,
|
||||
LdapConnectionSchema,
|
||||
ValidateLdapConnectionCredentialsSchema
|
||||
} from "./ldap-connection-schemas";
|
||||
|
||||
export type TLdapConnection = z.infer<typeof LdapConnectionSchema>;
|
||||
|
||||
export type TLdapConnectionInput = z.infer<typeof CreateLdapConnectionSchema> & {
|
||||
app: AppConnection.LDAP;
|
||||
};
|
||||
|
||||
export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema;
|
||||
|
||||
export type TLdapConnectionConfig = DiscriminativePick<TLdapConnection, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
@@ -31,7 +31,8 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [
|
||||
port: true,
|
||||
username: true,
|
||||
sslEnabled: true,
|
||||
sslRejectUnauthorized: true
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -29,7 +29,8 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method",
|
||||
port: true,
|
||||
username: true,
|
||||
sslEnabled: true,
|
||||
sslRejectUnauthorized: true
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./teamcity-connection-enums";
|
||||
export * from "./teamcity-connection-fns";
|
||||
export * from "./teamcity-connection-schemas";
|
||||
export * from "./teamcity-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum TeamCityConnectionMethod {
|
||||
AccessToken = "access-token"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { TeamCityConnectionMethod } from "./teamcity-connection-enums";
|
||||
import {
|
||||
TTeamCityConnection,
|
||||
TTeamCityConnectionConfig,
|
||||
TTeamCityListProjectsResponse
|
||||
} from "./teamcity-connection-types";
|
||||
|
||||
export const getTeamCityInstanceUrl = async (config: TTeamCityConnectionConfig) => {
|
||||
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||
|
||||
return instanceUrl;
|
||||
};
|
||||
|
||||
export const getTeamCityConnectionListItem = () => {
|
||||
return {
|
||||
name: "TeamCity" as const,
|
||||
app: AppConnection.TeamCity as const,
|
||||
methods: Object.values(TeamCityConnectionMethod) as [TeamCityConnectionMethod.AccessToken]
|
||||
};
|
||||
};
|
||||
|
||||
export const validateTeamCityConnectionCredentials = async (config: TTeamCityConnectionConfig) => {
|
||||
const instanceUrl = await getTeamCityInstanceUrl(config);
|
||||
|
||||
const { accessToken } = config.credentials;
|
||||
|
||||
try {
|
||||
await request.get(`${instanceUrl}/app/rest/server`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
||||
|
||||
export const listTeamCityProjects = async (appConnection: TTeamCityConnection) => {
|
||||
const instanceUrl = await getTeamCityInstanceUrl(appConnection);
|
||||
const { accessToken } = appConnection.credentials;
|
||||
|
||||
const resp = await request.get<TTeamCityListProjectsResponse>(
|
||||
`${instanceUrl}/app/rest/projects?fields=project(id,name,buildTypes(buildType(id,name)))`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Filter out the root project. Should not be seen by users.
|
||||
return resp.data.project.filter((proj) => proj.id !== "_Root");
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { TeamCityConnectionMethod } from "./teamcity-connection-enums";
|
||||
|
||||
export const TeamCityConnectionAccessTokenCredentialsSchema = z.object({
|
||||
accessToken: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Access Token required")
|
||||
.describe(AppConnections.CREDENTIALS.TEAMCITY.accessToken),
|
||||
instanceUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.url("Invalid Instance URL")
|
||||
.min(1, "Instance URL required")
|
||||
.describe(AppConnections.CREDENTIALS.TEAMCITY.instanceUrl)
|
||||
});
|
||||
|
||||
const BaseTeamCityConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.TeamCity) });
|
||||
|
||||
export const TeamCityConnectionSchema = BaseTeamCityConnectionSchema.extend({
|
||||
method: z.literal(TeamCityConnectionMethod.AccessToken),
|
||||
credentials: TeamCityConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedTeamCityConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseTeamCityConnectionSchema.extend({
|
||||
method: z.literal(TeamCityConnectionMethod.AccessToken),
|
||||
credentials: TeamCityConnectionAccessTokenCredentialsSchema.pick({
|
||||
instanceUrl: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateTeamCityConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(TeamCityConnectionMethod.AccessToken)
|
||||
.describe(AppConnections.CREATE(AppConnection.TeamCity).method),
|
||||
credentials: TeamCityConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.TeamCity).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateTeamCityConnectionSchema = ValidateTeamCityConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.TeamCity)
|
||||
);
|
||||
|
||||
export const UpdateTeamCityConnectionSchema = z
|
||||
.object({
|
||||
credentials: TeamCityConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.TeamCity).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TeamCity));
|
||||
|
||||
export const TeamCityConnectionListItemSchema = z.object({
|
||||
name: z.literal("TeamCity"),
|
||||
app: z.literal(AppConnection.TeamCity),
|
||||
methods: z.nativeEnum(TeamCityConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listTeamCityProjects } from "./teamcity-connection-fns";
|
||||
import { TTeamCityConnection } from "./teamcity-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TTeamCityConnection>;
|
||||
|
||||
export const teamcityConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.TeamCity, connectionId, actor);
|
||||
|
||||
try {
|
||||
const projects = await listTeamCityProjects(appConnection);
|
||||
return projects;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listProjects
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateTeamCityConnectionSchema,
|
||||
TeamCityConnectionSchema,
|
||||
ValidateTeamCityConnectionCredentialsSchema
|
||||
} from "./teamcity-connection-schemas";
|
||||
|
||||
export type TTeamCityConnection = z.infer<typeof TeamCityConnectionSchema>;
|
||||
|
||||
export type TTeamCityConnectionInput = z.infer<typeof CreateTeamCityConnectionSchema> & {
|
||||
app: AppConnection.TeamCity;
|
||||
};
|
||||
|
||||
export type TValidateTeamCityConnectionCredentialsSchema = typeof ValidateTeamCityConnectionCredentialsSchema;
|
||||
|
||||
export type TTeamCityConnectionConfig = DiscriminativePick<
|
||||
TTeamCityConnectionInput,
|
||||
"method" | "app" | "credentials"
|
||||
> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TTeamCityProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TTeamCityProjectWithBuildTypes = TTeamCityProject & {
|
||||
buildTypes: {
|
||||
buildType: {
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
export type TTeamCityListProjectsResponse = {
|
||||
project: TTeamCityProjectWithBuildTypes[];
|
||||
};
|
||||
Reference in New Issue
Block a user