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
@@ -8,6 +8,7 @@ export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable(TableName.AppConnection, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name", 32).notNullable();
t.string("description");
t.string("app").notNullable();
t.string("method").notNullable();
t.binary("encryptedCredentials").notNullable();
@@ -16,9 +17,9 @@ export async function up(knex: Knex): Promise<void> {
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.AppConnection);
await createOnUpdateTrigger(knex, TableName.AppConnection);
}
}
export async function down(knex: Knex): Promise<void> {
@@ -12,6 +12,7 @@ import { TImmutableDBKeys } from "./models";
export const AppConnectionsSchema = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable().optional(),
app: z.string(),
method: z.string(),
encryptedCredentials: zodBuffer,
@@ -4,9 +4,10 @@ import {
} from "@app/ee/services/project-template/project-template-types";
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
import { AppConnection, TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/lib/app-connections";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { TProjectPermission } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
import { ActorType } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
@@ -1875,8 +1876,10 @@ interface ApplyProjectTemplateEvent {
interface GetAppConnectionsEvent {
type: EventType.GET_APP_CONNECTIONS;
metadata?: {
app: AppConnection;
metadata: {
app?: AppConnection;
count: number;
connectionIds: string[];
};
}
@@ -50,7 +50,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
pkiEst: false,
enforceMfa: false,
projectTemplates: false,
appConnections: false
appConnections: true
});
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
+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,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,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,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 })
})
]);
-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);
@@ -1,19 +1,23 @@
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "src/services/app-connection/aws";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "src/services/app-connection/github";
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { AppConnection } from "@app/lib/app-connections";
import { SanitizedAwsConnectionSchema } from "@app/lib/app-connections/aws";
import { SanitizedGitHubConnectionSchema } from "@app/lib/app-connections/github";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
// can't use discriminated due to multiple schemas for certain apps
export const SanitizedAppConnectionSchema = z.union([
const SanitizedAppConnectionSchema = z.union([
...SanitizedAwsConnectionSchema.options,
...SanitizedGitHubConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AwsConnectionListItemSchema,
GitHubConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
@@ -25,18 +29,11 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
description: "List the available App Connection Options.",
response: {
200: z.object({
appConnectionOptions: z
.object({
name: z.string(),
app: z.nativeEnum(AppConnection),
methods: z.string().array()
})
.passthrough()
.array()
appConnectionOptions: AppConnectionOptionsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: () => {
const appConnectionOptions = server.services.appConnection.listAppConnectionOptions();
return { appConnectionOptions };
@@ -55,7 +52,7 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
200: z.object({ appConnections: SanitizedAppConnectionSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const appConnections = await server.services.appConnection.listAppConnectionsByOrg(req.permission);
@@ -63,7 +60,11 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_APP_CONNECTIONS
type: EventType.GET_APP_CONNECTIONS,
metadata: {
count: appConnections.length,
connectionIds: appConnections.map((connection) => connection.id)
}
}
});
@@ -2,10 +2,12 @@ import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection, TAppConnection, TAppConnectionInput } from "@app/lib/app-connections";
import { APP_CONNECTION_NAME_MAP } from "@app/lib/app-connections/maps";
import { startsWithVowel } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { TAppConnection, TAppConnectionInput } from "@app/services/app-connection/app-connection-types";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerAppConnectionEndpoints = <T extends TAppConnection, I extends TAppConnectionInput>({
@@ -17,8 +19,13 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
}: {
app: AppConnection;
server: FastifyZodProvider;
createSchema: z.ZodType<{ name: string; method: I["method"]; credentials: I["credentials"] }>;
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"] }>;
createSchema: z.ZodType<{
name: string;
method: I["method"];
credentials: I["credentials"];
description?: string | null;
}>;
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
responseSchema: z.ZodTypeAny;
}) => {
const appName = APP_CONNECTION_NAME_MAP[app];
@@ -35,7 +42,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
200: z.object({ appConnections: responseSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const appConnections = (await server.services.appConnection.listAppConnectionsByOrg(req.permission, app)) as T[];
@@ -45,7 +52,9 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
event: {
type: EventType.GET_APP_CONNECTIONS,
metadata: {
app
app,
count: appConnections.length,
connectionIds: appConnections.map((connection) => connection.id)
}
}
});
@@ -69,7 +78,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { connectionId } = req.params;
@@ -112,7 +121,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { connectionName } = req.params;
@@ -144,18 +153,20 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
rateLimit: writeLimit
},
schema: {
description: `Create an ${appName} Connection for the current organization.`,
description: `Create ${
startsWithVowel(appName) ? "an" : "a"
} ${appName} Connection for the current organization.`,
body: createSchema,
response: {
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, method, credentials } = req.body;
const { name, method, credentials, description } = req.body;
const appConnection = (await server.services.appConnection.createAppConnection(
{ name, method, app, credentials },
{ name, method, app, credentials, description },
req.permission
)) as TAppConnection;
@@ -193,13 +204,13 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, credentials } = req.body;
const { name, credentials, description } = req.body;
const { connectionId } = req.params;
const appConnection = (await server.services.appConnection.updateAppConnection(
{ name, credentials, connectionId },
{ name, credentials, connectionId, description },
req.permission
)) as T;
@@ -210,6 +221,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
type: EventType.UPDATE_APP_CONNECTION,
metadata: {
name,
description,
credentialsUpdated: Boolean(credentials),
connectionId
}
@@ -235,7 +247,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { connectionId } = req.params;
@@ -1,9 +1,10 @@
import { AppConnection } from "@app/lib/app-connections";
import {
CreateAwsConnectionSchema,
SanitizedAwsConnectionSchema,
UpdateAwsConnectionSchema
} from "@app/lib/app-connections/aws";
} from "src/services/app-connection/aws";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
@@ -1,9 +1,10 @@
import { AppConnection } from "@app/lib/app-connections";
import {
CreateGitHubConnectionSchema,
GitHubAppConnectionSchema,
SanitizedGitHubConnectionSchema,
UpdateGitHubConnectionSchema
} from "@app/lib/app-connections/github";
} from "src/services/app-connection/github";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
@@ -11,7 +12,7 @@ export const registerGitHubConnectionRouter = async (server: FastifyZodProvider)
registerAppConnectionEndpoints({
app: AppConnection.GitHub,
server,
responseSchema: GitHubAppConnectionSchema,
responseSchema: SanitizedGitHubConnectionSchema,
createSchema: CreateGitHubConnectionSchema,
updateSchema: UpdateGitHubConnectionSchema
});
@@ -1,6 +1,6 @@
import { AppConnection } from "@app/lib/app-connections";
import { registerAwsConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/aws-connection-router";
import { registerGitHubConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/github-connection-router";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const APP_CONNECTION_REGISTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> = {
[AppConnection.AWS]: registerAwsConnectionRouter,
@@ -5,7 +5,7 @@ import { ormify } from "@app/lib/knex";
export type TAppConnectionDALFactory = ReturnType<typeof appConnectionDALFactory>;
export const appConnectionDALFactory = (db: TDbClient) => {
const appConnection = ormify(db, TableName.AppConnection);
const appConnectionOrm = ormify(db, TableName.AppConnection);
return { ...appConnection };
return { ...appConnectionOrm };
};
@@ -1,10 +1,20 @@
import { AppConnection, AppConnectionListItem, TAppConnection, TAppConnectionConfig } from "@app/lib/app-connections";
import { getAwsAppConnectionListItem, validateAwsConnectionCredentials } from "@app/lib/app-connections/aws";
import { getGitHubConnectionListItem, validateGitHubConnectionCredentials } from "@app/lib/app-connections/github";
import {
AwsConnectionMethod,
getAwsAppConnectionListItem,
validateAwsConnectionCredentials
} from "src/services/app-connection/aws";
import {
getGitHubConnectionListItem,
GitHubConnectionMethod,
validateGitHubConnectionCredentials
} from "src/services/app-connection/github";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
import { KmsDataKey } from "@app/services/kms/kms-types";
export const listAppConnectionOptions = (): (AppConnectionListItem & Record<string, unknown>)[] => {
export const listAppConnectionOptions = () => {
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem()].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -65,3 +75,19 @@ export const validateAppConnectionCredentials = async (
throw new Error(`Unhandled App Connection ${app}`);
}
};
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
switch (method) {
case GitHubConnectionMethod.App:
return "GitHub App";
case GitHubConnectionMethod.OAuth:
return "OAuth";
case AwsConnectionMethod.AccessKey:
return "Access Key";
case AwsConnectionMethod.AssumeRole:
return "Assume Role";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
}
};
@@ -0,0 +1,6 @@
import { AppConnection } from "./app-connection-enums";
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.AWS]: "AWS",
[AppConnection.GitHub]: "GitHub"
};
@@ -1,7 +1,35 @@
import { z } from "zod";
import { AppConnectionsSchema } from "@app/db/schemas/app-connections";
import { AppConnections } from "@app/lib/api-docs";
import { slugSchema } from "@app/server/lib/schemas";
import { AppConnection } from "./app-connection-enums";
export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
encryptedCredentials: true,
app: true,
method: true
});
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
z.object({
name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name),
description: z
.string()
.trim()
.max(256, "Description cannot exceed 256 characters")
.nullish()
.describe(AppConnections.CREATE(app).description)
});
export const GenericUpdateAppConnectionFieldsSchema = (app: AppConnection) =>
z.object({
name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(),
description: z
.string()
.trim()
.max(256, "Description cannot exceed 256 characters")
.nullish()
.describe(AppConnections.UPDATE(app).description)
});
@@ -3,21 +3,26 @@ import { ForbiddenError } from "@casl/ability";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
AppConnection,
TAppConnection,
TAppConnectionConfig,
TCreateAppConnectionDTO,
TUpdateAppConnectionDTO
} from "@app/lib/app-connections";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
decryptAppConnectionCredentials,
encryptAppConnectionCredentials,
getAppConnectionMethodName,
listAppConnectionOptions,
validateAppConnectionCredentials
} from "@app/services/app-connection/app-connection-fns";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import {
TAppConnection,
TAppConnectionConfig,
TCreateAppConnectionDTO,
TUpdateAppConnectionDTO,
TValidateAppConnectionCredentials
} from "@app/services/app-connection/app-connection-types";
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "./app-connection-dal";
@@ -31,6 +36,11 @@ export type TAppConnectionServiceFactoryDep = {
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
appConnectionDAL,
permissionService,
@@ -160,40 +170,53 @@ export const appConnectionServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
const isConflictingName = Boolean(
await appConnectionDAL.findOne({
name: params.name,
orgId: actor.orgId
})
);
const appConnection = await appConnectionDAL.transaction(async (tx) => {
const isConflictingName = Boolean(
await appConnectionDAL.findOne(
{
name: params.name,
orgId: actor.orgId
},
tx
)
);
if (isConflictingName)
throw new BadRequestError({
message: `An App Connection with the name "${params.name}" already exists`
if (isConflictingName)
throw new BadRequestError({
message: `An App Connection with the name "${params.name}" already exists`
});
const validatedCredentials = await validateAppConnectionCredentials({
app,
credentials,
method,
orgId: actor.orgId
} as TAppConnectionConfig);
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: validatedCredentials,
orgId: actor.orgId,
kmsService
});
const validatedCredentials = await validateAppConnectionCredentials({
app,
credentials,
method,
orgId: actor.orgId
} as TAppConnectionConfig);
const connection = await appConnectionDAL.create(
{
orgId: actor.orgId,
encryptedCredentials,
method,
app,
...params
},
tx
);
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: validatedCredentials,
orgId: actor.orgId,
kmsService
return {
...connection,
credentials: validatedCredentials
};
});
const appConnection = await appConnectionDAL.create({
orgId: actor.orgId,
encryptedCredentials,
method,
app,
...params
});
return { ...appConnection, credentials: validatedCredentials };
return appConnection;
};
const updateAppConnection = async (
@@ -216,44 +239,69 @@ export const appConnectionServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
if (params.name && appConnection.name !== params.name) {
const isConflictingName = Boolean(
await appConnectionDAL.findOne({
name: params.name,
orgId: appConnection.orgId
})
const updatedAppConnection = await appConnectionDAL.transaction(async (tx) => {
if (params.name && appConnection.name !== params.name) {
const isConflictingName = Boolean(
await appConnectionDAL.findOne(
{
name: params.name,
orgId: appConnection.orgId
},
tx
)
);
if (isConflictingName)
throw new BadRequestError({
message: `An App Connection with the name "${params.name}" already exists`
});
}
let encryptedCredentials: undefined | Buffer;
if (credentials) {
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
if (
!VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({
method,
credentials
}).success
)
throw new BadRequestError({
message: `Invalid credential format for ${
APP_CONNECTION_NAME_MAP[app]
} Connection with method ${getAppConnectionMethodName(method)}`
});
const validatedCredentials = await validateAppConnectionCredentials({
app,
orgId: actor.orgId,
credentials,
method
} as TAppConnectionConfig);
if (!validatedCredentials)
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
encryptedCredentials = await encryptAppConnectionCredentials({
credentials: validatedCredentials,
orgId: actor.orgId,
kmsService
});
}
const updatedConnection = await appConnectionDAL.updateById(
connectionId,
{
orgId: actor.orgId,
encryptedCredentials,
...params
},
tx
);
if (isConflictingName)
throw new BadRequestError({
message: `An App Connection with the name "${params.name}" already exists`
});
}
let encryptedCredentials: undefined | Buffer;
if (credentials) {
const validatedCredentials = await validateAppConnectionCredentials({
app: appConnection.app,
credentials,
method: appConnection.method,
orgId: actor.orgId
} as TAppConnectionConfig);
if (!validatedCredentials)
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
encryptedCredentials = await encryptAppConnectionCredentials({
credentials: validatedCredentials,
orgId: actor.orgId,
kmsService
});
}
const updatedAppConnection = await appConnectionDAL.updateById(connectionId, {
orgId: actor.orgId,
encryptedCredentials,
...params
return updatedConnection;
});
return {
@@ -0,0 +1,31 @@
import {
TAwsConnection,
TAwsConnectionConfig,
TAwsConnectionInput,
TValidateAwsConnectionCredentials
} from "@app/services/app-connection/aws";
import {
TGitHubConnection,
TGitHubConnectionConfig,
TGitHubConnectionInput,
TValidateGitHubConnectionCredentials
} from "@app/services/app-connection/github";
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection);
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput);
export type TCreateAppConnectionDTO = Pick<
TAppConnectionInput,
"credentials" | "method" | "name" | "app" | "description"
>;
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
connectionId: string;
};
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig;
export type TValidateAppConnectionCredentials =
| TValidateAwsConnectionCredentials
| TValidateGitHubConnectionCredentials;
@@ -2,20 +2,20 @@ 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 { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { AwsConnectionMethod } from "./aws-connection-enums";
import { TAwsConnectionConfig } from "./aws-connection-types";
export const getAwsAppConnectionListItem = () => {
const { INF_APP_CONNECTION_AWS_ACCESS_KEY_ID } = getConfig();
return {
name: "AWS",
app: AppConnection.AWS,
methods: Object.values(AwsConnectionMethod),
name: "AWS" as const,
app: AppConnection.AWS as const,
methods: Object.values(AwsConnectionMethod) as [AwsConnectionMethod.AssumeRole, AwsConnectionMethod.AccessKey],
accessKeyId: INF_APP_CONNECTION_AWS_ACCESS_KEY_ID
};
};
@@ -0,0 +1,82 @@
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 { AwsConnectionMethod } from "./aws-connection-enums";
export const AwsConnectionAssumeRoleCredentialsSchema = z.object({
roleArn: z.string().trim().min(1, "Role ARN required")
});
export const AwsConnectionAccessTokenCredentialsSchema = z.object({
accessKeyId: z.string().trim().min(1, "Access Key ID required"),
secretAccessKey: z.string().trim().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 ValidateAwsConnectionCredentialsSchema = 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
)
})
]);
export const CreateAwsConnectionSchema = ValidateAwsConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.AWS)
);
export const UpdateAwsConnectionSchema = z
.object({
credentials: z
.union([AwsConnectionAccessTokenCredentialsSchema, AwsConnectionAssumeRoleCredentialsSchema])
.optional()
.describe(AppConnections.UPDATE(AppConnection.AWS).credentials)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AWS));
export const AwsConnectionListItemSchema = z.object({
name: z.literal("AWS"),
app: z.literal(AppConnection.AWS),
// the below is preferable but currently breaks mintlify
// methods: z.tuple([z.literal(AwsConnectionMethod.AssumeRole), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(AwsConnectionMethod).array(),
accessKeyId: z.string().optional()
});
@@ -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 {
AwsConnectionSchema,
CreateAwsConnectionSchema,
ValidateAwsConnectionCredentialsSchema
} from "./aws-connection-schemas";
export type TAwsConnection = z.infer<typeof AwsConnectionSchema>;
export type TAwsConnectionInput = z.infer<typeof CreateAwsConnectionSchema> & {
app: AppConnection.AWS;
};
export type TValidateAwsConnectionCredentials = typeof ValidateAwsConnectionCredentialsSchema;
export type TAwsConnectionConfig = DiscriminativePick<TAwsConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
@@ -3,10 +3,10 @@ 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 { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
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";
@@ -14,9 +14,9 @@ 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),
name: "GitHub" as const,
app: AppConnection.GitHub as const,
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG
};
@@ -53,7 +53,7 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
if (!clientId || !clientSecret) {
throw new InternalServerError({
message: `GitHub ${APP_CONNECTION_METHOD_NAME_MAP[method]} environment variables have not been configured`
message: `GitHub ${getAppConnectionMethodName(method)} environment variables have not been configured`
});
}
@@ -0,0 +1,93 @@
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 { GitHubConnectionMethod } from "./github-connection-enums";
export const GitHubConnectionOAuthInputCredentialsSchema = z.object({
code: z.string().trim().min(1, "OAuth code required")
});
export const GitHubConnectionAppInputCredentialsSchema = z.object({
code: z.string().trim().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 ValidateGitHubConnectionCredentialsSchema = 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
)
})
]);
export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.GitHub)
);
export const UpdateGitHubConnectionSchema = z
.object({
credentials: z
.union([GitHubConnectionAppInputCredentialsSchema, GitHubConnectionOAuthInputCredentialsSchema])
.optional()
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitHub));
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 })
})
]);
export const GitHubConnectionListItemSchema = z.object({
name: z.literal("GitHub"),
app: z.literal(AppConnection.GitHub),
// the below is preferable but currently breaks mintlify
// methods: z.tuple([z.literal(GitHubConnectionMethod.GitHubApp), z.literal(GitHubConnectionMethod.OAuth)]),
methods: z.nativeEnum(GitHubConnectionMethod).array(),
oauthClientId: z.string().optional(),
appClientSlug: z.string().optional()
});
@@ -3,12 +3,18 @@ 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">;
import {
CreateGitHubConnectionSchema,
GitHubAppConnectionSchema,
ValidateGitHubConnectionCredentialsSchema
} from "./github-connection-schemas";
export type TGitHubConnection = z.infer<typeof GitHubAppConnectionSchema>;
export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema> & {
app: AppConnection.GitHub;
};
export type TValidateGitHubConnectionCredentials = typeof ValidateGitHubConnectionCredentialsSchema;
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;