misc: redesigned org security settings page

This commit is contained in:
Sheen Capadngan
2024-07-06 00:25:27 +08:00
parent d2759ea378
commit d4a2f4590b
9 changed files with 770 additions and 455 deletions

View File

@@ -4,8 +4,17 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent, TextArea } from "@app/components/v2"; import {
Button,
DeleteActionModal,
FormControl,
Input,
Modal,
ModalContent,
TextArea
} from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { useToggle } from "@app/hooks";
import { import {
useCreateLDAPConfig, useCreateLDAPConfig,
useGetLDAPConfig, useGetLDAPConfig,
@@ -32,9 +41,10 @@ type Props = {
popUp: UsePopUpState<["addLDAP"]>; popUp: UsePopUpState<["addLDAP"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["addLDAP"]>) => void; handlePopUpClose: (popUpName: keyof UsePopUpState<["addLDAP"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addLDAP"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["addLDAP"]>, state?: boolean) => void;
hideDelete?: boolean;
}; };
export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => { export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelete }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateLDAPConfig(); const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateLDAPConfig();
@@ -46,6 +56,39 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
resolver: zodResolver(LDAPFormSchema) resolver: zodResolver(LDAPFormSchema)
}); });
const [isDeletePopupOpen, setIsDeletePopupOpen] = useToggle();
const handleLdapSoftDelete = async () => {
if (!currentOrg) {
return;
}
try {
await updateMutateAsync({
organizationId: currentOrg.id,
isActive: false,
url: "",
bindDN: "",
bindPass: "",
searchBase: "",
searchFilter: "",
uniqueUserAttribute: "",
groupSearchBase: "",
groupSearchFilter: "",
caCert: ""
});
createNotification({
text: "Successfully deleted OIDC configuration.",
type: "success"
});
} catch (err) {
createNotification({
text: "Failed deleting OIDC configuration.",
type: "error"
});
}
};
const watchUrl = watch("url"); const watchUrl = watch("url");
const watchBindDN = watch("bindDN"); const watchBindDN = watch("bindDN");
const watchBindPass = watch("bindPass"); const watchBindPass = watch("bindPass");
@@ -175,138 +218,154 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
}; };
return ( return (
<Modal <>
isOpen={popUp?.addLDAP?.isOpen} <Modal
onOpenChange={(isOpen) => { isOpen={popUp?.addLDAP?.isOpen}
handlePopUpToggle("addLDAP", isOpen); onOpenChange={(isOpen) => {
reset(); handlePopUpToggle("addLDAP", isOpen);
}} reset();
> }}
<ModalContent title="Manage LDAP configuration"> >
<form onSubmit={handleSubmit(onSSOModalSubmit)}> <ModalContent title="Manage LDAP configuration">
<Controller <form onSubmit={handleSubmit(onSSOModalSubmit)}>
control={control} <Controller
name="url" control={control}
render={({ field, fieldState: { error } }) => ( name="url"
<FormControl label="URL" errorText={error?.message} isError={Boolean(error)}> render={({ field, fieldState: { error } }) => (
<Input {...field} placeholder="ldaps://ldap.myorg.com:636" /> <FormControl label="URL" errorText={error?.message} isError={Boolean(error)}>
</FormControl> <Input {...field} placeholder="ldaps://ldap.myorg.com:636" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="bindDN" control={control}
render={({ field, fieldState: { error } }) => ( name="bindDN"
<FormControl label="Bind DN" errorText={error?.message} isError={Boolean(error)}> render={({ field, fieldState: { error } }) => (
<Input {...field} placeholder="cn=infisical,ou=Users,dc=example,dc=com" /> <FormControl label="Bind DN" errorText={error?.message} isError={Boolean(error)}>
</FormControl> <Input {...field} placeholder="cn=infisical,ou=Users,dc=example,dc=com" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="bindPass" control={control}
render={({ field, fieldState: { error } }) => ( name="bindPass"
<FormControl label="Bind Pass" errorText={error?.message} isError={Boolean(error)}> render={({ field, fieldState: { error } }) => (
<Input {...field} type="password" placeholder="********" /> <FormControl label="Bind Pass" errorText={error?.message} isError={Boolean(error)}>
</FormControl> <Input {...field} type="password" placeholder="********" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="searchBase" control={control}
render={({ field, fieldState: { error } }) => ( name="searchBase"
<FormControl render={({ field, fieldState: { error } }) => (
label="User Search Base / User DN" <FormControl
errorText={error?.message} label="User Search Base / User DN"
isError={Boolean(error)} errorText={error?.message}
> isError={Boolean(error)}
<Input {...field} placeholder="ou=people,dc=acme,dc=com" /> >
</FormControl> <Input {...field} placeholder="ou=people,dc=acme,dc=com" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="uniqueUserAttribute" control={control}
render={({ field, fieldState: { error } }) => ( name="uniqueUserAttribute"
<FormControl render={({ field, fieldState: { error } }) => (
label="Unique User Attribute (Optional)" <FormControl
errorText={error?.message} label="Unique User Attribute (Optional)"
isError={Boolean(error)} errorText={error?.message}
> isError={Boolean(error)}
<Input {...field} placeholder="uidNumber" /> >
</FormControl> <Input {...field} placeholder="uidNumber" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="searchFilter" control={control}
render={({ field, fieldState: { error } }) => ( name="searchFilter"
<FormControl render={({ field, fieldState: { error } }) => (
label="User Search Filter (Optional)" <FormControl
errorText={error?.message} label="User Search Filter (Optional)"
isError={Boolean(error)} errorText={error?.message}
> isError={Boolean(error)}
<Input {...field} placeholder="(uid={{username}})" /> >
</FormControl> <Input {...field} placeholder="(uid={{username}})" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="groupSearchBase" control={control}
render={({ field, fieldState: { error } }) => ( name="groupSearchBase"
<FormControl render={({ field, fieldState: { error } }) => (
label="Group Search Base / Group DN (Optional)" <FormControl
errorText={error?.message} label="Group Search Base / Group DN (Optional)"
isError={Boolean(error)} errorText={error?.message}
> isError={Boolean(error)}
<Input {...field} placeholder="ou=groups,dc=acme,dc=com" /> >
</FormControl> <Input {...field} placeholder="ou=groups,dc=acme,dc=com" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="groupSearchFilter" control={control}
render={({ field, fieldState: { error } }) => ( name="groupSearchFilter"
<FormControl render={({ field, fieldState: { error } }) => (
label="Group Filter (Optional)" <FormControl
errorText={error?.message} label="Group Filter (Optional)"
isError={Boolean(error)} errorText={error?.message}
> isError={Boolean(error)}
<Input >
{...field} <Input
placeholder="(&(objectClass=posixGroup)(memberUid={{.Username}}))" {...field}
/> placeholder="(&(objectClass=posixGroup)(memberUid={{.Username}}))"
</FormControl> />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="caCert" control={control}
render={({ field, fieldState: { error } }) => ( name="caCert"
<FormControl render={({ field, fieldState: { error } }) => (
label="CA Certificate" <FormControl
errorText={error?.message} label="CA Certificate"
isError={Boolean(error)} errorText={error?.message}
> isError={Boolean(error)}
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." /> >
</FormControl> <TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
)} </FormControl>
/> )}
<div className="mt-8 flex items-center"> />
<Button <div className="mt-8 flex justify-between">
className="mr-4" <div className="flex items-center">
size="sm" <Button
type="submit" className="mr-4"
isLoading={createIsLoading || updateIsLoading} size="sm"
> type="submit"
{!data ? "Add" : "Update"} isLoading={createIsLoading || updateIsLoading}
</Button> >
<Button colorSchema="secondary" onClick={handleTestLDAPConnection}> {!data ? "Add" : "Update"}
Test Connection </Button>
</Button> <Button colorSchema="secondary" onClick={handleTestLDAPConnection}>
</div> Test Connection
</form> </Button>
</ModalContent> </div>
</Modal> {!hideDelete && (
<Button colorSchema="danger" onClick={() => setIsDeletePopupOpen.on()}>
Delete
</Button>
)}
</div>
</form>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={isDeletePopupOpen}
title="Are you sure want to delete LDAP?"
onChange={() => setIsDeletePopupOpen.toggle()}
deleteKey="confirm"
onDeleteApproved={handleLdapSoftDelete}
/>
</>
); );
}; };

View File

@@ -6,6 +6,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
DeleteActionModal,
FormControl, FormControl,
Input, Input,
Modal, Modal,
@@ -28,6 +29,7 @@ type Props = {
popUp: UsePopUpState<["addOIDC"]>; popUp: UsePopUpState<["addOIDC"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["addOIDC"]>) => void; handlePopUpClose: (popUpName: keyof UsePopUpState<["addOIDC"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addOIDC"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["addOIDC"]>, state?: boolean) => void;
hideDelete?: boolean;
}; };
const schema = z const schema = z
@@ -94,11 +96,13 @@ const schema = z
export type OIDCFormData = z.infer<typeof schema>; export type OIDCFormData = z.infer<typeof schema>;
export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => { export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelete }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateOIDCConfig(); const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateOIDCConfig();
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateOIDCConfig(); const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateOIDCConfig();
const [isDeletePopupOpen, setIsDeletePopupOpen] = useToggle(false);
const { data } = useGetOIDCConfig(currentOrg?.slug ?? ""); const { data } = useGetOIDCConfig(currentOrg?.slug ?? "");
const { control, handleSubmit, reset, setValue, watch } = useForm<OIDCFormData>({ const { control, handleSubmit, reset, setValue, watch } = useForm<OIDCFormData>({
@@ -112,6 +116,36 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
const [isClientSecretFocused, setIsClientSecretFocused] = useToggle(); const [isClientSecretFocused, setIsClientSecretFocused] = useToggle();
const configurationTypeValue = watch("configurationType"); const configurationTypeValue = watch("configurationType");
const handleOidcSoftDelete = async () => {
if (!currentOrg) {
return;
}
try {
await updateMutateAsync({
issuer: "",
discoveryURL: "",
authorizationEndpoint: "",
allowedEmailDomains: "",
jwksUri: "",
tokenEndpoint: "",
userinfoEndpoint: "",
clientId: "",
clientSecret: "",
isActive: false,
orgSlug: currentOrg.slug
});
createNotification({
text: "Successfully deleted OIDC configuration.",
type: "success"
});
} catch (err) {
createNotification({
text: "Failed deleting OIDC configuration.",
type: "error"
});
}
};
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -193,212 +227,232 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
}; };
return ( return (
<Modal <>
isOpen={popUp?.addOIDC?.isOpen} <Modal
onOpenChange={(isOpen) => { isOpen={popUp?.addOIDC?.isOpen}
handlePopUpToggle("addOIDC", isOpen); onOpenChange={(isOpen) => {
reset(); handlePopUpToggle("addOIDC", isOpen);
}} reset();
> }}
<ModalContent title="Manage OIDC configuration"> >
<form onSubmit={handleSubmit(onOIDCModalSubmit)}> <ModalContent title="Manage OIDC configuration">
<Controller <form onSubmit={handleSubmit(onOIDCModalSubmit)}>
control={control}
name="configurationType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Configuration Type"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
className="w-full"
defaultValue="discoveryURL"
{...field}
onValueChange={(e) => onChange(e)}
>
<SelectItem value={ConfigurationType.DISCOVERY_URL}>Discovery URL</SelectItem>
<SelectItem value={ConfigurationType.CUSTOM}>Custom</SelectItem>
</Select>
</FormControl>
)}
/>
{configurationTypeValue === ConfigurationType.DISCOVERY_URL && (
<Controller <Controller
control={control} control={control}
name="discoveryURL" name="configurationType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Configuration Type"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
className="w-full"
defaultValue="discoveryURL"
{...field}
onValueChange={(e) => onChange(e)}
>
<SelectItem value={ConfigurationType.DISCOVERY_URL}>Discovery URL</SelectItem>
<SelectItem value={ConfigurationType.CUSTOM}>Custom</SelectItem>
</Select>
</FormControl>
)}
/>
{configurationTypeValue === ConfigurationType.DISCOVERY_URL && (
<Controller
control={control}
name="discoveryURL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Discovery Document URL"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://accounts.google.com/.well-known/openid-configuration"
autoComplete="off"
/>
</FormControl>
)}
/>
)}
{configurationTypeValue === ConfigurationType.CUSTOM && (
<>
<Controller
control={control}
name="issuer"
render={({ field, fieldState: { error } }) => (
<FormControl label="Issuer" errorText={error?.message} isError={Boolean(error)}>
<Input
{...field}
placeholder="https://accounts.google.com"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="authorizationEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Authorization Endpoint"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://accounts.google.com/o/oauth2/v2/auth"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="tokenEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Token Endpoint"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://oauth2.googleapis.com/token"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="userinfoEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label="User info endpoint"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://openidconnect.googleapis.com/v1/userinfo"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="jwksUri"
render={({ field, fieldState: { error } }) => (
<FormControl
label="JWKS URI"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://www.googleapis.com/oauth2/v3/certs"
autoComplete="off"
/>
</FormControl>
)}
/>
</>
)}
<Controller
control={control}
name="allowedEmailDomains"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Discovery Document URL" label="Allowed Email Domains (defaults to any)"
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="infisical.com, google.com" autoComplete="off" />
</FormControl>
)}
/>
<Controller
control={control}
name="clientId"
render={({ field, fieldState: { error } }) => (
<FormControl label="Client ID" errorText={error?.message} isError={Boolean(error)}>
<Input
placeholder="Client ID"
type={isClientIdFocused ? "text" : "password"}
onFocus={() => setIsClientIdFocused.on()}
{...field}
onBlur={() => {
field.onBlur();
setIsClientIdFocused.off();
}}
autoComplete="off"
className="bg-mineshaft-800"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
> >
<Input <Input
{...field} {...field}
placeholder="https://accounts.google.com/.well-known/openid-configuration" placeholder="Client Secret"
type={isClientSecretFocused ? "text" : "password"}
autoComplete="off" autoComplete="off"
onFocus={() => setIsClientSecretFocused.on()}
onBlur={() => {
field.onBlur();
setIsClientSecretFocused.off();
}}
className="bg-mineshaft-800"
/> />
</FormControl> </FormControl>
)} )}
/> />
)} <div className="mt-8 flex justify-between">
{configurationTypeValue === ConfigurationType.CUSTOM && ( <div className="flex items-center">
<> <Button
<Controller className="mr-4"
control={control} size="sm"
name="issuer" type="submit"
render={({ field, fieldState: { error } }) => ( isLoading={createIsLoading || updateIsLoading}
<FormControl label="Issuer" errorText={error?.message} isError={Boolean(error)}> >
<Input {!data ? "Add" : "Update"}
{...field} </Button>
placeholder="https://accounts.google.com" <Button
autoComplete="off" colorSchema="secondary"
/> variant="plain"
</FormControl> onClick={() => handlePopUpClose("addOIDC")}
)} >
/> Cancel
<Controller </Button>
control={control} </div>
name="authorizationEndpoint" {!hideDelete && (
render={({ field, fieldState: { error } }) => ( <Button colorSchema="danger" onClick={() => setIsDeletePopupOpen.on()}>
<FormControl Delete
label="Authorization Endpoint" </Button>
errorText={error?.message} )}
isError={Boolean(error)} </div>
> </form>
<Input </ModalContent>
{...field} </Modal>
placeholder="https://accounts.google.com/o/oauth2/v2/auth" <DeleteActionModal
autoComplete="off" isOpen={isDeletePopupOpen}
/> title="Are you sure want to delete OIDC?"
</FormControl> onChange={() => setIsDeletePopupOpen.toggle()}
)} deleteKey="confirm"
/> onDeleteApproved={handleOidcSoftDelete}
<Controller />
control={control} </>
name="tokenEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Token Endpoint"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://oauth2.googleapis.com/token"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="userinfoEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label="User info endpoint"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://openidconnect.googleapis.com/v1/userinfo"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="jwksUri"
render={({ field, fieldState: { error } }) => (
<FormControl label="JWKS URI" errorText={error?.message} isError={Boolean(error)}>
<Input
{...field}
placeholder="https://www.googleapis.com/oauth2/v3/certs"
autoComplete="off"
/>
</FormControl>
)}
/>
</>
)}
<Controller
control={control}
name="allowedEmailDomains"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Email Domains (defaults to any)"
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="infisical.com, google.com" autoComplete="off" />
</FormControl>
)}
/>
<Controller
control={control}
name="clientId"
render={({ field, fieldState: { error } }) => (
<FormControl label="Client ID" errorText={error?.message} isError={Boolean(error)}>
<Input
placeholder="Client ID"
type={isClientIdFocused ? "text" : "password"}
onFocus={() => setIsClientIdFocused.on()}
{...field}
onBlur={() => {
field.onBlur();
setIsClientIdFocused.off();
}}
autoComplete="off"
className="bg-mineshaft-800"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="Client Secret"
type={isClientSecretFocused ? "text" : "password"}
autoComplete="off"
onFocus={() => setIsClientSecretFocused.on()}
onBlur={() => {
field.onBlur();
setIsClientSecretFocused.off();
}}
className="bg-mineshaft-800"
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={createIsLoading || updateIsLoading}
>
{!data ? "Add" : "Update"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addOIDC")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
); );
}; };

View File

@@ -1,34 +1,189 @@
import { OrgPermissionActions, OrgPermissionSubjects, useServerConfig } from "@app/context"; import { twMerge } from "tailwind-merge";
import { Button, ContentLoader, UpgradePlanModal } from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useServerConfig,
useSubscription
} from "@app/context";
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useGetLDAPConfig, useGetOIDCConfig, useGetSSOConfig } from "@app/hooks/api";
import { LoginMethod } from "@app/hooks/api/admin/types"; import { LoginMethod } from "@app/hooks/api/admin/types";
import { LDAPModal } from "./LDAPModal";
import { OIDCModal } from "./OIDCModal";
import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection"; import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection";
import { OrgLDAPSection } from "./OrgLDAPSection"; import { OrgLDAPSection } from "./OrgLDAPSection";
import { OrgOIDCSection } from "./OrgOIDCSection"; import { OrgOIDCSection } from "./OrgOIDCSection";
import { OrgScimSection } from "./OrgSCIMSection"; import { OrgScimSection } from "./OrgSCIMSection";
import { OrgSSOSection } from "./OrgSSOSection"; import { OrgSSOSection } from "./OrgSSOSection";
import { SSOModal } from "./SSOModal";
export const OrgAuthTab = withPermission( export const OrgAuthTab = withPermission(
() => { () => {
const { const {
config: { enabledLoginMethods } config: { enabledLoginMethods }
} = useServerConfig(); } = useServerConfig();
const { currentOrg } = useOrganization();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addLDAP",
"addSSO",
"addOIDC",
"upgradePlan"
] as const);
const { subscription } = useSubscription();
const { data: oidcConfig, isLoading: isLoadingOidcConfig } = useGetOIDCConfig(
currentOrg?.slug ?? ""
);
const { data: samlConfig, isLoading: isLoadingSamlConfig } = useGetSSOConfig(
currentOrg?.id ?? ""
);
const { data: ldapConfig, isLoading: isLdapConfig } = useGetLDAPConfig(currentOrg?.id ?? "");
const areConfigsLoading = isLoadingOidcConfig || isLoadingSamlConfig || isLdapConfig;
const shouldDisplaySection = (method: LoginMethod) => const shouldDisplaySection = (method: LoginMethod) =>
!enabledLoginMethods || enabledLoginMethods.includes(method); !enabledLoginMethods || enabledLoginMethods.includes(method);
const isOidcConfigured = oidcConfig && (oidcConfig.discoveryURL || oidcConfig.issuer);
const isSamlConfigured = samlConfig && samlConfig.entryPoint;
const isLdapConfigured = ldapConfig && ldapConfig.url;
const shouldShowCreateIdentityProviderView =
!isOidcConfigured && !isSamlConfigured && !isLdapConfigured;
const createIdentityProviderView = (shouldDisplaySection(LoginMethod.SAML) ||
shouldDisplaySection(LoginMethod.OIDC) ||
shouldDisplaySection(LoginMethod.LDAP)) && (
<>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<p className="text-xl font-semibold text-gray-200">Connect an Identity Provider</p>
<p className="mb-2 mt-1 text-gray-400">
Connect your identity provider to simplify user management
</p>
{shouldDisplaySection(LoginMethod.SAML) && (
<div
className={twMerge(
"mt-4 flex items-center justify-between",
(shouldDisplaySection(LoginMethod.OIDC) ||
shouldDisplaySection(LoginMethod.LDAP)) &&
"border-b border-mineshaft-500 pb-4"
)}
>
<p className="text-lg text-gray-200">SAML</p>
<Button
colorSchema="secondary"
onClick={() => {
if (!subscription?.samlSSO) {
handlePopUpOpen("upgradePlan", { feature: "SAML SSO", plan: "Pro" });
return;
}
handlePopUpOpen("addSSO");
}}
>
Connect
</Button>
</div>
)}
{shouldDisplaySection(LoginMethod.OIDC) && (
<div
className={twMerge(
"mt-4 flex items-center justify-between",
shouldDisplaySection(LoginMethod.LDAP) && "border-b border-mineshaft-500 pb-4"
)}
>
<p className="text-lg text-gray-200">OIDC</p>
<Button
colorSchema="secondary"
onClick={() => {
if (!subscription?.oidcSSO) {
handlePopUpOpen("upgradePlan", { feature: "OIDC SSO", plan: "Pro" });
return;
}
handlePopUpOpen("addOIDC");
}}
>
Connect
</Button>
</div>
)}
{shouldDisplaySection(LoginMethod.LDAP) && (
<div className="mt-4 flex items-center justify-between">
<p className="text-lg text-gray-200">LDAP</p>
<Button
colorSchema="secondary"
onClick={() => {
if (!subscription?.ldap) {
handlePopUpOpen("upgradePlan", { feature: "LDAP", plan: "Enterprise" });
return;
}
handlePopUpOpen("addLDAP");
}}
>
Connect
</Button>
</div>
)}
</div>
<SSOModal
hideDelete
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<OIDCModal
hideDelete
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<LDAPModal
hideDelete
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
</>
);
if (areConfigsLoading) {
return <ContentLoader />;
}
return ( return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6"> <>
{shouldDisplaySection(LoginMethod.SAML) && ( {shouldShowCreateIdentityProviderView ? (
createIdentityProviderView
) : (
<> <>
<OrgGeneralAuthSection /> {isSamlConfigured && shouldDisplaySection(LoginMethod.SAML) && (
<OrgSSOSection /> <div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgGeneralAuthSection />
<OrgSSOSection />
</div>
)}
{isOidcConfigured && shouldDisplaySection(LoginMethod.OIDC) && <OrgOIDCSection />}
{isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && <OrgLDAPSection />}
</> </>
)} )}
{shouldDisplaySection(LoginMethod.OIDC) && <OrgOIDCSection />}
{shouldDisplaySection(LoginMethod.LDAP) && <OrgLDAPSection />}
<OrgScimSection /> <OrgScimSection />
</div> <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={`You can use ${
(popUp.upgradePlan.data as { feature: string })?.feature
} if you switch to Infisical's ${
(popUp.upgradePlan.data as { plan: string })?.plan
} plan.`}
/>
</>
); );
}, },
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Sso } { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Sso }

View File

@@ -87,7 +87,6 @@ export const OrgGeneralAuthSection = () => {
Enforce members to authenticate via SAML to access this organization Enforce members to authenticate via SAML to access this organization
</p> </p>
</div> </div>
<hr className="border-mineshaft-600" />
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}

View File

@@ -94,7 +94,7 @@ export const OrgLDAPSection = (): JSX.Element => {
}; };
return ( return (
<> <div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div className="py-4"> <div className="py-4">
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">LDAP</h2> <h2 className="text-md text-mineshaft-100">LDAP</h2>
@@ -151,7 +151,6 @@ export const OrgLDAPSection = (): JSX.Element => {
</p> </p>
</div> </div>
)} )}
<hr className="border-mineshaft-600" />
<LDAPModal <LDAPModal
popUp={popUp} popUp={popUp}
handlePopUpClose={handlePopUpClose} handlePopUpClose={handlePopUpClose}
@@ -167,6 +166,6 @@ export const OrgLDAPSection = (): JSX.Element => {
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use LDAP authentication if you switch to Infisical's Enterprise plan." text="You can use LDAP authentication if you switch to Infisical's Enterprise plan."
/> />
</> </div>
); );
}; };

View File

@@ -60,7 +60,7 @@ export const OrgOIDCSection = (): JSX.Element => {
}; };
return ( return (
<> <div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div className="py-4"> <div className="py-4">
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">OIDC</h2> <h2 className="text-md text-mineshaft-100">OIDC</h2>
@@ -102,7 +102,6 @@ export const OrgOIDCSection = (): JSX.Element => {
</p> </p>
</div> </div>
)} )}
<hr className="border-mineshaft-600" />
<OIDCModal <OIDCModal
popUp={popUp} popUp={popUp}
handlePopUpClose={handlePopUpClose} handlePopUpClose={handlePopUpClose}
@@ -113,6 +112,6 @@ export const OrgOIDCSection = (): JSX.Element => {
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use OIDC SSO if you switch to Infisical's Pro plan." text="You can use OIDC SSO if you switch to Infisical's Pro plan."
/> />
</> </div>
); );
}; };

View File

@@ -57,7 +57,8 @@ export const OrgScimSection = () => {
}; };
return ( return (
<> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<p className="text-xl font-semibold text-gray-200">Provision users via SCIM</p>
<div className="py-4"> <div className="py-4">
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">SCIM</h2> <h2 className="text-md text-mineshaft-100">SCIM</h2>
@@ -68,7 +69,7 @@ export const OrgScimSection = () => {
colorSchema="secondary" colorSchema="secondary"
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
Manage Configure
</Button> </Button>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
@@ -109,6 +110,6 @@ export const OrgScimSection = () => {
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use SCIM Provisioning if you switch to Infisical's Enterprise plan." text="You can use SCIM Provisioning if you switch to Infisical's Enterprise plan."
/> />
</> </div>
); );
}; };

View File

@@ -115,7 +115,6 @@ export const OrgSSOSection = (): JSX.Element => {
Allow members to authenticate into Infisical with SAML Allow members to authenticate into Infisical with SAML
</p> </p>
</div> </div>
<hr className="border-mineshaft-600" />
<SSOModal <SSOModal
popUp={popUp} popUp={popUp}
handlePopUpClose={handlePopUpClose} handlePopUpClose={handlePopUpClose}

View File

@@ -6,6 +6,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
DeleteActionModal,
FormControl, FormControl,
Input, Input,
Modal, Modal,
@@ -15,6 +16,7 @@ import {
TextArea TextArea
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { useToggle } from "@app/hooks";
import { useCreateSSOConfig, useGetSSOConfig, useUpdateSSOConfig } from "@app/hooks/api"; import { useCreateSSOConfig, useGetSSOConfig, useUpdateSSOConfig } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -49,13 +51,15 @@ type Props = {
popUp: UsePopUpState<["addSSO"]>; popUp: UsePopUpState<["addSSO"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["addSSO"]>) => void; handlePopUpClose: (popUpName: keyof UsePopUpState<["addSSO"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addSSO"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["addSSO"]>, state?: boolean) => void;
hideDelete?: boolean;
}; };
export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => { export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelete }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateSSOConfig(); const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateSSOConfig();
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateSSOConfig(); const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateSSOConfig();
const [isDeletePopupOpen, setIsDeletePopupOpen] = useToggle();
const { data } = useGetSSOConfig(currentOrg?.id ?? ""); const { data } = useGetSSOConfig(currentOrg?.id ?? "");
const { control, handleSubmit, reset, watch } = useForm<AddSSOFormData>({ const { control, handleSubmit, reset, watch } = useForm<AddSSOFormData>({
@@ -76,6 +80,31 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
} }
}, [data]); }, [data]);
const handleSamlSoftDelete = async () => {
if (!currentOrg) {
return;
}
try {
await updateMutateAsync({
organizationId: currentOrg.id,
isActive: false,
entryPoint: "",
issuer: "",
cert: ""
});
createNotification({
text: "Successfully deleted SAML SSO configuration.",
type: "success"
});
} catch (err) {
createNotification({
text: "Failed deleting SAML SSO configuration.",
type: "error"
});
}
};
const onSSOModalSubmit = async ({ authProvider, entryPoint, issuer, cert }: AddSSOFormData) => { const onSSOModalSubmit = async ({ authProvider, entryPoint, issuer, cert }: AddSSOFormData) => {
try { try {
if (!currentOrg) return; if (!currentOrg) return;
@@ -177,112 +206,133 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
const authProvider = watch("authProvider"); const authProvider = watch("authProvider");
return ( return (
<Modal <>
isOpen={popUp?.addSSO?.isOpen} <Modal
onOpenChange={(isOpen) => { isOpen={popUp?.addSSO?.isOpen}
handlePopUpToggle("addSSO", isOpen); onOpenChange={(isOpen) => {
reset(); handlePopUpToggle("addSSO", isOpen);
}} reset();
> }}
<ModalContent title="Manage SAML configuration"> >
<form onSubmit={handleSubmit(onSSOModalSubmit)}> <ModalContent title="Manage SAML configuration">
<Controller <form onSubmit={handleSubmit(onSSOModalSubmit)}>
control={control} <Controller
name="authProvider" control={control}
defaultValue="okta-saml" name="authProvider"
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( defaultValue="okta-saml"
<FormControl label="Type" errorText={error?.message} isError={Boolean(error)}> render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<Select <FormControl label="Type" errorText={error?.message} isError={Boolean(error)}>
defaultValue={field.value} <Select
{...field} defaultValue={field.value}
onValueChange={(e) => onChange(e)} {...field}
className="w-full" onValueChange={(e) => onChange(e)}
> className="w-full"
{ssoAuthProviders.map(({ label, value }) => ( >
<SelectItem value={String(value || "")} key={label}> {ssoAuthProviders.map(({ label, value }) => (
{label} <SelectItem value={String(value || "")} key={label}>
</SelectItem> {label}
))} </SelectItem>
</Select> ))}
</FormControl> </Select>
</FormControl>
)}
/>
{authProvider && data && (
<>
<div className="mb-4">
<h3 className="text-sm text-mineshaft-400">
{renderLabels(authProvider).acsUrl}
</h3>
<p className="text-md break-all text-gray-400">{`${window.origin}/api/v1/sso/saml2/${data.id}`}</p>
</div>
<div className="mb-4">
<h3 className="text-sm text-mineshaft-400">
{renderLabels(authProvider).entityId}
</h3>
<p className="text-md text-gray-400">{window.origin}</p>
</div>
<Controller
control={control}
name="entryPoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label={renderLabels(authProvider).entryPoint}
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder={renderLabels(authProvider).entryPointPlaceholder}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="issuer"
render={({ field, fieldState: { error } }) => (
<FormControl
label={renderLabels(authProvider).issuer}
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder={renderLabels(authProvider).issuerPlaceholder}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="cert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
</>
)} )}
/>
{authProvider && data && (
<>
<div className="mb-4">
<h3 className="text-sm text-mineshaft-400">{renderLabels(authProvider).acsUrl}</h3>
<p className="text-md break-all text-gray-400">{`${window.origin}/api/v1/sso/saml2/${data.id}`}</p>
</div>
<div className="mb-4">
<h3 className="text-sm text-mineshaft-400">
{renderLabels(authProvider).entityId}
</h3>
<p className="text-md text-gray-400">{window.origin}</p>
</div>
<Controller
control={control}
name="entryPoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label={renderLabels(authProvider).entryPoint}
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder={renderLabels(authProvider).entryPointPlaceholder}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="issuer"
render={({ field, fieldState: { error } }) => (
<FormControl
label={renderLabels(authProvider).issuer}
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder={renderLabels(authProvider).issuerPlaceholder} />
</FormControl>
)}
/>
<Controller
control={control}
name="cert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
</>
)}
<div className="mt-8 flex items-center"> <div className="mt-8 flex justify-between">
<Button <div className="flex items-center">
className="mr-4" <Button
size="sm" className="mr-4"
type="submit" size="sm"
isLoading={createIsLoading || updateIsLoading} type="submit"
> isLoading={createIsLoading || updateIsLoading}
{!data ? "Add" : "Update"} >
</Button> {!data ? "Add" : "Update"}
<Button </Button>
colorSchema="secondary" <Button
variant="plain" colorSchema="secondary"
onClick={() => handlePopUpClose("addSSO")} variant="plain"
> onClick={() => handlePopUpClose("addSSO")}
Cancel >
</Button> Cancel
</div> </Button>
</form> </div>
</ModalContent> {!hideDelete && (
</Modal> <Button colorSchema="danger" onClick={() => setIsDeletePopupOpen.on()}>
Delete
</Button>
)}
</div>
</form>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={isDeletePopupOpen}
title="Are you sure want to delete SAML SSO?"
onChange={() => setIsDeletePopupOpen.toggle()}
deleteKey="confirm"
onDeleteApproved={handleSamlSoftDelete}
/>
</>
); );
}; };