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

View File

@@ -0,0 +1,45 @@
import { components, OptionProps } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { faBuilding, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, Tooltip } from "@app/components/v2";
import { TAvailableAppConnection } from "@app/hooks/api/appConnections";
export const AppConnectionOption = ({
isSelected,
children,
...props
}: OptionProps<TAvailableAppConnection>) => {
const isCreateOption = props.data.id === "_create";
return (
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
{isCreateOption ? (
<div className="flex items-center gap-x-1 text-mineshaft-400">
<FontAwesomeIcon icon={faPlus} size="sm" />
<span className="mr-auto">Create New Connection</span>
</div>
) : (
<>
<p className="truncate">{children}</p>
{!props.data.projectId && (
<Tooltip content="This connection belongs to your organization.">
<div className="ml-2 mr-auto">
<Badge className="flex h-5 w-min items-center gap-1 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300 hover:text-bunker-300">
<FontAwesomeIcon icon={faBuilding} size="sm" />
Organization
</Badge>
</div>
</Tooltip>
)}
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</>
)}
</div>
</components.Option>
);
};

View File

@@ -0,0 +1 @@
export * from "./AppConnectionOption";

View File

@@ -1,8 +1,10 @@
import { FunctionComponent, ReactNode } from "react";
import { BoundCanProps, Can } from "@casl/react";
import { AbilityTuple, MongoAbility } from "@casl/ability";
import { Can } from "@casl/react";
import { TooltipProps } from "@app/components/v2/Tooltip/Tooltip";
import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext";
import { useOrgPermission } from "@app/context/OrgPermissionContext";
import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types";
import { AccessRestrictedBanner, Tooltip } from "../v2";
@@ -14,7 +16,7 @@ export const OrgPermissionGuardBanner = () => {
);
};
type Props = {
type Props<T extends AbilityTuple> = {
label?: ReactNode;
// this prop is used when there exist already a tooltip as helper text for users
// so when permission is allowed same tooltip will be reused to show helpertext
@@ -22,9 +24,18 @@ type Props = {
allowedLabel?: string;
renderGuardBanner?: boolean;
tooltipProps?: Omit<TooltipProps, "children">;
} & BoundCanProps<TOrgPermission>;
I: T[0];
ability?: MongoAbility<T>;
children: ReactNode | ((isAllowed: boolean, ability: T) => ReactNode);
passThrough?: boolean;
} & (
| { an: T[1] }
| {
a: T[1];
}
);
export const OrgPermissionCan: FunctionComponent<Props> = ({
export const OrgPermissionCan: FunctionComponent<Props<OrgPermissionSet>> = ({
label = "Access restricted",
children,
passThrough = true,
@@ -41,9 +52,7 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
{(isAllowed, ability) => {
// akhilmhdh: This is set as type due to error in casl react type.
const finalChild =
typeof children === "function"
? children(isAllowed, ability as TOrgPermission)
: children;
typeof children === "function" ? children(isAllowed, ability as any) : children;
if (!isAllowed && passThrough) {
return (

View File

@@ -0,0 +1,17 @@
import { OrgPermissionCan } from "./OrgPermissionCan";
import { ProjectPermissionCan } from "./ProjectPermissionCan";
interface PermissionCanProps {
type: "project" | "org";
I: any;
a: any;
children: (isAllowed: boolean, ability?: any) => React.ReactNode;
}
export const VariablePermissionCan = ({ type, children, ...props }: PermissionCanProps) => {
if (type === "project") {
return <ProjectPermissionCan {...props}>{children}</ProjectPermissionCan>;
}
return <OrgPermissionCan {...props}>{children}</OrgPermissionCan>;
};

View File

@@ -3,3 +3,4 @@ export { GlobPermissionInfo } from "./GlobPermissionInfo";
export { OrgPermissionCan } from "./OrgPermissionCan";
export { PermissionDeniedBanner } from "./PermissionDeniedBanner";
export { ProjectPermissionCan } from "./ProjectPermissionCan";
export * from "./VariablePermissionCan";

View File

@@ -1,8 +1,10 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { SecretRotationV2Form } from "@app/components/secret-rotations-v2/forms";
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
import { SecretRotationV2ModalHeader } from "@app/components/secret-rotations-v2/SecretRotationV2ModalHeader";
import { SecretRotationV2Select } from "@app/components/secret-rotations-v2/SecretRotationV2Select";
import { Modal, ModalContent } from "@app/components/v2";
@@ -24,14 +26,21 @@ type ContentProps = {
onComplete: (secretRotation: TSecretRotationV2) => void;
selectedRotation: SecretRotation | null;
setSelectedRotation: (selectedRotation: SecretRotation | null) => void;
initialFormData?: Partial<TSecretRotationV2Form>;
} & SharedProps;
const Content = ({ setSelectedRotation, selectedRotation, ...props }: ContentProps) => {
const Content = ({
setSelectedRotation,
selectedRotation,
initialFormData,
...props
}: ContentProps) => {
if (selectedRotation) {
return (
<SecretRotationV2Form
onCancel={() => setSelectedRotation(null)}
type={selectedRotation}
initialFormData={initialFormData}
{...props}
/>
);
@@ -42,12 +51,56 @@ const Content = ({ setSelectedRotation, selectedRotation, ...props }: ContentPro
export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }: Props) => {
const [selectedRotation, setSelectedRotation] = useState<SecretRotation | null>(null);
const [initialFormData, setInitialFormData] = useState<Partial<TSecretRotationV2Form>>();
const {
location: {
search: { connectionId, connectionName, ...search },
pathname
}
} = useRouterState();
const navigate = useNavigate();
useEffect(() => {
if (connectionId && connectionName) {
const storedFormData = localStorage.getItem("secretRotationFormData");
if (!storedFormData) return;
let form: Partial<TSecretRotationV2Form> = {};
try {
form = JSON.parse(storedFormData) as TSecretRotationV2Form;
} catch {
return;
} finally {
localStorage.removeItem("secretRotationFormData");
}
onOpenChange(true);
setSelectedRotation(form.type ?? null);
setInitialFormData({
...form,
connection: { id: connectionId, name: connectionName }
});
navigate({
to: pathname,
search
});
}
}, [connectionId, connectionName]);
return (
<Modal
isOpen={isOpen}
onOpenChange={(open) => {
if (!open) setSelectedRotation(null);
if (!open) {
setSelectedRotation(null);
setInitialFormData(undefined);
}
onOpenChange(open);
}}
>
@@ -87,6 +140,7 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
setSelectedRotation(null);
onOpenChange(false);
}}
initialFormData={initialFormData}
selectedRotation={selectedRotation}
setSelectedRotation={setSelectedRotation}
{...props}

View File

@@ -1,14 +1,17 @@
import { Controller, useFormContext } from "react-hook-form";
import { SingleValue } from "react-select";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { AppConnectionOption } from "@app/components/app-connections";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_ROTATION_CONNECTION_MAP } from "@app/helpers/secretRotationsV2";
import { usePopUp } from "@app/hooks";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
import { TSecretRotationV2Form } from "./schemas";
@@ -18,19 +21,26 @@ type Props = {
};
export const SecretRotationV2ConnectionField = ({ onChange: callback, isUpdate }: Props) => {
const { permission } = useOrgPermission();
const { control, watch } = useFormContext<TSecretRotationV2Form>();
const { permission } = useProjectPermission();
const { control, watch, setValue } = useFormContext<TSecretRotationV2Form>();
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const);
const rotationType = watch("type");
const app = SECRET_ROTATION_CONNECTION_MAP[rotationType];
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
const { currentWorkspace } = useWorkspace();
const { data: availableConnections, isPending } = useListAvailableAppConnections(
app,
currentWorkspace.id
);
const connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can(
OrgPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections
ProjectPermissionAppConnectionActions.Create,
ProjectPermissionSub.AppConnections
);
const appName = APP_CONNECTION_MAP[app].name;
@@ -66,37 +76,56 @@ export const SecretRotationV2ConnectionField = ({ onChange: callback, isUpdate }
<FilterableSelect
value={value}
onChange={(newValue) => {
if ((newValue as SingleValue<{ id: string; name: string }>)?.id === "_create") {
handlePopUpOpen("addConnection");
onChange(null);
// store for oauth callback connections
localStorage.setItem("secretRotationFormData", JSON.stringify(watch()));
if (callback) callback();
return;
}
onChange(newValue);
if (callback) callback();
}}
isLoading={isPending}
options={availableConnections}
options={[
...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []),
...(availableConnections ?? [])
]}
isDisabled={isUpdate}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
components={{ Option: AppConnectionOption }}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{!isUpdate && availableConnections?.length === 0 && (
{!isUpdate && !isPending && !availableConnections?.length && !canCreateConnection && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {appName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
page.
</>
) : (
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
)}
You do not have access to any ${appName} Connections. Contact an admin to create one.
</p>
)}
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => {
// remove form storage, not oauth connection
localStorage.removeItem("secretRotationFormData");
handlePopUpToggle("addConnection", isOpen);
}}
projectType={currentWorkspace.type}
projectId={currentWorkspace.id}
app={app}
onComplete={(connection) => {
if (connection) {
setValue("connection", connection);
}
}}
/>
</>
);
};

View File

@@ -34,6 +34,7 @@ type Props = {
environment?: string;
environments?: WorkspaceEnv[];
secretRotation?: TSecretRotationV2;
initialFormData?: Partial<TSecretRotationV2Form>;
};
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretRotationV2Form)[] }[] = [
@@ -64,7 +65,8 @@ export const SecretRotationV2Form = ({
environment: envSlug,
secretPath,
secretRotation,
environments
environments,
initialFormData
}: Props) => {
const createSecretRotation = useCreateSecretRotationV2();
const updateSecretRotation = useUpdateSecretRotationV2();
@@ -93,7 +95,8 @@ export const SecretRotationV2Form = ({
},
environment: currentWorkspace?.environments.find((env) => env.slug === envSlug),
secretPath,
...(rotationOption!.template as object) // can't infer type since we don't know which specific type it is
...((rotationOption?.template as object) ?? {}), // can't infer type since we don't know which specific type it is
...(initialFormData as object)
},
reValidateMode: "onChange"
});

View File

@@ -1,7 +1,9 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { TSecretScanningDataSourceForm } from "@app/components/secret-scanning/forms/schemas";
import { Modal, ModalContent } from "@app/components/v2";
import {
SecretScanningDataSource,
@@ -21,6 +23,7 @@ type ContentProps = {
onComplete: (dataSource: TSecretScanningDataSource) => void;
selectedDataSource: SecretScanningDataSource | null;
setSelectedDataSource: (selectedDataSource: SecretScanningDataSource | null) => void;
initialFormData?: Partial<TSecretScanningDataSourceForm>;
};
const Content = ({ setSelectedDataSource, selectedDataSource, ...props }: ContentProps) => {
@@ -41,6 +44,47 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
const [selectedDataSource, setSelectedDataSource] = useState<SecretScanningDataSource | null>(
null
);
const [initialFormData, setInitialFormData] = useState<Partial<TSecretScanningDataSourceForm>>();
const {
location: {
search: { connectionId, connectionName, ...search },
pathname
}
} = useRouterState();
const navigate = useNavigate();
useEffect(() => {
if (connectionId && connectionName) {
const storedFormData = localStorage.getItem("secretScanningDataSourceFormData");
if (!storedFormData) return;
let form: Partial<TSecretScanningDataSourceForm> = {};
try {
form = JSON.parse(storedFormData) as TSecretScanningDataSourceForm;
} catch {
return;
} finally {
localStorage.removeItem("secretScanningDataSourceFormData");
}
onOpenChange(true);
setSelectedDataSource(form.type ?? null);
setInitialFormData({
...form,
connection: { id: connectionId, name: connectionName }
});
navigate({
to: pathname,
search
});
}
}, [connectionId, connectionName]);
return (
<Modal
@@ -88,6 +132,7 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
}}
selectedDataSource={selectedDataSource}
setSelectedDataSource={setSelectedDataSource}
initialFormData={initialFormData}
{...props}
/>
</ModalContent>

View File

@@ -1,14 +1,17 @@
import { Controller, useFormContext } from "react-hook-form";
import { SingleValue } from "react-select";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { AppConnectionOption } from "@app/components/app-connections";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/helpers/secretScanningV2";
import { usePopUp } from "@app/hooks";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
import { TSecretScanningDataSourceForm } from "./schemas";
@@ -21,19 +24,26 @@ export const SecretScanningDataSourceConnectionField = ({
onChange: callback,
isUpdate
}: Props) => {
const { permission } = useOrgPermission();
const { control, watch } = useFormContext<TSecretScanningDataSourceForm>();
const { permission } = useProjectPermission();
const { control, watch, setValue } = useFormContext<TSecretScanningDataSourceForm>();
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const);
const dataSourceType = watch("type");
const app = SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSourceType];
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
const { currentWorkspace } = useWorkspace();
const { data: availableConnections, isPending } = useListAvailableAppConnections(
app,
currentWorkspace.id
);
const connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can(
OrgPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections
ProjectPermissionAppConnectionActions.Create,
ProjectPermissionSub.AppConnections
);
return (
@@ -67,37 +77,57 @@ export const SecretScanningDataSourceConnectionField = ({
<FilterableSelect
value={value}
onChange={(newValue) => {
if ((newValue as SingleValue<{ id: string; name: string }>)?.id === "_create") {
handlePopUpOpen("addConnection");
onChange(null);
// store for oauth callback connections
localStorage.setItem("secretScanningDataSourceFormData", JSON.stringify(watch()));
if (callback) callback();
return;
}
onChange(newValue);
if (callback) callback();
}}
isLoading={isPending}
options={availableConnections}
options={[
...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []),
...(availableConnections ?? [])
]}
isDisabled={isUpdate}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
components={{ Option: AppConnectionOption }}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{!isUpdate && availableConnections?.length === 0 && (
{!isUpdate && !isPending && !availableConnections?.length && !canCreateConnection && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {connectionName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
page.
</>
) : (
`You do not have access to any ${connectionName} Connections. Contact an admin to create one.`
)}
You do not have access to any ${connectionName} Connections. Contact an admin to create
one.
</p>
)}
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => {
// remove form storage, not oauth connection
localStorage.removeItem("secretScanningDataSourceFormData");
handlePopUpToggle("addConnection", isOpen);
}}
projectType={currentWorkspace.type}
projectId={currentWorkspace.id}
app={app}
onComplete={(connection) => {
if (connection) {
setValue("connection", connection);
}
}}
/>
</>
);
};

View File

@@ -27,6 +27,7 @@ type Props = {
type: SecretScanningDataSource;
onCancel: () => void;
dataSource?: TSecretScanningDataSource;
initialFormData?: Partial<TSecretScanningDataSourceForm>;
};
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretScanningDataSourceForm)[] }[] =
@@ -36,7 +37,13 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TSecretScanningData
{ name: "Review", key: "review", fields: [] }
];
export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataSource }: Props) => {
export const SecretScanningDataSourceForm = ({
type,
onComplete,
onCancel,
dataSource,
initialFormData
}: Props) => {
const createDataSource = useCreateSecretScanningDataSource();
const updateDataSource = useUpdateSecretScanningDataSource();
const { currentWorkspace } = useWorkspace();
@@ -48,7 +55,8 @@ export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataS
resolver: zodResolver(SecretScanningDataSourceSchema),
defaultValues: dataSource ?? {
type,
isAutoScanEnabled: true // scott: this may need to be derived from type in the future
isAutoScanEnabled: true, // scott: this may need to be derived from type in the future
...(initialFormData as object)
},
reValidateMode: "onChange"
});

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Modal, ModalContent } from "@app/components/v2";
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
@@ -11,18 +12,21 @@ type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
selectSync?: SecretSync | null;
initialFormData?: Partial<TSecretSyncForm>;
};
type ContentProps = {
onComplete: (secretSync: TSecretSync) => void;
selectedSync: SecretSync | null;
setSelectedSync: (selectedSync: SecretSync | null) => void;
initialFormData?: Partial<TSecretSyncForm>;
};
const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) => {
const Content = ({ onComplete, setSelectedSync, selectedSync, initialFormData }: ContentProps) => {
if (selectedSync) {
return (
<CreateSecretSyncForm
initialFormData={initialFormData}
onComplete={onComplete}
onCancel={() => setSelectedSync(null)}
destination={selectedSync}
@@ -33,7 +37,12 @@ const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) =>
return <SecretSyncSelect onSelect={setSelectedSync} />;
};
export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...props }: Props) => {
export const CreateSecretSyncModal = ({
onOpenChange,
selectSync = null,
initialFormData,
...props
}: Props) => {
const [selectedSync, setSelectedSync] = useState<SecretSync | null>(selectSync);
useEffect(() => {
@@ -67,6 +76,7 @@ export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...prop
}}
selectedSync={selectedSync}
setSelectedSync={setSelectedSync}
initialFormData={initialFormData}
/>
</ModalContent>
</Modal>

View File

@@ -29,6 +29,7 @@ type Props = {
onComplete: (secretSync: TSecretSync) => void;
destination: SecretSync;
onCancel: () => void;
initialFormData?: Partial<TSecretSyncForm>;
};
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [
@@ -39,14 +40,20 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[]
{ name: "Review", key: "review", fields: [] }
];
export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Props) => {
export const CreateSecretSyncForm = ({
destination,
onComplete,
onCancel,
initialFormData
}: Props) => {
const createSecretSync = useCreateSecretSync();
const { currentWorkspace } = useWorkspace();
const { name: destinationName } = SECRET_SYNC_MAP[destination];
const [showConfirmation, setShowConfirmation] = useState(false);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
// scoot: right now we only do this when creating a connection so we know index 1
const [selectedTabIndex, setSelectedTabIndex] = useState(initialFormData ? 1 : 0);
const { syncOption } = useSecretSyncOption(destination);
@@ -59,7 +66,8 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
initialSyncBehavior: syncOption?.canImportSecrets
? undefined
: SecretSyncInitialSyncBehavior.OverwriteDestination
}
},
...initialFormData
} as Partial<TSecretSyncForm>,
reValidateMode: "onChange"
});

