mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 14:28:20 +00:00
feat: migrate github app connection to env override (#4004)
* feat: migrate github app connection to env override * fix: remove usage of github app integration * chore: lint fix * fix: migration cleanup * fix: refactor integrations tab * fix: content * fix: remove integrations tab --------- Co-authored-by: sidwebworks <[email protected]>
This commit is contained in:
@@ -0,0 +1,66 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { inMemoryKeyStore } from "@app/keystore/memory";
|
||||||
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { getMigrationEnvConfig } from "./utils/env-config";
|
||||||
|
import { getMigrationEncryptionServices } from "./utils/services";
|
||||||
|
|
||||||
|
export async function up(knex: Knex) {
|
||||||
|
const existingSuperAdminsWithGithubConnection = await knex(TableName.SuperAdmin)
|
||||||
|
.select(selectAllTableCols(TableName.SuperAdmin))
|
||||||
|
.whereNotNull(`${TableName.SuperAdmin}.encryptedGitHubAppConnectionClientId`);
|
||||||
|
|
||||||
|
const envConfig = getMigrationEnvConfig();
|
||||||
|
const keyStore = inMemoryKeyStore();
|
||||||
|
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||||
|
|
||||||
|
const decryptor = kmsService.decryptWithRootKey();
|
||||||
|
const encryptor = kmsService.encryptWithRootKey();
|
||||||
|
|
||||||
|
const tasks = existingSuperAdminsWithGithubConnection.map(async (admin) => {
|
||||||
|
const overrides = (
|
||||||
|
admin.encryptedEnvOverrides ? JSON.parse(decryptor(Buffer.from(admin.encryptedEnvOverrides)).toString()) : {}
|
||||||
|
) as Record<string, string>;
|
||||||
|
|
||||||
|
if (admin.encryptedGitHubAppConnectionClientId) {
|
||||||
|
overrides.INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID = decryptor(
|
||||||
|
admin.encryptedGitHubAppConnectionClientId
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (admin.encryptedGitHubAppConnectionClientSecret) {
|
||||||
|
overrides.INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET = decryptor(
|
||||||
|
admin.encryptedGitHubAppConnectionClientSecret
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (admin.encryptedGitHubAppConnectionPrivateKey) {
|
||||||
|
overrides.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY = decryptor(
|
||||||
|
admin.encryptedGitHubAppConnectionPrivateKey
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (admin.encryptedGitHubAppConnectionSlug) {
|
||||||
|
overrides.INF_APP_CONNECTION_GITHUB_APP_SLUG = decryptor(admin.encryptedGitHubAppConnectionSlug).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (admin.encryptedGitHubAppConnectionId) {
|
||||||
|
overrides.INF_APP_CONNECTION_GITHUB_APP_ID = decryptor(admin.encryptedGitHubAppConnectionId).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedEnvOverrides = encryptor(Buffer.from(JSON.stringify(overrides)));
|
||||||
|
|
||||||
|
await knex(TableName.SuperAdmin).where({ id: admin.id }).update({
|
||||||
|
encryptedEnvOverrides
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down() {
|
||||||
|
// No down migration needed as this migration is only for data transformation
|
||||||
|
// and does not change the schema.
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import { request } from "@app/lib/config/request";
|
|||||||
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
|
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
|
||||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
import { getInstanceIntegrationsConfig } from "@app/services/super-admin/super-admin-service";
|
|
||||||
|
|
||||||
import { AppConnection } from "../app-connection-enums";
|
import { AppConnection } from "../app-connection-enums";
|
||||||
import { GitHubConnectionMethod } from "./github-connection-enums";
|
import { GitHubConnectionMethod } from "./github-connection-enums";
|
||||||
@@ -15,14 +14,13 @@ import { TGitHubConnection, TGitHubConnectionConfig } from "./github-connection-
|
|||||||
|
|
||||||
export const getGitHubConnectionListItem = () => {
|
export const getGitHubConnectionListItem = () => {
|
||||||
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
|
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
|
||||||
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "GitHub" as const,
|
name: "GitHub" as const,
|
||||||
app: AppConnection.GitHub as const,
|
app: AppConnection.GitHub as const,
|
||||||
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
|
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
|
||||||
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||||
appClientSlug: gitHubAppConnection.appSlug || INF_APP_CONNECTION_GITHUB_APP_SLUG
|
appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,10 +30,9 @@ export const getGitHubClient = (appConnection: TGitHubConnection) => {
|
|||||||
const { method, credentials } = appConnection;
|
const { method, credentials } = appConnection;
|
||||||
|
|
||||||
let client: Octokit;
|
let client: Octokit;
|
||||||
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
|
|
||||||
|
|
||||||
const appId = gitHubAppConnection.appId || appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
|
const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
|
||||||
const appPrivateKey = gitHubAppConnection.privateKey || appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
|
const appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case GitHubConnectionMethod.App:
|
case GitHubConnectionMethod.App:
|
||||||
@@ -157,8 +154,6 @@ type TokenRespData = {
|
|||||||
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
|
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
|
||||||
const { credentials, method } = config;
|
const { credentials, method } = config;
|
||||||
|
|
||||||
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
|
||||||
@@ -170,8 +165,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
|
|||||||
const { clientId, clientSecret } =
|
const { clientId, clientSecret } =
|
||||||
method === GitHubConnectionMethod.App
|
method === GitHubConnectionMethod.App
|
||||||
? {
|
? {
|
||||||
clientId: gitHubAppConnection.clientId || INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
|
clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
|
||||||
clientSecret: gitHubAppConnection.clientSecret || INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
|
clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
|
||||||
}
|
}
|
||||||
: // oauth
|
: // oauth
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,222 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { FaGithub } from "react-icons/fa";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
Input,
|
|
||||||
TextArea
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
import { useUpdateServerConfig } from "@app/hooks/api";
|
|
||||||
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
|
||||||
|
|
||||||
const gitHubAppFormSchema = z.object({
|
|
||||||
clientId: z.string(),
|
|
||||||
clientSecret: z.string(),
|
|
||||||
appSlug: z.string(),
|
|
||||||
appId: z.string(),
|
|
||||||
privateKey: z.string()
|
|
||||||
});
|
|
||||||
|
|
||||||
type TGitHubAppConnectionForm = z.infer<typeof gitHubAppFormSchema>;
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
adminIntegrationsConfig?: AdminIntegrationsConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GitHubAppConnectionForm = ({ adminIntegrationsConfig }: Props) => {
|
|
||||||
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
|
|
||||||
const [isGitHubAppClientSecretFocused, setIsGitHubAppClientSecretFocused] = useToggle();
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
setValue,
|
|
||||||
formState: { isSubmitting, isDirty }
|
|
||||||
} = useForm<TGitHubAppConnectionForm>({
|
|
||||||
resolver: zodResolver(gitHubAppFormSchema)
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (data: TGitHubAppConnectionForm) => {
|
|
||||||
await updateAdminServerConfig({
|
|
||||||
gitHubAppConnectionClientId: data.clientId,
|
|
||||||
gitHubAppConnectionClientSecret: data.clientSecret,
|
|
||||||
gitHubAppConnectionSlug: data.appSlug,
|
|
||||||
gitHubAppConnectionId: data.appId,
|
|
||||||
gitHubAppConnectionPrivateKey: data.privateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Updated GitHub app connection configuration. It can take up to 5 minutes to take effect.",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (adminIntegrationsConfig) {
|
|
||||||
setValue("clientId", adminIntegrationsConfig.gitHubAppConnection.clientId);
|
|
||||||
setValue("clientSecret", adminIntegrationsConfig.gitHubAppConnection.clientSecret);
|
|
||||||
setValue("appSlug", adminIntegrationsConfig.gitHubAppConnection.appSlug);
|
|
||||||
setValue("appId", adminIntegrationsConfig.gitHubAppConnection.appId);
|
|
||||||
setValue("privateKey", adminIntegrationsConfig.gitHubAppConnection.privateKey);
|
|
||||||
}
|
|
||||||
}, [adminIntegrationsConfig]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
|
||||||
<AccordionItem value="github-app-integration" className="data-[state=open]:border-none">
|
|
||||||
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
|
|
||||||
<div className="text-md group order-1 ml-3 flex items-center gap-2">
|
|
||||||
<FaGithub className="text-lg group-hover:text-primary-400" />
|
|
||||||
<div className="text-[15px] font-semibold">GitHub App</div>
|
|
||||||
</div>
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent childrenClassName="px-0 py-0">
|
|
||||||
<div className="flex w-full flex-col justify-start rounded-md rounded-t-none border border-t-0 border-mineshaft-500 bg-mineshaft-700 px-4 py-4">
|
|
||||||
<div className="mb-2 max-w-lg text-sm text-mineshaft-300">
|
|
||||||
Step 1: Create and configure GitHub App. Please refer to the documentation below for
|
|
||||||
more information.
|
|
||||||
</div>
|
|
||||||
<div className="mb-6">
|
|
||||||
<a
|
|
||||||
href="https://infisical.com/docs/integrations/app-connections/github#self-hosted-instance"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Button colorSchema="secondary">Documentation</Button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
|
||||||
Step 2: Configure your instance-wide settings to enable GitHub App connections. Copy
|
|
||||||
the credentials from your GitHub App's settings page.
|
|
||||||
</div>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="clientId"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Client ID"
|
|
||||||
className="w-96"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value || ""}
|
|
||||||
type="text"
|
|
||||||
onChange={(e) => field.onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="clientSecret"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Client Secret"
|
|
||||||
tooltipText="You can find your Client Secret in the GitHub App's settings under 'Client secrets'."
|
|
||||||
className="w-96"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value || ""}
|
|
||||||
type={isGitHubAppClientSecretFocused ? "text" : "password"}
|
|
||||||
onFocus={() => setIsGitHubAppClientSecretFocused.on()}
|
|
||||||
onBlur={() => setIsGitHubAppClientSecretFocused.off()}
|
|
||||||
onChange={(e) => field.onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="appSlug"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="App Slug"
|
|
||||||
tooltipText="The GitHub App slug from the app's URL (e.g., 'my-app' from github.com/apps/my-app)."
|
|
||||||
className="w-96"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value || ""}
|
|
||||||
type="text"
|
|
||||||
onChange={(e) => field.onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="appId"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="App ID"
|
|
||||||
tooltipText="The numeric App ID found in your GitHub App's settings."
|
|
||||||
className="w-96"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value || ""}
|
|
||||||
type="text"
|
|
||||||
onChange={(e) => field.onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="privateKey"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Private Key"
|
|
||||||
tooltipText="The private key generated for your GitHub App (PEM format)."
|
|
||||||
className="w-96"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<TextArea
|
|
||||||
{...field}
|
|
||||||
value={field.value || ""}
|
|
||||||
className="min-h-32"
|
|
||||||
onChange={(e) => field.onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
className="mt-2"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting || !isDirty}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -5,23 +5,17 @@ import { ROUTE_PATHS } from "@app/const/routes";
|
|||||||
import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
|
import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
|
||||||
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
||||||
|
|
||||||
import { GitHubAppConnectionForm } from "./GitHubAppConnectionForm";
|
|
||||||
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||||
|
|
||||||
enum IntegrationTabSections {
|
enum IntegrationTabSections {
|
||||||
Workflow = "workflow",
|
Workflow = "workflow"
|
||||||
AppConnections = "app-connections"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkflowTabProps {
|
interface WorkflowTabProps {
|
||||||
adminIntegrationsConfig: AdminIntegrationsConfig;
|
adminIntegrationsConfig: AdminIntegrationsConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppConnectionsTabProps {
|
|
||||||
adminIntegrationsConfig: AdminIntegrationsConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => (
|
const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||||
@@ -29,12 +23,6 @@ const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const AppConnectionsTab = ({ adminIntegrationsConfig }: AppConnectionsTabProps) => (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<GitHubAppConnectionForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const IntegrationsPageForm = () => {
|
export const IntegrationsPageForm = () => {
|
||||||
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
|
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
|
||||||
|
|
||||||
@@ -59,11 +47,6 @@ export const IntegrationsPageForm = () => {
|
|||||||
key: IntegrationTabSections.Workflow,
|
key: IntegrationTabSections.Workflow,
|
||||||
label: "Workflows",
|
label: "Workflows",
|
||||||
component: WorkflowTab
|
component: WorkflowTab
|
||||||
},
|
|
||||||
{
|
|
||||||
key: IntegrationTabSections.AppConnections,
|
|
||||||
label: "App Connections",
|
|
||||||
component: AppConnectionsTab
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user