feature: add suport for project scoped app connections

This commit is contained in:
Scott Wilson
2025-09-12 17:26:01 -07:00
parent 666ae3bc54
commit 93e5cf4b2b
135 changed files with 2823 additions and 742 deletions
@@ -4,6 +4,7 @@ import { SingleValue } from "react-select";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { AppConnectionOption } from "@app/components/app-connections";
import { createNotification } from "@app/components/notifications";
import {
Button,
@@ -201,17 +202,17 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
}, [popUp?.ca?.isOpen, popUp?.ca?.data, reset, ca]);
const { data: availableRoute53Connections, isPending: isRoute53Pending } =
useListAvailableAppConnections(AppConnection.AWS, {
useListAvailableAppConnections(AppConnection.AWS, currentWorkspace.id, {
enabled: caType === CaType.ACME
});
const { data: availableCloudflareConnections, isPending: isCloudflarePending } =
useListAvailableAppConnections(AppConnection.Cloudflare, {
useListAvailableAppConnections(AppConnection.Cloudflare, currentWorkspace.id, {
enabled: caType === CaType.ACME
});
const { data: availableAzureConnections, isPending: isAzurePending } =
useListAvailableAppConnections(AppConnection.AzureADCS, {
useListAvailableAppConnections(AppConnection.AzureADCS, currentWorkspace.id, {
enabled: caType === CaType.AZURE_AD_CS
});
@@ -457,6 +458,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
components={{ Option: AppConnectionOption }}
/>
</FormControl>
)}
@@ -1,24 +1,17 @@
import { Helmet } from "react-helmet";
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, PageHeader } from "@app/components/v2";
import { PageHeader } from "@app/components/v2";
import {
OrgPermissionAppConnectionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import {
AddAppConnectionModal,
AppConnectionsTable
} from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
import { AppConnectionsTable } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
export const AppConnectionsPage = withPermission(
() => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addConnection"] as const);
return (
<div className="bg-bunker-800">
<Helmet>
@@ -30,54 +23,18 @@ export const AppConnectionsPage = withPermission(
<div className="w-full max-w-7xl">
<PageHeader
className="w-full"
title={
<div className="flex w-full items-center">
<span>App Connections</span>
<a
className="-mt-1.5"
href="https://infisical.com/docs/integrations/app-connections/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Create}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
handlePopUpOpen("addConnection");
}}
isDisabled={!isAllowed}
className="ml-auto"
>
Add Connection
</Button>
)}
</OrgPermissionCan>
</div>
}
description="Create and configure connections with third-party apps for re-use across Infisical projects"
title="App Connections"
description="Manage organization App Connections"
/>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<AppConnectionsTable />
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addConnection", isOpen)}
/>
<div className="mb-4 flex w-full flex-col rounded-md border border-blue-500/50 bg-blue-500/30 px-4 py-2 text-sm text-blue-200">
<div className="flex items-center">
<FontAwesomeIcon icon={faInfoCircle} className="mb-0.5 mr-2 text-sm" />
<span className="text-sm text-blue-200">
App connections can also be created and managed independently in projects now.
</span>
</div>
</div>
<AppConnectionsTable />
</div>
</div>
</div>
@@ -3,6 +3,7 @@ import { useState } from "react";
import { Modal, ModalContent } from "@app/components/v2";
import { TAppConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { AppConnectionForm } from "./AppConnectionForm";
import { AppConnectionsSelect } from "./AppConnectionList";
@@ -10,29 +11,44 @@ import { AppConnectionsSelect } from "./AppConnectionList";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
projectId?: string;
projectType?: ProjectType;
app?: AppConnection;
onComplete?: (appConnection: TAppConnection) => void;
};
type ContentProps = {
onComplete: (appConnection: TAppConnection) => void;
projectId?: string;
projectType?: ProjectType;
app?: AppConnection;
};
const Content = ({ onComplete }: ContentProps) => {
const Content = ({ onComplete, projectId, projectType, app }: ContentProps) => {
const [selectedApp, setSelectedApp] = useState<AppConnection | null>(null);
if (selectedApp) {
if (app ?? selectedApp) {
return (
<AppConnectionForm
onComplete={onComplete}
onBack={() => setSelectedApp(null)}
app={selectedApp}
app={(app ?? selectedApp)!}
projectId={projectId}
/>
);
}
return <AppConnectionsSelect onSelect={setSelectedApp} />;
return <AppConnectionsSelect onSelect={setSelectedApp} projectType={projectType} />;
};
export const AddAppConnectionModal = ({ isOpen, onOpenChange }: Props) => {
export const AddAppConnectionModal = ({
isOpen,
onOpenChange,
projectId,
projectType,
app,
onComplete
}: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
@@ -40,7 +56,15 @@ export const AddAppConnectionModal = ({ isOpen, onOpenChange }: Props) => {
title="Add Connection"
subTitle="Select a third-party app to connect to."
>
<Content onComplete={() => onOpenChange(false)} />
<Content
projectId={projectId}
projectType={projectType}
app={app}
onComplete={(appConnection) => {
if (onComplete) onComplete(appConnection);
onOpenChange(false);
}}
/>
</ModalContent>
</Modal>
);
@@ -52,12 +52,15 @@ type FormProps = {
onComplete: (appConnection: TAppConnection) => void;
} & ({ appConnection: TAppConnection } | { app: AppConnection });
type CreateFormProps = FormProps & { app: AppConnection };
type CreateFormProps = FormProps & {
app: AppConnection;
projectId?: string;
};
type UpdateFormProps = FormProps & {
appConnection: TAppConnection;
};
const CreateForm = ({ app, onComplete }: CreateFormProps) => {
const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
const createAppConnection = useCreateAppConnection();
const { name: appName } = APP_CONNECTION_MAP[app];
@@ -68,7 +71,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
>
) => {
try {
const connection = await createAppConnection.mutateAsync(formData);
const connection = await createAppConnection.mutateAsync({
...formData,
projectId
});
createNotification({
text: `Successfully added ${appName} Connection`,
type: "success"
@@ -88,15 +94,15 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.AWS:
return <AwsConnectionForm onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm />;
return <GitHubConnectionForm projectId={projectId} />;
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm />;
return <GitHubRadarConnectionForm projectId={projectId} />;
case AppConnection.GCP:
return <GcpConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
return <AzureKeyVaultConnectionForm onSubmit={onSubmit} />;
return <AzureKeyVaultConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.AzureAppConfiguration:
return <AzureAppConfigurationConnectionForm onSubmit={onSubmit} />;
return <AzureAppConfigurationConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.AzureADCS:
return <AzureADCSConnectionForm onSubmit={onSubmit} />;
case AppConnection.Databricks:
@@ -118,9 +124,9 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureClientSecrets:
return <AzureClientSecretsConnectionForm onSubmit={onSubmit} />;
return <AzureClientSecretsConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.AzureDevOps:
return <AzureDevOpsConnectionForm onSubmit={onSubmit} />;
return <AzureDevOpsConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.Windmill:
return <WindmillConnectionForm onSubmit={onSubmit} />;
case AppConnection.Auth0:
@@ -142,7 +148,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.Flyio:
return <FlyioConnectionForm onSubmit={onSubmit} />;
case AppConnection.GitLab:
return <GitLabConnectionForm onSubmit={onSubmit} />;
return <GitLabConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} />;
case AppConnection.Bitbucket:
@@ -200,16 +206,33 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.AWS:
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm appConnection={appConnection} />;
return (
<GitHubConnectionForm appConnection={appConnection} projectId={appConnection.projectId} />
);
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm appConnection={appConnection} />;
return (
<GitHubRadarConnectionForm
appConnection={appConnection}
projectId={appConnection.projectId}
/>
);
case AppConnection.GCP:
return <GcpConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
return <AzureKeyVaultConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
return (
<AzureKeyVaultConnectionForm
appConnection={appConnection}
onSubmit={onSubmit}
projectId={appConnection.projectId}
/>
);
case AppConnection.AzureAppConfiguration:
return (
<AzureAppConfigurationConnectionForm appConnection={appConnection} onSubmit={onSubmit} />
<AzureAppConfigurationConnectionForm
appConnection={appConnection}
onSubmit={onSubmit}
projectId={appConnection.projectId}
/>
);
case AppConnection.AzureADCS:
return <AzureADCSConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
@@ -232,9 +255,21 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.AzureClientSecrets:
return <AzureClientSecretsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
return (
<AzureClientSecretsConnectionForm
appConnection={appConnection}
onSubmit={onSubmit}
projectId={appConnection.projectId}
/>
);
case AppConnection.AzureDevOps:
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
return (
<AzureDevOpsConnectionForm
appConnection={appConnection}
onSubmit={onSubmit}
projectId={appConnection.projectId}
/>
);
case AppConnection.Windmill:
return <WindmillConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Auth0:
@@ -256,7 +291,13 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.Flyio:
return <FlyioConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.GitLab:
return <GitLabConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
return (
<GitLabConnectionForm
onSubmit={onSubmit}
appConnection={appConnection}
projectId={appConnection.projectId}
/>
);
case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Bitbucket:
@@ -278,12 +319,12 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
}
};
type Props = { onBack?: () => void } & Pick<FormProps, "onComplete"> &
type Props = { onBack?: () => void; projectId?: string } & Pick<FormProps, "onComplete"> &
(
| { app: AppConnection; appConnection?: undefined }
| { app?: undefined; appConnection: TAppConnection }
);
export const AppConnectionForm = ({ onBack, ...props }: Props) => {
export const AppConnectionForm = ({ onBack, projectId, ...props }: Props) => {
const { app, appConnection } = props;
return (
@@ -296,7 +337,7 @@ export const AppConnectionForm = ({ onBack, ...props }: Props) => {
{appConnection ? (
<UpdateForm {...props} appConnection={appConnection} />
) : (
<CreateForm {...props} app={app} />
<CreateForm {...props} app={app} projectId={projectId} />
)}
</div>
);
@@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, 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 {
AzureAppConfigurationConnectionMethod,
@@ -15,6 +19,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { AzureAppConfigurationFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -25,6 +30,7 @@ type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type Props = {
appConnection?: TAzureAppConfigurationConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
projectId: string | undefined | null;
};
const baseSchema = genericAppConnectionFieldsSchema.extend({
@@ -96,7 +102,11 @@ const getDefaultValues = (appConnection?: TAzureAppConfigurationConnection): Par
return base;
};
export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }: Props) => {
export const AzureAppConfigurationConnectionForm = ({
appConnection,
onSubmit,
projectId
}: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -110,6 +120,8 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }:
defaultValues: getDefaultValues(appConnection)
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -128,7 +140,12 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }:
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureAppConfigurationConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as AzureAppConfigurationFormData)
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-app-configuration`
@@ -7,7 +7,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, 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 {
AzureClientSecretsConnectionMethod,
@@ -16,6 +20,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { AzureClientSecretsFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -26,6 +31,7 @@ type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type Props = {
appConnection?: TAzureClientSecretsConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
projectId: string | undefined | null;
};
const baseSchema = genericAppConnectionFieldsSchema.extend({
@@ -97,7 +103,7 @@ const getDefaultValues = (appConnection?: TAzureClientSecretsConnection): Partia
return base;
};
export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Props) => {
export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -111,6 +117,8 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
defaultValues: getDefaultValues(appConnection)
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -129,7 +137,12 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureClientSecretsConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as AzureClientSecretsFormData)
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://graph.microsoft.com/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
@@ -7,7 +7,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, 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 {
AzureDevOpsConnectionMethod,
@@ -16,6 +20,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { AzureDevOpsFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -65,6 +70,7 @@ type OnSubmitForm = z.infer<typeof accessTokenSchema> | z.infer<typeof clientSec
type Props = {
appConnection?: TAzureDevOpsConnection;
onSubmit: (formData: OnSubmitForm) => Promise<void>;
projectId: string | undefined | null;
};
const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial<FormData> => {
@@ -132,7 +138,7 @@ const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial<FormD
return base;
};
export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) => {
export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -146,6 +152,8 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
defaultValues: getDefaultValues(appConnection)
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -164,7 +172,12 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureDevOpsConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as AzureDevOpsFormData)
);
window.location.assign(
@@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, 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";
@@ -15,6 +19,7 @@ import {
TAzureKeyVaultConnection
} from "@app/hooks/api/appConnections/types/azure-key-vault-connection";
import { AzureKeyVaultFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -25,6 +30,7 @@ type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type Props = {
appConnection?: TAzureKeyVaultConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
projectId: string | undefined | null;
};
const baseSchema = genericAppConnectionFieldsSchema.extend({
@@ -96,7 +102,7 @@ const getDefaultValues = (appConnection?: TAzureKeyVaultConnection): Partial<For
return base;
};
export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props) => {
export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -110,6 +116,8 @@ export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props)
defaultValues: getDefaultValues(appConnection)
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -129,7 +137,12 @@ export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props)
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureKeyVaultConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as AzureKeyVaultFormData)
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://vault.azure.net/.default%20openid%20offline_access&state=${state}<:>azure-key-vault`
@@ -25,7 +25,11 @@ import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
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 { gatewaysQueryKeys } from "@app/hooks/api";
import {
@@ -35,6 +39,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { GithubFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -42,6 +47,7 @@ import {
type Props = {
appConnection?: TGitHubConnection;
projectId: string | undefined | null;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
@@ -63,7 +69,7 @@ const formSchema = genericAppConnectionFieldsSchema.extend({
type FormData = z.infer<typeof formSchema>;
export const GitHubConnectionForm = ({ appConnection }: Props) => {
export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -98,13 +104,21 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
const selectedMethod = watch("method");
const instanceType = watch("credentials.instanceType");
const returnUrl = useGetAppConnectionOauthReturnUrl();
const onSubmit = (formData: FormData) => {
setIsRedirecting(true);
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"githubConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
credentials: formData.credentials as TGitHubConnection["credentials"],
connectionId: appConnection?.id,
projectId,
returnUrl
} as GithubFormData)
);
const githubHost =
@@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, ModalClose, 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 {
GitHubRadarConnectionMethod,
@@ -15,6 +19,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { GithubRadarFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -22,6 +27,7 @@ import {
type Props = {
appConnection?: TGitHubRadarConnection;
projectId: string | undefined | null;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
@@ -31,7 +37,7 @@ const formSchema = genericAppConnectionFieldsSchema.extend({
type FormData = z.infer<typeof formSchema>;
export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
export const GitHubRadarConnectionForm = ({ appConnection, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -48,6 +54,8 @@ export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
}
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -63,7 +71,12 @@ export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"githubRadarConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as GithubRadarFormData)
);
switch (formData.method) {
@@ -16,7 +16,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";
@@ -26,6 +30,7 @@ import {
TGitLabConnection
} from "@app/hooks/api/appConnections/types/gitlab-connection";
import { GitLabFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -34,6 +39,7 @@ import {
type Props = {
appConnection?: TGitLabConnection;
onSubmit: (formData: FormData) => Promise<void>;
projectId: string | undefined | null;
};
const formSchema = z.discriminatedUnion("method", [
@@ -72,7 +78,7 @@ const formSchema = z.discriminatedUnion("method", [
type FormData = z.infer<typeof formSchema>;
export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Props) => {
export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -98,6 +104,8 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
} as FormData))
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -132,8 +140,10 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
JSON.stringify({
...formData,
connectionId: appConnection?.id,
isUpdate
})
isUpdate,
projectId,
returnUrl
} as GitLabFormData)
);
// Redirect to Gitlab OAuth
@@ -9,14 +9,16 @@ import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useAppConnectionOptions } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { ProjectType } from "@app/hooks/api/workspace/types";
type Props = {
onSelect: (app: AppConnection) => void;
projectType?: ProjectType;
};
export const AppConnectionsSelect = ({ onSelect }: Props) => {
export const AppConnectionsSelect = ({ onSelect, projectType }: Props) => {
const { subscription } = useSubscription();
const { isPending, data: appConnectionOptions } = useAppConnectionOptions();
const { isPending, data: appConnectionOptions } = useAppConnectionOptions(projectType);
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
@@ -1,19 +1,23 @@
import { useCallback } from "react";
import { subject } from "@casl/ability";
import {
faAsterisk,
faBuilding,
faCheck,
faCopy,
faEdit,
faEllipsisV,
faInfoCircle,
faServer,
faTable,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { VariablePermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
@@ -25,9 +29,11 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionSubjects } from "@app/context";
import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { getProjectBaseURL } from "@app/helpers/project";
import { useToggle } from "@app/hooks";
import { TAppConnection } from "@app/hooks/api/appConnections";
@@ -36,15 +42,18 @@ type Props = {
onDelete: (appConnection: TAppConnection) => void;
onEditCredentials: (appConnection: TAppConnection) => void;
onEditDetails: (appConnection: TAppConnection) => void;
isProjectView: boolean;
};
export const AppConnectionRow = ({
appConnection,
onDelete,
onEditCredentials,
onEditDetails
onEditDetails,
isProjectView
}: Props) => {
const { id, name, method, app, description, isPlatformManagedCredentials } = appConnection;
const { id, name, method, app, description, isPlatformManagedCredentials, project } =
appConnection;
const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -111,7 +120,38 @@ export const AppConnectionRow = ({
{methodDetails.name}
</p>
</Td>
{!isProjectView && (
<Td className="!min-w-[8rem] max-w-0">
{project ? (
<Link
// @ts-expect-error app-connections aren't in kms/ssh
to={`${getProjectBaseURL(project.type)}/app-connections`}
params={{
projectId: project.id
}}
className="underline"
>
<p className="truncate">
<FontAwesomeIcon
size="sm"
className="mr-1.5 text-mineshaft-300/75"
icon={faTable}
/>
{project.name}
</p>
</Link>
) : (
<p className="truncate">
<FontAwesomeIcon
size="sm"
className="mr-1.5 text-mineshaft-300/75"
icon={faBuilding}
/>
Organization
</p>
)}
</Td>
)}
<Td>
<div className="flex items-center justify-end gap-2">
{isPlatformManagedCredentials && (
@@ -143,48 +183,91 @@ export const AppConnectionRow = ({
>
Copy Connection ID
</DropdownMenuItem>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Edit}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => onEditDetails(appConnection)}
{(isProjectView || !project) && (
<>
<VariablePermissionCan
type={isProjectView ? "project" : "org"}
I={
isProjectView
? ProjectPermissionAppConnectionActions.Edit
: OrgPermissionAppConnectionActions.Edit
}
a={
isProjectView
? subject(ProjectPermissionSub.AppConnections, {
connectionId: id
})
: subject(OrgPermissionSubjects.AppConnections, {
connectionId: id
})
}
>
Edit Details
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Edit}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faAsterisk} />}
onClick={() => onEditCredentials(appConnection)}
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => onEditDetails(appConnection)}
>
Edit Details
</DropdownMenuItem>
)}
</VariablePermissionCan>
<VariablePermissionCan
type={isProjectView ? "project" : "org"}
I={
isProjectView
? ProjectPermissionAppConnectionActions.Edit
: OrgPermissionAppConnectionActions.Edit
}
a={
isProjectView
? subject(ProjectPermissionSub.AppConnections, {
connectionId: id
})
: subject(OrgPermissionSubjects.AppConnections, {
connectionId: id
})
}
>
{isPlatformManagedCredentials ? "View" : "Edit"} Credentials
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Delete}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => onDelete(appConnection)}
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faAsterisk} />}
onClick={() => onEditCredentials(appConnection)}
>
{isPlatformManagedCredentials ? "View" : "Edit"} Credentials
</DropdownMenuItem>
)}
</VariablePermissionCan>
<VariablePermissionCan
type={isProjectView ? "project" : "org"}
I={
isProjectView
? ProjectPermissionAppConnectionActions.Delete
: OrgPermissionAppConnectionActions.Delete
}
a={
isProjectView
? subject(ProjectPermissionSub.AppConnections, {
connectionId: id
})
: subject(OrgPermissionSubjects.AppConnections, {
connectionId: id
})
}
>
Delete Connection
</DropdownMenuItem>
)}
</OrgPermissionCan>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => onDelete(appConnection)}
>
Delete Connection
</DropdownMenuItem>
)}
</VariablePermissionCan>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
@@ -2,16 +2,21 @@ import { useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faArrowUpRightFromSquare,
faBookOpen,
faCheckCircle,
faFilter,
faMagnifyingGlass,
faPlug,
faPlus,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { VariablePermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -29,6 +34,9 @@ import {
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
getUserTablePreference,
@@ -39,7 +47,9 @@ import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { TAppConnection, useListAppConnections } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { AddAppConnectionModal } from "./AddAppConnectionModal";
import { AppConnectionRow } from "./AppConnectionRow";
import { DeleteAppConnectionModal } from "./DeleteAppConnectionModal";
import { EditAppConnectionCredentialsModal } from "./EditAppConnectionCredentialsModal";
@@ -48,22 +58,45 @@ import { EditAppConnectionDetailsModal } from "./EditAppConnectionDetailsModal";
enum AppConnectionsOrderBy {
App = "app",
Name = "name",
Method = "method"
Method = "method",
ManagedBy = "managed-by"
}
type AppConnectionFilters = {
apps: AppConnection[];
};
export const AppConnectionsTable = () => {
const { isPending, data: appConnections = [] } = useListAppConnections();
enum View {
All = "all",
Scope = "scope"
}
const APP_CONNECTION_VIEW_STORAGE_KEY = "app-connection-view";
type Props = {
projectId?: string;
projectType?: ProjectType;
};
export const AppConnectionsTable = ({ projectId, projectType }: Props) => {
const isProjectView = Boolean(projectId);
const { isPending, data: appConnections = [] } = useListAppConnections(projectId);
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"addConnection",
"deleteConnection",
"editCredentials",
"editDetails"
] as const);
const [view, setView] = useState<View>(() => {
const storedView = localStorage.getItem(APP_CONNECTION_VIEW_STORAGE_KEY) as View | null;
if (storedView && Object.values(View).includes(storedView)) return storedView;
return View.Scope;
});
const [filters, setFilters] = useState<AppConnectionFilters>({
apps: []
});
@@ -96,6 +129,10 @@ export const AppConnectionsTable = () => {
.filter((appConnection) => {
const { app, method, name } = appConnection;
if (view === View.Scope && !isProjectView && appConnection.projectId) {
return false;
}
if (filters.apps.length && !filters.apps.includes(app)) return false;
const searchValue = search.trim().toLowerCase();
@@ -121,6 +158,13 @@ export const AppConnectionsTable = () => {
.localeCompare(
getAppConnectionMethodDetails(connectionTwo.method).name.toLowerCase()
);
case AppConnectionsOrderBy.ManagedBy:
if (!connectionOne.project) return 1;
if (!connectionTwo.project) return -1;
return connectionOne.project.name
.toLowerCase()
.localeCompare(connectionTwo.project.name.toLowerCase());
case AppConnectionsOrderBy.App:
default:
return APP_CONNECTION_MAP[connectionOne.app].name
@@ -128,7 +172,7 @@ export const AppConnectionsTable = () => {
.localeCompare(APP_CONNECTION_MAP[connectionTwo.app].name.toLowerCase());
}
}),
[appConnections, orderDirection, search, orderBy, filters]
[appConnections, orderDirection, search, orderBy, filters, view]
);
useResetPageHelper({
@@ -165,8 +209,87 @@ export const AppConnectionsTable = () => {
handlePopUpOpen("editDetails", appConnection);
return (
<div>
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">App Connections</p>
<a
href="https://infisical.com/docs/integrations/app-connections/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.12rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm text-bunker-300">
Create and configure connections with third-party apps for re-use across your project
{isProjectView ? "" : "s"}.
</p>
</div>
<VariablePermissionCan
type={isProjectView ? "project" : "org"}
I={
isProjectView
? ProjectPermissionAppConnectionActions.Create
: OrgPermissionAppConnectionActions.Create
}
a={
isProjectView
? ProjectPermissionSub.AppConnections
: OrgPermissionSubjects.AppConnections
}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addConnection")}
isDisabled={!isAllowed}
>
Add Connection
</Button>
)}
</VariablePermissionCan>
</div>
<div className="flex gap-2">
{!isProjectView && (
<div className="flex gap-x-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<Button
variant="outline_bg"
onClick={() => {
setView(View.Scope);
localStorage.setItem(APP_CONNECTION_VIEW_STORAGE_KEY, View.Scope);
}}
size="xs"
className={`${
view === View.Scope ? "bg-mineshaft-500" : "bg-transparent"
} min-w-[5rem] rounded border-none hover:bg-mineshaft-600`}
>
Organization
</Button>
<Button
variant="outline_bg"
onClick={() => {
setView(View.All);
localStorage.setItem(APP_CONNECTION_VIEW_STORAGE_KEY, View.All);
}}
size="xs"
className={`${
view === View.All ? "bg-mineshaft-500" : "bg-transparent"
} min-w-[5rem] rounded border-none hover:bg-mineshaft-600`}
>
All
</Button>
</div>
)}
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
@@ -269,7 +392,21 @@ export const AppConnectionsTable = () => {
</IconButton>
</div>
</Th>
{!isProjectView && (
<Th>
<div className="flex items-center">
Managed By
<IconButton
variant="plain"
className={getClassName(AppConnectionsOrderBy.ManagedBy)}
ariaLabel="sort"
onClick={() => handleSort(AppConnectionsOrderBy.ManagedBy)}
>
<FontAwesomeIcon icon={getColSortIcon(AppConnectionsOrderBy.ManagedBy)} />
</IconButton>
</div>
</Th>
)}
<Th className="w-5" />
</Tr>
</THead>
@@ -284,6 +421,7 @@ export const AppConnectionsTable = () => {
onDelete={handleDelete}
onEditCredentials={handleEditCredentials}
onEditDetails={handleEditDetails}
isProjectView={isProjectView}
/>
))}
</TBody>
@@ -323,6 +461,12 @@ export const AppConnectionsTable = () => {
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
appConnection={popUp.editDetails.data}
/>
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addConnection", isOpen)}
projectId={projectId}
projectType={projectType}
/>
</div>
);
};
@@ -12,73 +12,14 @@ import {
AzureKeyVaultConnectionMethod,
GitHubConnectionMethod,
GitLabConnectionMethod,
TAzureAppConfigurationConnection,
TAzureClientSecretsConnection,
TAzureDevOpsConnection,
TAzureKeyVaultConnection,
TGitHubConnection,
TGitHubRadarConnection,
TGitLabConnection,
TAppConnection,
useCreateAppConnection,
useUpdateAppConnection
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { IntegrationsListPageTabs } from "@app/types/integrations";
type BaseFormData = {
returnUrl?: string;
connectionId?: string;
isUpdate?: boolean;
};
type GithubFormData = BaseFormData &
Pick<TGitHubConnection, "name" | "method" | "description" | "gatewayId" | "credentials">;
type GithubRadarFormData = BaseFormData &
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
type GitLabFormData = BaseFormData &
Pick<TGitLabConnection, "name" | "method" | "description" | "credentials">;
type AzureKeyVaultFormData = BaseFormData &
Pick<TAzureKeyVaultConnection, "name" | "method" | "description"> &
Pick<TAzureKeyVaultConnection["credentials"], "tenantId">;
type AzureAppConfigurationFormData = BaseFormData &
Pick<TAzureAppConfigurationConnection, "name" | "method" | "description"> &
Pick<TAzureAppConfigurationConnection["credentials"], "tenantId">;
type AzureClientSecretsFormData = BaseFormData &
Pick<TAzureClientSecretsConnection, "name" | "method" | "description"> &
Pick<TAzureClientSecretsConnection["credentials"], "tenantId">;
type OAuthCredentials = Extract<
TAzureDevOpsConnection,
{ method: AzureDevOpsConnectionMethod.OAuth }
>["credentials"];
type AccessTokenCredentials = Extract<
TAzureDevOpsConnection,
{ method: AzureDevOpsConnectionMethod.AccessToken }
>["credentials"];
type AzureDevOpsFormData = BaseFormData &
Pick<TAzureDevOpsConnection, "name" | "method" | "description"> &
(Pick<OAuthCredentials, "tenantId" | "orgName"> | Pick<AccessTokenCredentials, "orgName">);
type FormDataMap = {
[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 & {
app: AppConnection.AzureAppConfiguration;
};
[AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & {
app: AppConnection.AzureClientSecrets;
};
[AppConnection.AzureDevOps]: AzureDevOpsFormData & {
app: AppConnection.AzureDevOps;
};
};
import { FormDataMap } from "./OauthCallbackPage.types";
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
[AppConnection.GitHub]: "githubConnectionFormData",
@@ -148,11 +89,14 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitLab);
const { connectionId, name, description, returnUrl, isUpdate, credentials } = formData;
const { connectionId, name, description, returnUrl, isUpdate, projectId, credentials } =
formData;
let connection: TAppConnection;
try {
if (isUpdate && connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.GitLab,
connectionId,
credentials: {
@@ -161,10 +105,11 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.GitLab,
name,
description,
projectId,
method: GitLabConnectionMethod.OAuth,
credentials: {
code: code as string,
@@ -173,14 +118,12 @@ export const OAuthCallbackPage = () => {
});
}
navigate({
to: returnUrl ?? "/organization/app-connections"
});
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
} catch (err: any) {
createNotification({
@@ -189,7 +132,10 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
@@ -201,11 +147,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureKeyVault);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureKeyVault,
connectionId,
credentials: {
@@ -214,10 +162,11 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureKeyVault,
name,
description,
projectId,
method: AzureKeyVaultConnectionMethod.OAuth,
credentials: {
tenantId: formData.tenantId,
@@ -232,14 +181,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -249,11 +204,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureAppConfiguration);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureAppConfiguration,
connectionId,
credentials: {
@@ -262,10 +219,11 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureAppConfiguration,
name,
description,
projectId,
method: AzureAppConfigurationConnectionMethod.OAuth,
credentials: {
code: code as string,
@@ -280,14 +238,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -297,11 +261,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureClientSecrets);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureClientSecrets,
connectionId,
credentials: {
@@ -310,11 +276,12 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureClientSecrets,
name,
description,
method: AzureClientSecretsConnectionMethod.OAuth,
projectId,
credentials: {
code: code as string,
tenantId: formData.tenantId
@@ -328,14 +295,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -345,7 +318,9 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureDevOps);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (!("tenantId" in formData)) {
@@ -353,7 +328,7 @@ export const OAuthCallbackPage = () => {
}
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureDevOps,
connectionId,
credentials: {
@@ -363,11 +338,12 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureDevOps,
name,
description,
method: AzureDevOpsConnectionMethod.OAuth,
projectId,
credentials: {
code: code as string,
tenantId: formData.tenantId as string,
@@ -382,14 +358,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -399,11 +381,14 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitHub);
const { connectionId, name, description, returnUrl, gatewayId, credentials } = formData;
const { connectionId, name, description, returnUrl, gatewayId, credentials, projectId } =
formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.GitHub,
...(installationId
? {
@@ -427,10 +412,11 @@ export const OAuthCallbackPage = () => {
})
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.GitHub,
name,
description,
projectId,
...(installationId
? {
method: GitHubConnectionMethod.App,
@@ -460,14 +446,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -477,11 +469,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitHubRadar);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
connectionId,
credentials: {
@@ -490,11 +484,12 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
name,
description,
method: GitHubConnectionMethod.App,
projectId,
credentials: {
code: code as string,
installationId: installationId as string
@@ -508,14 +503,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -530,8 +531,13 @@ export const OAuthCallbackPage = () => {
if (!isReady) return;
(async () => {
let data: { connectionId?: string; returnUrl?: string; appConnectionName?: string } | null =
null;
let data: {
returnUrl: string;
appConnectionName: string;
connectionId?: string;
projectId?: string;
connection: TAppConnection;
} | null = null;
if (appConnection === AppConnection.GitHub) {
data = await handleGithub();
@@ -554,16 +560,26 @@ export const OAuthCallbackPage = () => {
text: `Successfully ${data.connectionId ? "updated" : "added"} ${data.appConnectionName ? APP_CONNECTION_MAP[data.appConnectionName as AppConnection].name : ""} Connection`,
type: "success"
});
} else {
createNotification({
text: "Failed to add connection",
type: "error"
await navigate({
to: data.returnUrl,
params: {
projectId: data.projectId ?? undefined
},
// scott: if it's not an app connection page we need to pass connection details as it's an inline creation
search: data.returnUrl.includes("app-connections")
? undefined
: {
connectionId: data.connection.id,
connectionName: data.connection.name,
...(data.returnUrl.includes("integrations")
? {
selectedTab: IntegrationsListPageTabs.SecretSyncs
}
: {})
}
});
}
await navigate({
to: data?.returnUrl ?? "/organization/app-connections"
});
})();
}, [isReady]);
@@ -0,0 +1,68 @@
import {
AzureDevOpsConnectionMethod,
TAzureAppConfigurationConnection,
TAzureClientSecretsConnection,
TAzureDevOpsConnection,
TAzureKeyVaultConnection,
TGitHubConnection,
TGitHubRadarConnection,
TGitLabConnection
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
type BaseFormData = {
returnUrl: string;
connectionId?: string;
isUpdate?: boolean;
projectId: string;
};
export type GithubFormData = BaseFormData &
Pick<TGitHubConnection, "name" | "method" | "description" | "gatewayId" | "credentials">;
export type GithubRadarFormData = BaseFormData &
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
export type GitLabFormData = BaseFormData &
Pick<TGitLabConnection, "name" | "method" | "description" | "credentials">;
export type AzureKeyVaultFormData = BaseFormData &
Pick<TAzureKeyVaultConnection, "name" | "method" | "description"> &
Pick<TAzureKeyVaultConnection["credentials"], "tenantId">;
export type AzureAppConfigurationFormData = BaseFormData &
Pick<TAzureAppConfigurationConnection, "name" | "method" | "description"> &
Pick<TAzureAppConfigurationConnection["credentials"], "tenantId">;
export type AzureClientSecretsFormData = BaseFormData &
Pick<TAzureClientSecretsConnection, "name" | "method" | "description"> &
Pick<TAzureClientSecretsConnection["credentials"], "tenantId">;
type OAuthCredentials = Extract<
TAzureDevOpsConnection,
{ method: AzureDevOpsConnectionMethod.OAuth }
>["credentials"];
type AccessTokenCredentials = Extract<
TAzureDevOpsConnection,
{ method: AzureDevOpsConnectionMethod.AccessToken }
>["credentials"];
export type AzureDevOpsFormData = BaseFormData &
Pick<TAzureDevOpsConnection, "name" | "method" | "description"> &
(Pick<OAuthCredentials, "tenantId" | "orgName"> | Pick<AccessTokenCredentials, "orgName">);
export type FormDataMap = {
[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 & {
app: AppConnection.AzureAppConfiguration;
};
[AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & {
app: AppConnection.AzureClientSecrets;
};
[AppConnection.AzureDevOps]: AzureDevOpsFormData & {
app: AppConnection.AzureDevOps;
};
};
@@ -4,11 +4,11 @@ import {
faArrowUpRightFromSquare,
faBookOpen,
faCopy,
faDoorClosed,
faEdit,
faEllipsisV,
faInfoCircle,
faMagnifyingGlass,
faPlug,
faSearch,
faTrash
} from "@fortawesome/free-solid-svg-icons";
@@ -243,7 +243,7 @@ export const GatewayListPage = withPermission(
? "No Gateways match search..."
: "No Gateways have been configured"
}
icon={gateways?.length ? faSearch : faPlug}
icon={gateways?.length ? faSearch : faDoorClosed}
/>
)}
<DeleteActionModal
@@ -0,0 +1,44 @@
import { Helmet } from "react-helmet";
import { PageHeader } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
ProjectPermissionAppConnectionActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { withProjectPermission } from "@app/hoc";
import { AppConnectionsTable } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
export const AppConnectionsPage = withProjectPermission(
() => {
const { currentWorkspace } = useWorkspace();
return (
<div className="bg-bunker-800">
<Helmet>
<title>Infisical | App Connections</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
className="w-full"
title="App Connections"
description="Manage project App Connections"
/>
<AppConnectionsTable
projectId={currentWorkspace.id}
projectType={currentWorkspace.type}
/>
</div>
</div>
</div>
);
},
{
action: ProjectPermissionAppConnectionActions.Read,
subject: ProjectPermissionSub.AppConnections
}
);
@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { AppConnectionsPage } from "./AppConnectionsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections"
)({
component: AppConnectionsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "App Connections"
}
]
};
}
});
@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { AppConnectionsPage } from "./AppConnectionsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections"
)({
component: AppConnectionsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "App Connections"
}
]
};
}
});
@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { AppConnectionsPage } from "./AppConnectionsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections"
)({
component: AppConnectionsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "App Connections"
}
]
};
}
});
@@ -0,0 +1,19 @@
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { ConditionsFields } from "./ConditionsFields";
type Props = {
position?: number;
isDisabled?: boolean;
};
export const AppConnectionPermissionConditions = ({ position = 0, isDisabled }: Props) => {
return (
<ConditionsFields
isDisabled={isDisabled}
subject={ProjectPermissionSub.AppConnections}
position={position}
selectOptions={[{ value: "connectionId", label: "Connection ID" }]}
/>
);
};
@@ -23,6 +23,7 @@ export const renderOperatorSelectItems = (type: string) => {
case "secretTags":
return <SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>;
case "identityId":
case "connectionId":
return (
<>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
@@ -12,6 +12,7 @@ import {
} from "@app/context";
import {
PermissionConditionOperators,
ProjectPermissionAppConnectionActions,
ProjectPermissionAuditLogsActions,
ProjectPermissionCommitsActions,
ProjectPermissionDynamicSecretActions,
@@ -134,6 +135,14 @@ const SecretScanningConfigPolicyActionSchema = z.object({
[ProjectPermissionSecretScanningConfigActions.Update]: z.boolean().optional()
});
const AppConnectionPolicyActionSchema = z.object({
[ProjectPermissionAppConnectionActions.Create]: z.boolean().optional(),
[ProjectPermissionAppConnectionActions.Read]: z.boolean().optional(),
[ProjectPermissionAppConnectionActions.Edit]: z.boolean().optional(),
[ProjectPermissionAppConnectionActions.Delete]: z.boolean().optional(),
[ProjectPermissionAppConnectionActions.Connect]: z.boolean().optional()
});
const KmipPolicyActionSchema = z.object({
[ProjectPermissionKmipActions.ReadClients]: z.boolean().optional(),
[ProjectPermissionKmipActions.CreateClients]: z.boolean().optional(),
@@ -311,6 +320,12 @@ export const projectRoleFormSchema = z.object({
})
.array()
.default([]),
[ProjectPermissionSub.AppConnections]: AppConnectionPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.Commits]: CommitPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Member]: MemberPolicyActionSchema.array().default([]),
@@ -393,7 +408,8 @@ type TConditionalFields =
| ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.Identity
| ProjectPermissionSub.SecretSyncs
| ProjectPermissionSub.SecretEvents;
| ProjectPermissionSub.SecretEvents
| ProjectPermissionSub.AppConnections;
export const isConditionalSubjects = (
subject: ProjectPermissionSub
@@ -408,7 +424,8 @@ export const isConditionalSubjects = (
subject === ProjectPermissionSub.PkiSubscribers ||
subject === ProjectPermissionSub.CertificateTemplates ||
subject === ProjectPermissionSub.SecretSyncs ||
subject === ProjectPermissionSub.SecretEvents;
subject === ProjectPermissionSub.SecretEvents ||
subject === ProjectPermissionSub.AppConnections;
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
const formConditions: z.infer<typeof ConditionSchema> = [];
@@ -515,7 +532,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshHostGroups,
ProjectPermissionSub.SecretSyncs,
ProjectPermissionSub.SecretEvents
ProjectPermissionSub.SecretEvents,
ProjectPermissionSub.AppConnections
].includes(subject)
) {
// from above statement we are sure it won't be undefined
@@ -654,6 +672,27 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
return;
}
if (subject === ProjectPermissionSub.AppConnections) {
const canCreate = action.includes(ProjectPermissionAppConnectionActions.Create);
const canRead = action.includes(ProjectPermissionAppConnectionActions.Read);
const canEdit = action.includes(ProjectPermissionAppConnectionActions.Edit);
const canDelete = action.includes(ProjectPermissionAppConnectionActions.Delete);
const canConnect = action.includes(ProjectPermissionAppConnectionActions.Connect);
// from above statement we are sure it won't be undefined
formVal[subject]!.push({
[ProjectPermissionAppConnectionActions.Read]: canRead,
[ProjectPermissionAppConnectionActions.Create]: canCreate,
[ProjectPermissionAppConnectionActions.Edit]: canEdit,
[ProjectPermissionAppConnectionActions.Delete]: canDelete,
[ProjectPermissionAppConnectionActions.Connect]: canConnect,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
return;
}
// for other subjects
const canRead = action.includes(ProjectPermissionActions.Read);
const canEdit = action.includes(ProjectPermissionActions.Edit);
@@ -1597,6 +1636,31 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
value: ProjectPermissionSecretEventActions.SubscribeImportMutations
}
]
},
[ProjectPermissionSub.AppConnections]: {
title: "App Connections",
actions: [
{
label: "Read",
value: ProjectPermissionAppConnectionActions.Read
},
{
label: "Create",
value: ProjectPermissionAppConnectionActions.Create
},
{
label: "Update",
value: ProjectPermissionAppConnectionActions.Edit
},
{
label: "Delete",
value: ProjectPermissionAppConnectionActions.Delete
},
{
label: "Connect",
value: ProjectPermissionAppConnectionActions.Connect
}
]
}
};
@@ -1669,7 +1733,8 @@ export const ProjectTypePermissionSubjects: Record<
...KmsPermissionSubjects(),
...CertificateManagerPermissionSubjects(),
...SshPermissionSubjects(),
...SecretScanningSubject()
...SecretScanningSubject(),
[ProjectPermissionSub.AppConnections]: true
},
[ProjectType.KMS]: {
...SharedPermissionSubjects,
@@ -1677,7 +1742,8 @@ export const ProjectTypePermissionSubjects: Record<
...SecretsManagerPermissionSubjects(),
...CertificateManagerPermissionSubjects(),
...SshPermissionSubjects(),
...SecretScanningSubject()
...SecretScanningSubject(),
[ProjectPermissionSub.AppConnections]: false
},
[ProjectType.CertificateManager]: {
...SharedPermissionSubjects,
@@ -1685,7 +1751,8 @@ export const ProjectTypePermissionSubjects: Record<
...KmsPermissionSubjects(),
...SecretsManagerPermissionSubjects(),
...SshPermissionSubjects(),
...SecretScanningSubject()
...SecretScanningSubject(),
[ProjectPermissionSub.AppConnections]: true
},
[ProjectType.SSH]: {
...SharedPermissionSubjects,
@@ -1693,7 +1760,8 @@ export const ProjectTypePermissionSubjects: Record<
...CertificateManagerPermissionSubjects(),
...KmsPermissionSubjects(),
...SecretsManagerPermissionSubjects(),
...SecretScanningSubject()
...SecretScanningSubject(),
[ProjectPermissionSub.AppConnections]: false
},
[ProjectType.SecretScanning]: {
...SharedPermissionSubjects,
@@ -1701,7 +1769,8 @@ export const ProjectTypePermissionSubjects: Record<
...SshPermissionSubjects(),
...CertificateManagerPermissionSubjects(),
...KmsPermissionSubjects(),
...SecretsManagerPermissionSubjects()
...SecretsManagerPermissionSubjects(),
[ProjectPermissionSub.AppConnections]: true
}
};
@@ -16,6 +16,7 @@ import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { AddPoliciesButton } from "./AddPoliciesButton";
import { AppConnectionPermissionConditions } from "./AppConnectionPermissionConditions";
import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions";
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
@@ -77,6 +78,10 @@ export const renderConditionalComponents = (
return <SecretEventPermissionConditions isDisabled={isDisabled} />;
}
if (subject === ProjectPermissionSub.AppConnections) {
return <AppConnectionPermissionConditions isDisabled={isDisabled} />;
}
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
}
@@ -1,10 +1,11 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { CreateSecretSyncModal } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Button, Spinner } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub, useWorkspace } from "@app/context";
@@ -16,8 +17,9 @@ import { SecretSyncsTable } from "./SecretSyncTable";
export const SecretSyncsTab = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addSync"] as const);
const [initialSyncFormData, setInitialSyncFormData] = useState<Partial<TSecretSyncForm>>();
const { addSync, ...search } = useSearch({
const { addSync, connectionId, connectionName, ...search } = useSearch({
from: ROUTE_PATHS.SecretManager.IntegrationsListPage.id
});
@@ -38,6 +40,38 @@ export const SecretSyncsTab = () => {
});
}, [addSync]);
useEffect(() => {
if (connectionId && connectionName) {
const storedFormData = localStorage.getItem("secretSyncFormData");
if (!storedFormData) return;
let form: Partial<TSecretSyncForm> = {};
try {
form = JSON.parse(storedFormData) as TSecretSyncForm;
} catch {
return;
} finally {
localStorage.removeItem("secretSyncFormData");
}
handlePopUpOpen("addSync", form.destination);
setInitialSyncFormData({
...form,
connection: { id: connectionId, name: connectionName }
});
navigate({
to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path,
params: {
projectId: currentWorkspace.id
},
search
});
}
}, [connectionId, connectionName]);
const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs(
currentWorkspace.id,
{
@@ -100,7 +134,11 @@ export const SecretSyncsTab = () => {
<CreateSecretSyncModal
selectSync={popUp.addSync.data}
isOpen={popUp.addSync.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addSync", isOpen)}
initialFormData={initialSyncFormData}
onOpenChange={(isOpen) => {
if (!isOpen) setInitialSyncFormData(undefined);
handlePopUpToggle("addSync", isOpen);
}}
/>
</>
);
@@ -17,7 +17,9 @@ import { IntegrationsListPage } from "./IntegrationsListPage";
const IntegrationsListPageQuerySchema = z.object({
selectedTab: z.nativeEnum(IntegrationsListPageTabs).optional(),
addSync: z.nativeEnum(SecretSync).optional()
addSync: z.nativeEnum(SecretSync).optional(),
connectionId: z.string().optional(),
connectionName: z.string().optional()
});
export const Route = createFileRoute(
@@ -6,7 +6,9 @@ import { OverviewPage } from "./OverviewPage";
const SecretOverviewPageQuerySchema = z.object({
search: z.string().catch(""),
secretPath: z.string().catch("/")
secretPath: z.string().catch("/"),
connectionId: z.string().optional(),
connectionName: z.string().optional()
});
export const Route = createFileRoute(
@@ -10,7 +10,9 @@ import { SecretDashboardPage } from "./SecretDashboardPage";
const SecretDashboardPageQueryParamsSchema = z.object({
secretPath: z.string().catch("/"),
search: z.string().catch(""),
tags: z.string().catch("")
tags: z.string().catch(""),
connectionId: z.string().optional(),
connectionName: z.string().optional()
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug"
@@ -1,11 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { SecretScanningDataSourcesPage } from "./SecretScanningDataSourcesPage";
const SecretScanningDataSourcesPageQueryParamsSchema = z.object({
connectionId: z.string().optional(),
connectionName: z.string().optional()
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/"
)({
component: SecretScanningDataSourcesPage,
validateSearch: zodValidator(SecretScanningDataSourcesPageQueryParamsSchema),
beforeLoad: ({ context }) => {
return {
breadcrumbs: [