View File

@@ -1,14 +1,17 @@
import { Controller, useFormContext } from "react-hook-form";
import { SingleValue } from "react-select";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { AppConnectionOption } from "@app/components/app-connections";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_SYNC_CONNECTION_MAP } from "@app/helpers/secretSyncs";
import { usePopUp } from "@app/hooks";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
import { TSecretSyncForm } from "./schemas";
@@ -17,19 +20,26 @@ type Props = {
};
export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
const { permission } = useOrgPermission();
const { control, watch } = useFormContext<TSecretSyncForm>();
const { permission } = useProjectPermission();
const { control, watch, setValue } = useFormContext<TSecretSyncForm>();
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const);
const destination = watch("destination");
const app = SECRET_SYNC_CONNECTION_MAP[destination];
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
const { currentWorkspace } = useWorkspace();
const { data: availableConnections, isPending } = useListAvailableAppConnections(
app,
currentWorkspace.id
);
const connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can(
OrgPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections
ProjectPermissionAppConnectionActions.Create,
ProjectPermissionSub.AppConnections
);
const appName = APP_CONNECTION_MAP[SECRET_SYNC_CONNECTION_MAP[destination]].name;
@@ -51,36 +61,55 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
<FilterableSelect
value={value}
onChange={(newValue) => {
if ((newValue as SingleValue<{ id: string; name: string }>)?.id === "_create") {
handlePopUpOpen("addConnection");
onChange(null);
// store for oauth callback connections
localStorage.setItem("secretSyncFormData", JSON.stringify(watch()));
if (callback) callback();
return;
}
onChange(newValue);
if (callback) callback();
}}
isLoading={isPending}
options={availableConnections}
options={[
...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []),
...(availableConnections ?? [])
]}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
components={{ Option: AppConnectionOption }}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{availableConnections?.length === 0 && (
{!isPending && !availableConnections?.length && !canCreateConnection && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {appName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
page.
</>
) : (
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
)}
You do not have access to any ${appName} Connections. Contact an admin to create one.
</p>
)}
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => {
// remove form storage, not oauth connection
localStorage.removeItem("secretSyncFormData");
handlePopUpToggle("addConnection", isOpen);
}}
projectType={currentWorkspace.type}
projectId={currentWorkspace.id}
app={app}
onComplete={(connection) => {
if (connection) {
setValue("connection", connection);
}
}}
/>
</>
);
};

