diff --git a/.env.example b/.env.example index 059ec124f..5220d5a03 100644 --- a/.env.example +++ b/.env.example @@ -122,7 +122,7 @@ INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET= #gcp app connection INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL= -# azure app connection +# azure app connections INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID= INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET= @@ -135,6 +135,10 @@ INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET= INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID= INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET= +# heroku app connection +INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID= +INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET= + # datadog SHOULD_USE_DATADOG_TRACER= DATADOG_PROFILING_ENABLED= diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index da0dd61a7..613b12842 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -323,6 +323,10 @@ const envSchema = z INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET: zpStr(z.string().optional()), + // Heroku App Connection + INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID: zpStr(z.string().optional()), + INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET: zpStr(z.string().optional()), + // datadog SHOULD_USE_DATADOG_TRACER: zodStrBool.default("false"), DATADOG_PROFILING_ENABLED: zodStrBool.default("false"), @@ -736,6 +740,19 @@ export const overwriteSchema: { description: "The Client Secret of your GCP OAuth2 application." } ] + }, + heroku: { + name: "Heroku", + fields: [ + { + key: "INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID", + description: "The Client ID of your Heroku application." + }, + { + key: "INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET", + description: "The Client Secret of your Heroku application." + } + ] } }; diff --git a/backend/src/services/app-connection/heroku/heroku-connection-fns.ts b/backend/src/services/app-connection/heroku/heroku-connection-fns.ts index adbc5cd2b..211ac6d4c 100644 --- a/backend/src/services/app-connection/heroku/heroku-connection-fns.ts +++ b/backend/src/services/app-connection/heroku/heroku-connection-fns.ts @@ -22,13 +22,13 @@ interface HerokuOAuthTokenResponse { } export const getHerokuConnectionListItem = () => { - const { CLIENT_ID_HEROKU } = getConfig(); + const { INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID } = getConfig(); return { name: "Heroku" as const, app: AppConnection.Heroku as const, methods: Object.values(HerokuConnectionMethod) as [HerokuConnectionMethod.AuthToken, HerokuConnectionMethod.OAuth], - oauthClientId: CLIENT_ID_HEROKU + oauthClientId: INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID }; }; diff --git a/docs/images/integrations/heroku/integrations-heroku-config-credentials.png b/docs/images/integrations/heroku/integrations-heroku-config-credentials.png index e84aaca01..82588e7f1 100644 Binary files a/docs/images/integrations/heroku/integrations-heroku-config-credentials.png and b/docs/images/integrations/heroku/integrations-heroku-config-credentials.png differ diff --git a/docs/images/integrations/heroku/integrations-heroku-config-new-app-form.png b/docs/images/integrations/heroku/integrations-heroku-config-new-app-form.png index dee5a5336..bbc62f70a 100644 Binary files a/docs/images/integrations/heroku/integrations-heroku-config-new-app-form.png and b/docs/images/integrations/heroku/integrations-heroku-config-new-app-form.png differ diff --git a/docs/integrations/app-connections/heroku.mdx b/docs/integrations/app-connections/heroku.mdx index 9c3397e02..d0824741c 100644 --- a/docs/integrations/app-connections/heroku.mdx +++ b/docs/integrations/app-connections/heroku.mdx @@ -24,7 +24,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To ![Heroku config applications](/images/integrations/heroku/integrations-heroku-config-applications.png) ![Heroku config new app](/images/integrations/heroku/integrations-heroku-config-new-app.png) - Create the API client. As part of the form, set the **OAuth callback URL** to `https://your-domain.com/integrations/heroku/oauth2/callback`. + Create the API client. As part of the form, set the **OAuth callback URL** to `https://your-domain.com/organization/app-connections/heroku/oauth/callback`. The domain you defined in the OAuth callback URL should be equivalent to the `SITE_URL` configured in your Infisical instance. @@ -39,8 +39,8 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To Back in your Infisical instance, add two new environment variables for the credentials of your Heroku API client: - - `CLIENT_ID_HEROKU`: The **Client ID** of your Heroku API client. - - `CLIENT_SECRET_HEROKU`: The **Client Secret** of your Heroku API client. + - `INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID`: The **Client ID** of your Heroku API client. + - `INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET`: The **Client Secret** of your Heroku API client. Once added, restart your Infisical instance and use the Heroku Connection. diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 8d351d7f2..fc6f4d794 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -672,6 +672,16 @@ You can configure third-party app connections for re-use across Infisical Projec + + + The Application ID of your Heroku OAuth application. + + + + The Secret of your GitLab Heroku OAuth application. + + + ## Native Secret Integrations To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box. diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index f82e4107d..e7cca2b90 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -142,7 +142,7 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => { case AppConnection.OnePass: return ; case AppConnection.Heroku: - return ; + return ; case AppConnection.Render: return ; case AppConnection.Flyio: @@ -285,7 +285,13 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { case AppConnection.OnePass: return ; case AppConnection.Heroku: - return ; + return ( + + ); case AppConnection.Render: return ; case AppConnection.Flyio: diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx index c69f662c6..5faa7d5b0 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx @@ -39,7 +39,7 @@ import { } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; -import { GithubFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types"; +import { GitHubFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types"; import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields @@ -118,7 +118,7 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => { connectionId: appConnection?.id, projectId, returnUrl - } as GithubFormData) + } as GitHubFormData) ); const githubHost = diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubRadarConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubRadarConnectionForm.tsx index 446eeb4cc..5cb559410 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubRadarConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubRadarConnectionForm.tsx @@ -19,7 +19,7 @@ import { } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; -import { GithubRadarFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types"; +import { GitHubRadarFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types"; import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields @@ -76,7 +76,7 @@ export const GitHubRadarConnectionForm = ({ appConnection, projectId }: Props) = connectionId: appConnection?.id, projectId, returnUrl - } as GithubRadarFormData) + } as GitHubRadarFormData) ); switch (formData.method) { diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HerokuAppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HerokuAppConnectionForm.tsx index 085ef49c5..ebc81e138 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HerokuAppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HerokuAppConnectionForm.tsx @@ -15,7 +15,11 @@ import { Select, SelectItem } from "@app/components/v2"; -import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { + APP_CONNECTION_MAP, + getAppConnectionMethodDetails, + useGetAppConnectionOauthReturnUrl +} from "@app/helpers/appConnections"; import { isInfisicalCloud } from "@app/helpers/platform"; import { useGetAppConnectionOption } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; @@ -32,6 +36,7 @@ import { type Props = { appConnection?: THerokuConnection; onSubmit: (formData: FormData) => Promise; + projectId: string | undefined | null; }; const formSchema = z.discriminatedUnion("method", [ @@ -53,10 +58,12 @@ const formSchema = z.discriminatedUnion("method", [ type FormData = z.infer; -export const HerokuConnectionForm = ({ appConnection, onSubmit: formSubmit }: Props) => { +export const HerokuConnectionForm = ({ appConnection, onSubmit: formSubmit, projectId }: Props) => { const isUpdate = Boolean(appConnection); const [isRedirecting, setIsRedirecting] = useState(false); + const returnUrl = useGetAppConnectionOauthReturnUrl(); + const { option: { oauthClientId }, isLoading @@ -110,7 +117,8 @@ export const HerokuConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr JSON.stringify({ ...formData, connectionId: appConnection?.id, - isUpdate + returnUrl, + projectId }) ); diff --git a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx index f5f6d2c88..df3d1327a 100644 --- a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx +++ b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx @@ -12,6 +12,7 @@ import { AzureKeyVaultConnectionMethod, GitHubConnectionMethod, GitLabConnectionMethod, + HerokuConnectionMethod, TAppConnection, useCreateAppConnection, useUpdateAppConnection @@ -28,7 +29,8 @@ const formDataStorageFieldMap: Partial> = { [AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData", [AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData", [AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData", - [AppConnection.AzureDevOps]: "azureDevOpsConnectionFormData" + [AppConnection.AzureDevOps]: "azureDevOpsConnectionFormData", + [AppConnection.Heroku]: "herokuConnectionFormData" }; export const OAuthCallbackPage = () => { @@ -83,7 +85,7 @@ export const OAuthCallbackPage = () => { } }; - const handleGitlab = useCallback(async () => { + const handleGitLab = useCallback(async () => { const formData = getFormData(AppConnection.GitLab); if (formData === null) return null; @@ -375,7 +377,7 @@ export const OAuthCallbackPage = () => { }; }, []); - const handleGithub = useCallback(async () => { + const handleGitHub = useCallback(async () => { const formData = getFormData(AppConnection.GitHub); if (formData === null) return null; @@ -463,7 +465,7 @@ export const OAuthCallbackPage = () => { }; }, []); - const handleGithubRadar = useCallback(async () => { + const handleGitHubRadar = useCallback(async () => { const formData = getFormData(AppConnection.GitHubRadar); if (formData === null) return null; @@ -520,6 +522,61 @@ export const OAuthCallbackPage = () => { }; }, []); + const handleHeroku = useCallback(async () => { + const formData = getFormData(AppConnection.Heroku); + if (formData === null) return null; + + clearState(AppConnection.Heroku); + + const { connectionId, name, description, returnUrl, projectId } = formData; + + let connection: TAppConnection; + + try { + if (connectionId) { + connection = await updateAppConnection.mutateAsync({ + app: AppConnection.Heroku, + connectionId, + credentials: { + code: code as string + } + }); + } else { + connection = await createAppConnection.mutateAsync({ + app: AppConnection.Heroku, + name, + description, + method: HerokuConnectionMethod.OAuth, + projectId, + credentials: { + code: code as string + } + }); + } + } catch (e: any) { + createNotification({ + title: `Failed to ${connectionId ? "update" : "add"} Heroku Connection`, + text: e.message, + type: "error" + }); + navigate({ + to: returnUrl, + params: { + projectId + } + }); + return null; + } + + return { + connectionId, + returnUrl, + appConnectionName: formData.app, + projectId, + connection + }; + }, []); + // Ensure that the localstorage is ready for use, to avoid the form data being malformed useEffect(() => { if (!isReady) { @@ -540,11 +597,11 @@ export const OAuthCallbackPage = () => { } | null = null; if (appConnection === AppConnection.GitHub) { - data = await handleGithub(); + data = await handleGitHub(); } else if (appConnection === AppConnection.GitHubRadar) { - data = await handleGithubRadar(); + data = await handleGitHubRadar(); } else if (appConnection === AppConnection.GitLab) { - data = await handleGitlab(); + data = await handleGitLab(); } else if (appConnection === AppConnection.AzureKeyVault) { data = await handleAzureKeyVault(); } else if (appConnection === AppConnection.AzureAppConfiguration) { @@ -553,6 +610,8 @@ export const OAuthCallbackPage = () => { data = await handleAzureClientSecrets(); } else if (appConnection === AppConnection.AzureDevOps) { data = await handleAzureDevOps(); + } else if (appConnection === AppConnection.Heroku) { + data = await handleHeroku(); } if (data) { diff --git a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.types.ts b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.types.ts index 6c76a60b3..e2e5e3d68 100644 --- a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.types.ts +++ b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.types.ts @@ -6,7 +6,8 @@ import { TAzureKeyVaultConnection, TGitHubConnection, TGitHubRadarConnection, - TGitLabConnection + TGitLabConnection, + THerokuConnection } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; @@ -17,10 +18,10 @@ type BaseFormData = { projectId: string; }; -export type GithubFormData = BaseFormData & +export type GitHubFormData = BaseFormData & Pick; -export type GithubRadarFormData = BaseFormData & +export type GitHubRadarFormData = BaseFormData & Pick; export type GitLabFormData = BaseFormData & @@ -51,9 +52,12 @@ export type AzureDevOpsFormData = BaseFormData & Pick & (Pick | Pick); +export type HerokuFormData = BaseFormData & + Pick; + export type FormDataMap = { - [AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub }; - [AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar }; + [AppConnection.GitHub]: GitHubFormData & { app: AppConnection.GitHub }; + [AppConnection.GitHubRadar]: GitHubRadarFormData & { app: AppConnection.GitHubRadar }; [AppConnection.GitLab]: GitLabFormData & { app: AppConnection.GitLab }; [AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault }; [AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & { @@ -65,4 +69,7 @@ export type FormDataMap = { [AppConnection.AzureDevOps]: AzureDevOpsFormData & { app: AppConnection.AzureDevOps; }; + [AppConnection.Heroku]: HerokuFormData & { + app: AppConnection.Heroku; + }; };