improvements: address feedback

This commit is contained in:
Scott Wilson
2024-12-18 21:18:38 -08:00
parent 62968c5e43
commit 2d60f389c2
55 changed files with 1040 additions and 690 deletions
+5 -3
View File
@@ -1,5 +1,5 @@
import { AppConnection } from "@app/lib/app-connections";
import { APP_CONNECTION_NAME_MAP } from "@app/lib/app-connections/maps";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
export const GROUPS = {
CREATE: {
@@ -1620,6 +1620,7 @@ export const AppConnections = {
const appName = APP_CONNECTION_NAME_MAP[app];
return {
name: `The name of the ${appName} Connection to create. Must be slug-friendly.`,
description: `An optional description for the ${appName} Connection.`,
credentials: `The credentials used to connect with ${appName}.`,
method: `The method used to authenticate with ${appName}.`
};
@@ -1629,11 +1630,12 @@ export const AppConnections = {
return {
connectionId: `The ID of the ${appName} Connection to be updated.`,
name: `The updated name of the ${appName} Connection. Must be slug-friendly.`,
description: `The updated description of the ${appName} Connection.`,
credentials: `The credentials used to connect with ${appName}.`,
method: `The method used to authenticate with ${appName}.`
};
},
DELETE: (app: AppConnection) => ({
connectionId: `The ID of the ${app} connection to be deleted.`
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} connection to be deleted.`
})
};
@@ -1,4 +0,0 @@
export enum AppConnection {
GitHub = "github",
AWS = "aws"
}
@@ -1,26 +0,0 @@
import { TAwsConnection } from "@app/lib/app-connections/aws/aws-connection-types";
import { TGitHubConnection, TGitHubConnectionInput } from "@app/lib/app-connections/github";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "./app-connection-enums";
export type AppConnectionListItem = {
app: AppConnection;
name: string;
methods: string[];
};
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection);
export type TAppConnectionInput = { id: string } & (TAwsConnection | TGitHubConnectionInput);
export type TCreateAppConnectionDTO = Pick<TAppConnectionInput, "credentials" | "method" | "name" | "app">;
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
connectionId: string;
};
export type TAppConnectionConfig = { orgId: string } & DiscriminativePick<
TAppConnectionInput,
"app" | "method" | "credentials"
>;
@@ -1,4 +0,0 @@
export enum AwsConnectionMethod {
AssumeRole = "assume-role",
AccessKey = "access-key"
}
@@ -1,105 +0,0 @@
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import AWS from "aws-sdk";
import { randomUUID } from "crypto";
import { AppConnection } from "@app/lib/app-connections/app-connection-enums";
import { TAwsConnectionConfig } from "@app/lib/app-connections/aws/aws-connection-types";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { AwsConnectionMethod } from "./aws-connection-enums";
export const getAwsAppConnectionListItem = () => {
const { INF_APP_CONNECTION_AWS_ACCESS_KEY_ID } = getConfig();
return {
name: "AWS",
app: AppConnection.AWS,
methods: Object.values(AwsConnectionMethod),
accessKeyId: INF_APP_CONNECTION_AWS_ACCESS_KEY_ID
};
};
export const getAwsConnectionConfig = async (appConnection: TAwsConnectionConfig, region = "us-east-1") => {
const appCfg = getConfig();
let accessKeyId: string;
let secretAccessKey: string;
let sessionToken: undefined | string;
const { method, credentials, orgId } = appConnection;
switch (method) {
case AwsConnectionMethod.AssumeRole: {
const client = new STSClient({
region,
credentials:
appCfg.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID && appCfg.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY
? {
accessKeyId: appCfg.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID,
secretAccessKey: appCfg.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY
}
: undefined // if hosting on AWS
});
const command = new AssumeRoleCommand({
RoleArn: credentials.roleArn,
RoleSessionName: `infisical-app-connection-${randomUUID()}`,
DurationSeconds: 900, // 15 mins
ExternalId: orgId
});
const assumeRes = await client.send(command);
if (!assumeRes.Credentials?.AccessKeyId || !assumeRes.Credentials?.SecretAccessKey) {
throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" });
}
accessKeyId = assumeRes.Credentials.AccessKeyId;
secretAccessKey = assumeRes.Credentials.SecretAccessKey;
sessionToken = assumeRes.Credentials?.SessionToken;
break;
}
case AwsConnectionMethod.AccessKey: {
accessKeyId = credentials.accessKeyId;
secretAccessKey = credentials.secretAccessKey;
break;
}
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new InternalServerError({ message: `Unsupported AWS connection method: ${method}` });
}
return new AWS.Config({
region,
credentials: {
accessKeyId,
secretAccessKey,
sessionToken
}
});
};
export const validateAwsConnectionCredentials = async (appConnection: TAwsConnectionConfig) => {
const awsConfig = await getAwsConnectionConfig(appConnection);
const sts = new AWS.STS(awsConfig);
let resp: Awaited<ReturnType<ReturnType<typeof sts.getCallerIdentity>["promise"]>>;
try {
resp = await sts.getCallerIdentity().promise();
} catch (e: unknown) {
throw new BadRequestError({
message: `Unable to validate connection - verify credentials`
});
}
if (resp.$response.httpResponse.statusCode !== 200)
throw new InternalServerError({
message: `Unable to validate credentials: ${
resp.$response.error?.message ??
`AWS responded with a status code of ${resp.$response.httpResponse.statusCode}. Verify credentials and try again.`
}`
});
return appConnection.credentials;
};
@@ -1,69 +0,0 @@
import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { slugSchema } from "@app/server/lib/schemas";
import { BaseAppConnectionSchema } from "@app/services/app-connection/app-connection-schemas";
import { AppConnection } from "../app-connection-enums";
import { AwsConnectionMethod } from "./aws-connection-enums";
export const AwsConnectionAssumeRoleCredentialsSchema = z.object({
roleArn: z.string().min(1, "Role ARN required")
});
export const AwsConnectionAccessTokenCredentialsSchema = z.object({
accessKeyId: z.string().min(1, "Access Key ID required"),
secretAccessKey: z.string().min(1, "Secret Access Key required")
});
const BaseAwsConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.AWS) });
export const AwsConnectionSchema = z.intersection(
BaseAwsConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(AwsConnectionMethod.AssumeRole),
credentials: AwsConnectionAssumeRoleCredentialsSchema
}),
z.object({
method: z.literal(AwsConnectionMethod.AccessKey),
credentials: AwsConnectionAccessTokenCredentialsSchema
})
])
);
export const SanitizedAwsConnectionSchema = z.discriminatedUnion("method", [
BaseAwsConnectionSchema.extend({
method: z.literal(AwsConnectionMethod.AssumeRole),
credentials: AwsConnectionAssumeRoleCredentialsSchema.omit({ roleArn: true })
}),
BaseAwsConnectionSchema.extend({
method: z.literal(AwsConnectionMethod.AccessKey),
credentials: AwsConnectionAccessTokenCredentialsSchema.omit({ secretAccessKey: true })
})
]);
export const CreateAwsConnectionSchema = z
.discriminatedUnion("method", [
z.object({
method: z.literal(AwsConnectionMethod.AssumeRole).describe(AppConnections.CREATE(AppConnection.AWS).method),
credentials: AwsConnectionAssumeRoleCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.AWS).credentials
)
}),
z.object({
method: z.literal(AwsConnectionMethod.AccessKey).describe(AppConnections.CREATE(AppConnection.AWS).method),
credentials: AwsConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.AWS).credentials
)
})
])
.and(z.object({ name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(AppConnection.AWS).name) }));
export const UpdateAwsConnectionSchema = z.object({
name: slugSchema({ field: "name" }).optional().describe(AppConnections.UPDATE(AppConnection.AWS).name),
credentials: z
.union([AwsConnectionAccessTokenCredentialsSchema, AwsConnectionAssumeRoleCredentialsSchema])
.optional()
.describe(AppConnections.UPDATE(AppConnection.AWS).credentials)
});
@@ -1,9 +0,0 @@
import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AwsConnectionSchema } from "./aws-connection-schemas";
export type TAwsConnection = z.infer<typeof AwsConnectionSchema>;
export type TAwsConnectionConfig = DiscriminativePick<TAwsConnection, "orgId" | "method" | "app" | "credentials">;
@@ -1,4 +0,0 @@
export * from "./aws-connection-enums";
export * from "./aws-connection-fns";
export * from "./aws-connection-schemas";
export * from "./aws-connection-types";
@@ -1,4 +0,0 @@
export enum GitHubConnectionMethod {
OAuth = "oauth",
App = "github-app"
}
@@ -1,129 +0,0 @@
import { AxiosResponse } from "axios";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums";
import { APP_CONNECTION_METHOD_NAME_MAP } from "../maps";
import { GitHubConnectionMethod } from "./github-connection-enums";
import { TGitHubConnectionConfig } from "./github-connection-types";
export const getGitHubConnectionListItem = () => {
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
return {
name: "GitHub",
app: AppConnection.GitHub,
methods: Object.values(GitHubConnectionMethod),
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG
};
};
type TokenRespData = {
access_token: string;
scope: string;
token_type: string;
};
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
const { credentials, method } = config;
const {
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET,
SITE_URL
} = getConfig();
const { clientId, clientSecret } =
method === GitHubConnectionMethod.App
? {
clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
}
: // oauth
{
clientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
clientSecret: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET
};
if (!clientId || !clientSecret) {
throw new InternalServerError({
message: `GitHub ${APP_CONNECTION_METHOD_NAME_MAP[method]} environment variables have not been configured`
});
}
let tokenResp: AxiosResponse<TokenRespData>;
try {
tokenResp = await request.get<TokenRespData>("https://github.com/login/oauth/access_token", {
params: {
client_id: clientId,
client_secret: clientSecret,
code: credentials.code,
redirect_uri: `${SITE_URL}/app-connections/github/oauth/callback`
},
headers: {
Accept: "application/json",
"Accept-Encoding": "application/json"
}
});
} catch (e: unknown) {
throw new BadRequestError({
message: `Unable to validate connection - verify credentials`
});
}
if (tokenResp.status !== 200) {
throw new BadRequestError({
message: `Unable to validate credentials: GitHub responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.`
});
}
if (method === GitHubConnectionMethod.App) {
const installationsResp = await request.get<{
installations: {
id: number;
account: {
login: string;
};
}[];
}>(IntegrationUrls.GITHUB_USER_INSTALLATIONS, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${tokenResp.data.access_token}`,
"Accept-Encoding": "application/json"
}
});
const matchingInstallation = installationsResp.data.installations.find(
(installation) => installation.id === +credentials.installationId
);
if (!matchingInstallation) {
throw new ForbiddenRequestError({
message: "User does not have access to the provided installation"
});
}
}
switch (method) {
case GitHubConnectionMethod.App:
return {
// access token not needed for GitHub App
installationId: credentials.installationId
};
case GitHubConnectionMethod.OAuth:
return {
accessToken: tokenResp.data.access_token
};
default:
throw new InternalServerError({
message: `Unhandled GitHub connection method: ${method as GitHubConnectionMethod}`
});
}
};
@@ -1,77 +0,0 @@
import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/lib/app-connections";
import { slugSchema } from "@app/server/lib/schemas";
import { BaseAppConnectionSchema } from "@app/services/app-connection/app-connection-schemas";
import { GitHubConnectionMethod } from "./github-connection-enums";
export const GitHubConnectionOAuthInputCredentialsSchema = z.object({
code: z.string().min(1, "OAuth code required")
});
export const GitHubConnectionAppInputCredentialsSchema = z.object({
code: z.string().min(1, "GitHub App code required"),
installationId: z.string().min(1, "GitHub App Installation ID required")
});
export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({
accessToken: z.string()
});
export const GitHubConnectionAppOutputCredentialsSchema = z.object({
installationId: z.string()
});
export const CreateGitHubConnectionSchema = z
.discriminatedUnion("method", [
z.object({
method: z.literal(GitHubConnectionMethod.App).describe(AppConnections.CREATE(AppConnection.GitHub).method),
credentials: GitHubConnectionAppInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.GitHub).credentials
)
}),
z.object({
method: z.literal(GitHubConnectionMethod.OAuth).describe(AppConnections.CREATE(AppConnection.GitHub).method),
credentials: GitHubConnectionOAuthInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.GitHub).credentials
)
})
])
.and(z.object({ name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(AppConnection.GitHub).name) }));
export const UpdateGitHubConnectionSchema = z.object({
name: slugSchema({ field: "name" }).optional().describe(AppConnections.UPDATE(AppConnection.GitHub).name),
credentials: z
.union([GitHubConnectionAppInputCredentialsSchema, GitHubConnectionOAuthInputCredentialsSchema])
.optional()
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
});
const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) });
export const GitHubAppConnectionSchema = z.intersection(
BaseGitHubConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(GitHubConnectionMethod.App),
credentials: GitHubConnectionAppOutputCredentialsSchema
}),
z.object({
method: z.literal(GitHubConnectionMethod.OAuth),
credentials: GitHubConnectionOAuthOutputCredentialsSchema
})
])
);
export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
BaseGitHubConnectionSchema.extend({
method: z.literal(GitHubConnectionMethod.App),
credentials: GitHubConnectionAppOutputCredentialsSchema.omit({ installationId: true })
}),
BaseGitHubConnectionSchema.extend({
method: z.literal(GitHubConnectionMethod.OAuth),
credentials: GitHubConnectionOAuthOutputCredentialsSchema.omit({ accessToken: true })
})
]);
@@ -1,14 +0,0 @@
import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { CreateGitHubConnectionSchema, GitHubAppConnectionSchema } from "./github-connection-schemas";
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;
export type TGitHubConnection = z.infer<typeof GitHubAppConnectionSchema>;
export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema> & {
app: AppConnection.GitHub;
};
@@ -1,4 +0,0 @@
export * from "./github-connection-enums";
export * from "./github-connection-fns";
export * from "./github-connection-schemas";
export * from "./github-connection-types";
-2
View File
@@ -1,2 +0,0 @@
export * from "./app-connection-enums";
export * from "./app-connection-types";
-17
View File
@@ -1,17 +0,0 @@
import { TAppConnection } from "@app/lib/app-connections/app-connection-types";
import { AppConnection } from "./app-connection-enums";
import { AwsConnectionMethod } from "./aws/aws-connection-enums";
import { GitHubConnectionMethod } from "./github/github-connection-enums";
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.AWS]: "AWS",
[AppConnection.GitHub]: "GitHub"
};
export const APP_CONNECTION_METHOD_NAME_MAP: Record<TAppConnection["method"], string> = {
[AwsConnectionMethod.AssumeRole]: "Assume Role",
[AwsConnectionMethod.AccessKey]: "Access Key",
[GitHubConnectionMethod.App]: "Github App",
[GitHubConnectionMethod.OAuth]: "OAuth"
};
+2
View File
@@ -14,3 +14,5 @@ export const prefixWithSlash = (str: string) => {
if (str.startsWith("/")) return str;
return `/${str}`;
};
export const startsWithVowel = (str: string) => /^[aeiou]/i.test(str);