View File

@@ -1,4 +1,4 @@
import { MongoAbility } from "@casl/ability";
import { ForcedSubject, MongoAbility } from "@casl/ability";
export enum OrgPermissionActions {
Read = "read",
@@ -126,7 +126,6 @@ export type OrgPermissionSet =
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [
@@ -134,14 +133,13 @@ export type OrgPermissionSet =
OrgPermissionSubjects.MachineIdentityAuthTemplate
]
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway]
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
// TODO(scott): add back once org UI refactored
// | [
// OrgPermissionAppConnectionActions,
// (
// | OrgPermissionSubjects.AppConnections
// | (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
// )
// ];
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]
| [
OrgPermissionAppConnectionActions,
(
| OrgPermissionSubjects.AppConnections
| (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
)
];
export type TOrgPermission = MongoAbility<OrgPermissionSet>;

View File

@@ -154,6 +154,14 @@ export enum ProjectPermissionAuditLogsActions {
Read = "read"
}
export enum ProjectPermissionAppConnectionActions {
Read = "read-app-connections",
Create = "create-app-connections",
Edit = "edit-app-connections",
Delete = "delete-app-connections",
Connect = "connect-app-connections"
}
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
@@ -173,6 +181,10 @@ export type IdentityManagementSubjectFields = {
identityId: string;
};
export type AppConnectionSubjectFields = {
connectionId: string;
};
export type ConditionalProjectPermissionSubject =
| ProjectPermissionSub.SecretSyncs
| ProjectPermissionSub.Secrets
@@ -184,7 +196,8 @@ export type ConditionalProjectPermissionSubject =
| ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports
| ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.SecretEvents;
| ProjectPermissionSub.SecretEvents
| ProjectPermissionSub.AppConnections;
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
[PermissionConditionOperators.$EQ]: "equal to",
@@ -263,7 +276,8 @@ export enum ProjectPermissionSub {
SecretScanningDataSources = "secret-scanning-data-sources",
SecretScanningFindings = "secret-scanning-findings",
SecretScanningConfigs = "secret-scanning-configs",
SecretEvents = "secret-events"
SecretEvents = "secret-events",
AppConnections = "app-connections"
}
export type SecretSubjectFields = {
@@ -431,6 +445,13 @@ export type ProjectPermissionSet =
| ProjectPermissionSub.SecretEvents
| (ForcedSubject<ProjectPermissionSub.SecretEvents> & SecretEventSubjectFields)
)
]
| [
ProjectPermissionAppConnectionActions,
(
| ProjectPermissionSub.AppConnections
| (ForcedSubject<ProjectPermissionSub.AppConnections> & AppConnectionSubjectFields)
)
];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;

View File

@@ -8,6 +8,7 @@ import {
faServer,
faUser
} from "@fortawesome/free-solid-svg-icons";
import { useRouterState } from "@tanstack/react-router";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
@@ -221,3 +222,11 @@ export const AWS_REGIONS = [
{ name: "AWS GovCloud (US-East)", slug: "us-gov-east-1" },
{ name: "AWS GovCloud (US-West)", slug: "us-gov-west-1" }
];
export const useGetAppConnectionOauthReturnUrl = () => {
const {
location: { pathname }
} = useRouterState();
return pathname;
};

View File

@@ -6,6 +6,7 @@ import {
TAppConnectionResponse,
TCreateAppConnectionDTO,
TDeleteAppConnectionDTO,
TMigrateAppConnectionDTO,
TUpdateAppConnectionDTO
} from "@app/hooks/api/appConnections/types";
@@ -20,7 +21,10 @@ export const useCreateAppConnection = () => {
return data.appConnection;
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() })
onSuccess: ({ projectId, app }) => {
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: appConnectionKeys.listAvailable(app, projectId) });
}
});
};
@@ -35,9 +39,10 @@ export const useUpdateAppConnection = () => {
return data.appConnection;
},
onSuccess: (_, { connectionId, app }) => {
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() });
queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) });
onSuccess: ({ projectId, app }) => {
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: appConnectionKeys.listAvailable(app, projectId) });
// queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) });
}
});
};
@@ -50,9 +55,28 @@ export const useDeleteAppConnection = () => {
return data;
},
onSuccess: (_, { connectionId, app }) => {
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() });
queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) });
onSuccess: ({ projectId, app }) => {
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: appConnectionKeys.listAvailable(app, projectId) });
// queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) });
}
});
};
export const useMigrateAppConnection = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ connectionId, app }: TMigrateAppConnectionDTO) => {
const { data } = await apiRequest.post(
`/api/v1/app-connections/${app}/${connectionId}/migrate`
);
return data;
},
onSuccess: ({ projectId, app }) => {
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: appConnectionKeys.listAvailable(app, projectId) });
// queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) });
}
});
};

View File

@@ -5,29 +5,36 @@ import { apiRequest } from "@app/config/request";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
TAppConnection,
TAppConnectionMap,
TAppConnectionOptions,
TAvailableAppConnection,
TAvailableAppConnectionsResponse,
TGetAppConnection,
TListAppConnections
} from "@app/hooks/api/appConnections/types";
import {
TAppConnectionOption,
TAppConnectionOptionMap
} from "@app/hooks/api/appConnections/types/app-options";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const appConnectionKeys = {
all: ["app-connection"] as const,
options: () => [...appConnectionKeys.all, "options"] as const,
list: () => [...appConnectionKeys.all, "list"] as const,
listAvailable: (app: AppConnection) => [...appConnectionKeys.all, app, "list-available"] as const,
listByApp: (app: AppConnection) => [...appConnectionKeys.list(), app],
byId: (app: AppConnection, connectionId: string) =>
[...appConnectionKeys.all, app, "by-id", connectionId] as const
options: (projectType?: ProjectType) =>
[...appConnectionKeys.all, "options", ...(projectType ? [projectType] : [])] as const,
list: (projectId?: string | null) =>
[...appConnectionKeys.all, "list", ...(projectId ? [projectId] : [])] as const,
listAvailable: (app: AppConnection, projectId?: string | null) =>
[...appConnectionKeys.all, app, "list-available", ...(projectId ? [projectId] : [])] as const
// scott: may need these in the future but not using now
// getUsage: (app: AppConnection, connectionId: string) =>
// [...appConnectionKeys.all, "usage", app, connectionId] as const
// listByApp: (app: AppConnection) => [...appConnectionKeys.list(), app],
// scott: we will need this once we have individual app connection page
// byId: (app: AppConnection, connectionId: string) =>
// [...appConnectionKeys.all, app, "by-id", connectionId] as const
};
export const useAppConnectionOptions = (
projectType?: ProjectType,
options?: Omit<
UseQueryOptions<
TAppConnectionOption[],
@@ -39,10 +46,11 @@ export const useAppConnectionOptions = (
>
) => {
return useQuery({
queryKey: appConnectionKeys.options(),
queryKey: appConnectionKeys.options(projectType),
queryFn: async () => {
const { data } = await apiRequest.get<TAppConnectionOptions>(
"/api/v1/app-connections/options"
"/api/v1/app-connections/options",
{ params: { projectType } }
);
return data.appConnectionOptions;
@@ -64,6 +72,7 @@ export const useGetAppConnectionOption = <T extends AppConnection>(app: T) => {
};
export const useListAppConnections = (
projectId?: string,
options?: Omit<
UseQueryOptions<
TAppConnection[],
@@ -75,10 +84,12 @@ export const useListAppConnections = (
>
) => {
return useQuery({
queryKey: appConnectionKeys.list(),
queryKey: appConnectionKeys.list(projectId),
queryFn: async () => {
const { data } =
await apiRequest.get<TListAppConnections<TAppConnection>>("/api/v1/app-connections");
const { data } = await apiRequest.get<TListAppConnections<TAppConnection>>(
"/api/v1/app-connections",
{ params: { projectId } }
);
return data.appConnections;
},
@@ -88,6 +99,7 @@ export const useListAppConnections = (
export const useListAvailableAppConnections = (
app: AppConnection,
projectId: string,
options?: Omit<
UseQueryOptions<
TAvailableAppConnection[],
@@ -99,10 +111,11 @@ export const useListAvailableAppConnections = (
>
) => {
return useQuery({
queryKey: appConnectionKeys.listAvailable(app),
queryKey: appConnectionKeys.listAvailable(app, projectId),
queryFn: async () => {
const { data } = await apiRequest.get<TAvailableAppConnectionsResponse>(
`/api/v1/app-connections/${app}/available`
`/api/v1/app-connections/${app}/available`,
{ params: { projectId } }
);
return data.appConnections;
@@ -111,53 +124,82 @@ export const useListAvailableAppConnections = (
});
};
export const useListAppConnectionsByApp = <T extends AppConnection>(
app: T,
options?: Omit<
UseQueryOptions<
TAppConnectionMap[T][],
unknown,
TAppConnectionMap[T][],
ReturnType<typeof appConnectionKeys.listByApp>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: appConnectionKeys.listByApp(app),
queryFn: async () => {
const { data } = await apiRequest.get<TListAppConnections<TAppConnectionMap[T]>>(
`/api/v1/app-connections/${app}`
);
// scott: may need these in the future but not using now
// export const useGetAppConnectionUsageById = (
// app: AppConnection,
// connectionId: string,
// options?: Omit<
// UseQueryOptions<
// AppConnectionUsage,
// unknown,
// AppConnectionUsage,
// ReturnType<typeof appConnectionKeys.getUsage>
// >,
// "queryKey" | "queryFn"
// >
// ) => {
// return useQuery({
// queryKey: appConnectionKeys.getUsage(app, connectionId),
// queryFn: async () => {
// const { data } = await apiRequest.get<AppConnectionUsage>(
// `/api/v1/app-connections/${app}/${connectionId}/usage`
// );
//
// return data;
// },
// ...options
// });
// };
return data.appConnections;
},
...options
});
};
// scott: may need these in the future but not using now
// export const useListAppConnectionsByApp = <T extends AppConnection>(
// app: T,
// options?: Omit<
// UseQueryOptions<
// TAppConnectionMap[T][],
// unknown,
// TAppConnectionMap[T][],
// ReturnType<typeof appConnectionKeys.listByApp>
// >,
// "queryKey" | "queryFn"
// >
// ) => {
// return useQuery({
// queryKey: appConnectionKeys.listByApp(app),
// queryFn: async () => {
// const { data } = await apiRequest.get<TListAppConnections<TAppConnectionMap[T]>>(
// `/api/v1/app-connections/${app}`
// );
//
// return data.appConnections;
// },
// ...options
// });
// };
export const useGetAppConnectionById = <T extends AppConnection>(
app: T,
connectionId: string,
options?: Omit<
UseQueryOptions<
TAppConnectionMap[T],
unknown,
TAppConnectionMap[T],
ReturnType<typeof appConnectionKeys.byId>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: appConnectionKeys.byId(app, connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TGetAppConnection<TAppConnectionMap[T]>>(
`/api/v1/app-connections/${app}/${connectionId}`
);
return data.appConnection;
},
...options
});
};
// scott: we will need this once we have individual app connection page
// export const useGetAppConnectionById = <T extends AppConnection>(
// app: T,
// connectionId: string,
// options?: Omit<
// UseQueryOptions<
// TAppConnectionMap[T],
// unknown,
// TAppConnectionMap[T],
// ReturnType<typeof appConnectionKeys.byId>
// >,
// "queryKey" | "queryFn"
// >
// ) => {
// return useQuery({
// queryKey: appConnectionKeys.byId(app, connectionId),
// queryFn: async () => {
// const { data } = await apiRequest.get<TGetAppConnection<TAppConnectionMap[T]>>(
// `/api/v1/app-connections/${app}/${connectionId}`
// );
//
// return data.appConnection;
// },
// ...options
// });
// };

View File

@@ -1,3 +1,5 @@
import { ProjectType } from "@app/hooks/api/workspace/types";
import { AppConnection } from "../enums";
import { TOnePassConnection } from "./1password-connection";
import { TAppConnectionOption } from "./app-options";
@@ -116,10 +118,11 @@ export type TAppConnection =
| TNetlifyConnection
| TOktaConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;
export type TListAppConnections<T extends TAppConnection> = { appConnections: T[] };
export type TGetAppConnection<T extends TAppConnection> = { appConnection: T };
// scott: we will need this once we have individual app connection page
// export type TGetAppConnection<T extends TAppConnection> = { appConnection: T };
export type TAppConnectionOptions = { appConnectionOptions: TAppConnectionOption[] };
export type TAppConnectionResponse = { appConnection: TAppConnection };
export type TAvailableAppConnectionsResponse = { appConnections: TAvailableAppConnection[] };
@@ -133,6 +136,7 @@ export type TCreateAppConnectionDTO = Pick<
| "description"
| "isPlatformManagedCredentials"
| "gatewayId"
| "projectId"
>;
export type TUpdateAppConnectionDTO = Partial<
@@ -150,43 +154,64 @@ export type TDeleteAppConnectionDTO = {
connectionId: string;
};
export type TAppConnectionMap = {
[AppConnection.AWS]: TAwsConnection;
[AppConnection.GitHub]: TGitHubConnection;
[AppConnection.GitHubRadar]: TGitHubRadarConnection;
[AppConnection.GCP]: TGcpConnection;
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnection;
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
[AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection;
[AppConnection.AzureDevOps]: TAzureDevOpsConnection;
[AppConnection.AzureADCS]: TAzureADCSConnection;
[AppConnection.Databricks]: TDatabricksConnection;
[AppConnection.Humanitec]: THumanitecConnection;
[AppConnection.TerraformCloud]: TTerraformCloudConnection;
[AppConnection.Vercel]: TVercelConnection;
[AppConnection.Postgres]: TPostgresConnection;
[AppConnection.MsSql]: TMsSqlConnection;
[AppConnection.MySql]: TMySqlConnection;
[AppConnection.OracleDB]: TOracleDBConnection;
[AppConnection.Camunda]: TCamundaConnection;
[AppConnection.Windmill]: TWindmillConnection;
[AppConnection.Auth0]: TAuth0Connection;
[AppConnection.HCVault]: THCVaultConnection;
[AppConnection.LDAP]: TLdapConnection;
[AppConnection.TeamCity]: TTeamCityConnection;
[AppConnection.OCI]: TOCIConnection;
[AppConnection.OnePass]: TOnePassConnection;
[AppConnection.Heroku]: THerokuConnection;
[AppConnection.Render]: TRenderConnection;
[AppConnection.Flyio]: TFlyioConnection;
[AppConnection.GitLab]: TGitLabConnection;
[AppConnection.Cloudflare]: TCloudflareConnection;
[AppConnection.Bitbucket]: TBitbucketConnection;
[AppConnection.Zabbix]: TZabbixConnection;
[AppConnection.Railway]: TRailwayConnection;
[AppConnection.Checkly]: TChecklyConnection;
[AppConnection.Supabase]: TSupabaseConnection;
[AppConnection.DigitalOcean]: TDigitalOceanConnection;
[AppConnection.Netlify]: TNetlifyConnection;
[AppConnection.Okta]: TOktaConnection;
// scott: we will need this once we have individual app connection page
// export type TAppConnectionMap = {
// [AppConnection.AWS]: TAwsConnection;
// [AppConnection.GitHub]: TGitHubConnection;
// [AppConnection.GitHubRadar]: TGitHubRadarConnection;
// [AppConnection.GCP]: TGcpConnection;
// [AppConnection.AzureKeyVault]: TAzureKeyVaultConnection;
// [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
// [AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection;
// [AppConnection.AzureDevOps]: TAzureDevOpsConnection;
// [AppConnection.AzureADCS]: TAzureADCSConnection;
// [AppConnection.Databricks]: TDatabricksConnection;
// [AppConnection.Humanitec]: THumanitecConnection;
// [AppConnection.TerraformCloud]: TTerraformCloudConnection;
// [AppConnection.Vercel]: TVercelConnection;
// [AppConnection.Postgres]: TPostgresConnection;
// [AppConnection.MsSql]: TMsSqlConnection;
// [AppConnection.MySql]: TMySqlConnection;
// [AppConnection.OracleDB]: TOracleDBConnection;
// [AppConnection.Camunda]: TCamundaConnection;
// [AppConnection.Windmill]: TWindmillConnection;
// [AppConnection.Auth0]: TAuth0Connection;
// [AppConnection.HCVault]: THCVaultConnection;
// [AppConnection.LDAP]: TLdapConnection;
// [AppConnection.TeamCity]: TTeamCityConnection;
// [AppConnection.OCI]: TOCIConnection;
// [AppConnection.OnePass]: TOnePassConnection;
// [AppConnection.Heroku]: THerokuConnection;
// [AppConnection.Render]: TRenderConnection;
// [AppConnection.Flyio]: TFlyioConnection;
// [AppConnection.GitLab]: TGitLabConnection;
// [AppConnection.Cloudflare]: TCloudflareConnection;
// [AppConnection.Bitbucket]: TBitbucketConnection;
// [AppConnection.Zabbix]: TZabbixConnection;
// [AppConnection.Railway]: TRailwayConnection;
// [AppConnection.Checkly]: TChecklyConnection;
// [AppConnection.Supabase]: TSupabaseConnection;
// [AppConnection.DigitalOcean]: TDigitalOceanConnection;
// [AppConnection.Netlify]: TNetlifyConnection;
// [AppConnection.Okta]: TOktaConnection;
// };
export type TMigrateAppConnectionDTO = {
app: AppConnection;
connectionId: string;
};
export type AppConnectionUsage = {
projects: Array<{
id: string;
name: string;
slug: string;
type: ProjectType;
resources: {
secretSyncs: Array<{ id: string; name: string }>;
externalCas: Array<{ id: string; name: string }>;
secretRotations: Array<{ id: string; name: string }>;
dataSources: Array<{ id: string; name: string }>;
};
}>;
};

View File

@@ -1,3 +1,5 @@
import { ProjectType } from "@app/hooks/api/workspace/types";
export type TRootAppConnection = {
id: string;
name: string;
@@ -8,4 +10,11 @@ export type TRootAppConnection = {
updatedAt: string;
isPlatformManagedCredentials?: boolean;
gatewayId?: string | null;
projectId?: string | null;
project?: {
name: string;
type: ProjectType;
slug: string;
id: string;
} | null;
};

View File

@@ -132,6 +132,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.CREATE_APP_CONNECTION]: "Create App Connection",
[EventType.UPDATE_APP_CONNECTION]: "Update App Connection",
[EventType.DELETE_APP_CONNECTION]: "Delete App Connection",
[EventType.GET_APP_CONNECTION_USAGE]: "Get App Connection Usage",
[EventType.MIGRATE_APP_CONNECTION]: "Migrate App Connection",
[EventType.GET_SECRET_SYNCS]: "List secret syncs",
[EventType.GET_SECRET_SYNC]: "Get Secret Sync",
[EventType.CREATE_SECRET_SYNC]: "Create Secret Sync",

View File

@@ -140,6 +140,8 @@ export enum EventType {
CREATE_APP_CONNECTION = "create-app-connection",
UPDATE_APP_CONNECTION = "update-app-connection",
DELETE_APP_CONNECTION = "delete-app-connection",
GET_APP_CONNECTION_USAGE = "get-app-connection-usage",
MIGRATE_APP_CONNECTION = "migrate-app-connection",
GET_SECRET_SYNCS = "get-secret-syncs",
GET_SECRET_SYNC = "get-secret-sync",
CREATE_SECRET_SYNC = "create-secret-sync",

View File

@@ -7,6 +7,7 @@ import {
faFileLines,
faHome,
faMobile,
faPlug,
faSitemap,
faStamp,
faUsers
@@ -130,6 +131,23 @@ export const PkiManagerLayout = () => {
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/app-connections"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link

View File

@@ -6,6 +6,7 @@ import {
faCog,
faHome,
faMobile,
faPlug,
faPuzzlePiece,
faUsers,
faVault
@@ -159,6 +160,23 @@ export const SecretManagerLayout = () => {
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/app-connections"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link

View File

@@ -4,6 +4,7 @@ import {
faDatabase,
faHome,
faMagnifyingGlass,
faPlug,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -100,6 +101,23 @@ export const SecretScanningLayout = () => {
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/app-connections"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link

View File

@@ -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>
)}

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -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`

View File

@@ -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`

View File

@@ -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(

View File

@@ -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`

View File

@@ -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 =

View File

@@ -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) {

View File

@@ -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

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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]);

View File

@@ -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;
};
};

View File

@@ -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

View File

@@ -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
}
);

View File

@@ -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"
}
]
};
}
});

View File

@@ -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"
}
]
};
}
});

View File

@@ -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"
}
]
};
}
});

View File

@@ -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" }]}
/>
);
};

View File

@@ -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>

View File

@@ -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
}
};

View File

@@ -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} />;
}

View File

@@ -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);
}}
/>
</>
);

View File

@@ -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(

View File

@@ -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(

View File

@@ -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"

View File

@@ -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: [

View File

@@ -82,12 +82,15 @@ import { Route as organizationSettingsPageOauthCallbackPageRouteImport } from '.
import { Route as projectAuditLogsPageRouteSshImport } from './pages/project/AuditLogsPage/route-ssh'
import { Route as projectAccessControlPageRouteSshImport } from './pages/project/AccessControlPage/route-ssh'
import { Route as projectAuditLogsPageRouteSecretScanningImport } from './pages/project/AuditLogsPage/route-secret-scanning'
import { Route as projectAppConnectionsPageRouteSecretScanningImport } from './pages/project/AppConnectionsPage/route-secret-scanning'
import { Route as projectAccessControlPageRouteSecretScanningImport } from './pages/project/AccessControlPage/route-secret-scanning'
import { Route as projectAuditLogsPageRouteSecretManagerImport } from './pages/project/AuditLogsPage/route-secret-manager'
import { Route as projectAppConnectionsPageRouteSecretManagerImport } from './pages/project/AppConnectionsPage/route-secret-manager'
import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager'
import { Route as projectAuditLogsPageRouteKmsImport } from './pages/project/AuditLogsPage/route-kms'
import { Route as projectAccessControlPageRouteKmsImport } from './pages/project/AccessControlPage/route-kms'
import { Route as projectAuditLogsPageRouteCertManagerImport } from './pages/project/AuditLogsPage/route-cert-manager'
import { Route as projectAppConnectionsPageRouteCertManagerImport } from './pages/project/AppConnectionsPage/route-cert-manager'
import { Route as projectAccessControlPageRouteCertManagerImport } from './pages/project/AccessControlPage/route-cert-manager'
import { Route as sshSettingsPageRouteImport } from './pages/ssh/SettingsPage/route'
import { Route as sshSshHostsPageRouteImport } from './pages/ssh/SshHostsPage/route'
@@ -920,6 +923,13 @@ const projectAuditLogsPageRouteSecretScanningRoute =
getParentRoute: () => secretScanningLayoutRoute,
} as any)
const projectAppConnectionsPageRouteSecretScanningRoute =
projectAppConnectionsPageRouteSecretScanningImport.update({
id: '/app-connections',
path: '/app-connections',
getParentRoute: () => secretScanningLayoutRoute,
} as any)
const projectAccessControlPageRouteSecretScanningRoute =
projectAccessControlPageRouteSecretScanningImport.update({
id: '/access-management',
@@ -943,6 +953,13 @@ const projectAuditLogsPageRouteSecretManagerRoute =
getParentRoute: () => secretManagerLayoutRoute,
} as any)
const projectAppConnectionsPageRouteSecretManagerRoute =
projectAppConnectionsPageRouteSecretManagerImport.update({
id: '/app-connections',
path: '/app-connections',
getParentRoute: () => secretManagerLayoutRoute,
} as any)
const projectAccessControlPageRouteSecretManagerRoute =
projectAccessControlPageRouteSecretManagerImport.update({
id: '/access-management',
@@ -989,6 +1006,13 @@ const projectAuditLogsPageRouteCertManagerRoute =
getParentRoute: () => certManagerLayoutRoute,
} as any)
const projectAppConnectionsPageRouteCertManagerRoute =
projectAppConnectionsPageRouteCertManagerImport.update({
id: '/app-connections',
path: '/app-connections',
getParentRoute: () => certManagerLayoutRoute,
} as any)
const projectAccessControlPageRouteCertManagerRoute =
projectAccessControlPageRouteCertManagerImport.update({
id: '/access-management',
@@ -2730,6 +2754,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof projectAccessControlPageRouteCertManagerImport
parentRoute: typeof certManagerLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections'
path: '/app-connections'
fullPath: '/projects/cert-management/$projectId/app-connections'
preLoaderRoute: typeof projectAppConnectionsPageRouteCertManagerImport
parentRoute: typeof certManagerLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs'
path: '/audit-logs'
@@ -2772,6 +2803,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof projectAccessControlPageRouteSecretManagerImport
parentRoute: typeof secretManagerLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections'
path: '/app-connections'
fullPath: '/projects/secret-management/$projectId/app-connections'
preLoaderRoute: typeof projectAppConnectionsPageRouteSecretManagerImport
parentRoute: typeof secretManagerLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs'
path: '/audit-logs'
@@ -2793,6 +2831,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof projectAccessControlPageRouteSecretScanningImport
parentRoute: typeof secretScanningLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections'
path: '/app-connections'
fullPath: '/projects/secret-scanning/$projectId/app-connections'
preLoaderRoute: typeof projectAppConnectionsPageRouteSecretScanningImport
parentRoute: typeof secretScanningLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs'
path: '/audit-logs'
@@ -3822,6 +3867,7 @@ interface certManagerLayoutRouteChildren {
certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute
certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute
projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute
projectAppConnectionsPageRouteCertManagerRoute: typeof projectAppConnectionsPageRouteCertManagerRoute
projectAuditLogsPageRouteCertManagerRoute: typeof projectAuditLogsPageRouteCertManagerRoute
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren
@@ -3841,6 +3887,8 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = {
certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute,
projectAccessControlPageRouteCertManagerRoute:
projectAccessControlPageRouteCertManagerRoute,
projectAppConnectionsPageRouteCertManagerRoute:
projectAppConnectionsPageRouteCertManagerRoute,
projectAuditLogsPageRouteCertManagerRoute:
projectAuditLogsPageRouteCertManagerRoute,
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRoute:
@@ -4212,6 +4260,7 @@ interface secretManagerLayoutRouteChildren {
secretManagerSecretRotationPageRouteRoute: typeof secretManagerSecretRotationPageRouteRoute
secretManagerSettingsPageRouteRoute: typeof secretManagerSettingsPageRouteRoute
projectAccessControlPageRouteSecretManagerRoute: typeof projectAccessControlPageRouteSecretManagerRoute
projectAppConnectionsPageRouteSecretManagerRoute: typeof projectAppConnectionsPageRouteSecretManagerRoute
projectAuditLogsPageRouteSecretManagerRoute: typeof projectAuditLogsPageRouteSecretManagerRoute
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren
secretManagerSecretDashboardPageRouteRoute: typeof secretManagerSecretDashboardPageRouteRoute
@@ -4233,6 +4282,8 @@ const secretManagerLayoutRouteChildren: secretManagerLayoutRouteChildren = {
secretManagerSettingsPageRouteRoute: secretManagerSettingsPageRouteRoute,
projectAccessControlPageRouteSecretManagerRoute:
projectAccessControlPageRouteSecretManagerRoute,
projectAppConnectionsPageRouteSecretManagerRoute:
projectAppConnectionsPageRouteSecretManagerRoute,
projectAuditLogsPageRouteSecretManagerRoute:
projectAuditLogsPageRouteSecretManagerRoute,
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute:
@@ -4290,6 +4341,7 @@ interface secretScanningLayoutRouteChildren {
secretScanningSecretScanningFindingsPageRouteRoute: typeof secretScanningSecretScanningFindingsPageRouteRoute
secretScanningSettingsPageRouteRoute: typeof secretScanningSettingsPageRouteRoute
projectAccessControlPageRouteSecretScanningRoute: typeof projectAccessControlPageRouteSecretScanningRoute
projectAppConnectionsPageRouteSecretScanningRoute: typeof projectAppConnectionsPageRouteSecretScanningRoute
projectAuditLogsPageRouteSecretScanningRoute: typeof projectAuditLogsPageRouteSecretScanningRoute
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren
projectGroupDetailsByIDPageRouteSecretScanningRoute: typeof projectGroupDetailsByIDPageRouteSecretScanningRoute
@@ -4304,6 +4356,8 @@ const secretScanningLayoutRouteChildren: secretScanningLayoutRouteChildren = {
secretScanningSettingsPageRouteRoute: secretScanningSettingsPageRouteRoute,
projectAccessControlPageRouteSecretScanningRoute:
projectAccessControlPageRouteSecretScanningRoute,
projectAppConnectionsPageRouteSecretScanningRoute:
projectAppConnectionsPageRouteSecretScanningRoute,
projectAuditLogsPageRouteSecretScanningRoute:
projectAuditLogsPageRouteSecretScanningRoute,
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute:
@@ -4689,15 +4743,18 @@ export interface FileRoutesByFullPath {
'/projects/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute
'/projects/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
'/projects/cert-management/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/projects/cert-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteCertManagerRoute
'/projects/cert-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute
'/projects/cert-management/$projectId/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren
'/projects/cert-management/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren
'/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
'/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
'/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren
'/projects/secret-scanning/$projectId/access-management': typeof projectAccessControlPageRouteSecretScanningRoute
'/projects/secret-scanning/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretScanningRoute
'/projects/secret-scanning/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute
'/projects/secret-scanning/$projectId/data-sources': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren
'/projects/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
@@ -4902,12 +4959,15 @@ export interface FileRoutesByTo {
'/projects/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute
'/projects/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
'/projects/cert-management/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/projects/cert-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteCertManagerRoute
'/projects/cert-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute
'/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
'/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
'/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute
'/projects/secret-scanning/$projectId/access-management': typeof projectAccessControlPageRouteSecretScanningRoute
'/projects/secret-scanning/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretScanningRoute
'/projects/secret-scanning/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute
'/projects/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
'/projects/ssh/$projectId/audit-logs': typeof projectAuditLogsPageRouteSshRoute
@@ -5128,15 +5188,18 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/overview': typeof sshSshHostsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections': typeof projectAppConnectionsPageRouteCertManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management': typeof projectAccessControlPageRouteKmsRoute
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management': typeof projectAccessControlPageRouteSecretScanningRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections': typeof projectAppConnectionsPageRouteSecretScanningRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute
@@ -5351,15 +5414,18 @@ export interface FileRouteTypes {
| '/projects/ssh/$projectId/overview'
| '/projects/ssh/$projectId/settings'
| '/projects/cert-management/$projectId/access-management'
| '/projects/cert-management/$projectId/app-connections'
| '/projects/cert-management/$projectId/audit-logs'
| '/projects/cert-management/$projectId/certificate-templates'
| '/projects/cert-management/$projectId/subscribers'
| '/projects/kms/$projectId/access-management'
| '/projects/kms/$projectId/audit-logs'
| '/projects/secret-management/$projectId/access-management'
| '/projects/secret-management/$projectId/app-connections'
| '/projects/secret-management/$projectId/audit-logs'
| '/projects/secret-management/$projectId/integrations'
| '/projects/secret-scanning/$projectId/access-management'
| '/projects/secret-scanning/$projectId/app-connections'
| '/projects/secret-scanning/$projectId/audit-logs'
| '/projects/secret-scanning/$projectId/data-sources'
| '/projects/ssh/$projectId/access-management'
@@ -5563,12 +5629,15 @@ export interface FileRouteTypes {
| '/projects/ssh/$projectId/overview'
| '/projects/ssh/$projectId/settings'
| '/projects/cert-management/$projectId/access-management'
| '/projects/cert-management/$projectId/app-connections'
| '/projects/cert-management/$projectId/audit-logs'
| '/projects/kms/$projectId/access-management'
| '/projects/kms/$projectId/audit-logs'
| '/projects/secret-management/$projectId/access-management'
| '/projects/secret-management/$projectId/app-connections'
| '/projects/secret-management/$projectId/audit-logs'
| '/projects/secret-scanning/$projectId/access-management'
| '/projects/secret-scanning/$projectId/app-connections'
| '/projects/secret-scanning/$projectId/audit-logs'
| '/projects/ssh/$projectId/access-management'
| '/projects/ssh/$projectId/audit-logs'
@@ -5787,15 +5856,18 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/overview'
| '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/settings'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources'
| '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management'
@@ -6388,6 +6460,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers",
@@ -6424,6 +6497,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secret-rotation",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/settings",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug",
@@ -6441,6 +6515,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/findings",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/settings",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId",
@@ -6547,6 +6622,10 @@ export const routeTree = rootRoute
"filePath": "project/AccessControlPage/route-cert-manager.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections": {
"filePath": "project/AppConnectionsPage/route-cert-manager.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs": {
"filePath": "project/AuditLogsPage/route-cert-manager.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout"
@@ -6578,6 +6657,10 @@ export const routeTree = rootRoute
"filePath": "project/AccessControlPage/route-secret-manager.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections": {
"filePath": "project/AppConnectionsPage/route-secret-manager.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs": {
"filePath": "project/AuditLogsPage/route-secret-manager.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout"
@@ -6670,6 +6753,10 @@ export const routeTree = rootRoute
"filePath": "project/AccessControlPage/route-secret-scanning.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/app-connections": {
"filePath": "project/AppConnectionsPage/route-secret-scanning.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs": {
"filePath": "project/AuditLogsPage/route-secret-scanning.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout"

View File

@@ -63,6 +63,7 @@ const secretManagerRoutes = route("/projects/secret-management/$projectId", [
]),
route("/audit-logs", "project/AuditLogsPage/route-secret-manager.tsx"),
route("/access-management", "project/AccessControlPage/route-secret-manager.tsx"),
route("/app-connections", "project/AppConnectionsPage/route-secret-manager.tsx"),
route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-manager.tsx"),
route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-manager.tsx"),
route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-manager.tsx"),
@@ -313,6 +314,7 @@ const certManagerRoutes = route("/projects/cert-management/$projectId", [
route("/settings", "cert-manager/SettingsPage/route.tsx"),
route("/audit-logs", "project/AuditLogsPage/route-cert-manager.tsx"),
route("/access-management", "project/AccessControlPage/route-cert-manager.tsx"),
route("/app-connections", "project/AppConnectionsPage/route-cert-manager.tsx"),
route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-cert-manager.tsx"),
route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-cert-manager.tsx"),
route("/members/$membershipId", "project/MemberDetailsByIDPage/route-cert-manager.tsx"),
@@ -361,6 +363,7 @@ const secretScanningRoutes = route("/projects/secret-scanning/$projectId", [
route("/settings", "secret-scanning/SettingsPage/route.tsx"),
route("/audit-logs", "project/AuditLogsPage/route-secret-scanning.tsx"),
route("/access-management", "project/AccessControlPage/route-secret-scanning.tsx"),
route("/app-connections", "project/AppConnectionsPage/route-secret-scanning.tsx"),
route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-scanning.tsx"),
route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-scanning.tsx"),
route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-scanning.tsx"),