IDP management UI (#2487)

Adds UI to interact with IDP Configurations (CRUD)
This commit is contained in:
Javier Adriel
2022-12-09 16:13:10 -06:00
committed by GitHub
parent bee98e1ba0
commit 2368199e03
15 changed files with 1587 additions and 0 deletions

View File

@@ -130,6 +130,15 @@ export const IAM_PAGES = {
ACCOUNT_ADD: "/access-keys/new-account",
USER_SA_ACCOUNT_ADD: "/identity/users/new-user-sa/:userName",
/* IDP */
IDP_LDAP_CONFIGURATIONS: "/idp/ldap/configurations",
IDP_LDAP_CONFIGURATIONS_VIEW: "/idp/ldap/configurations/:idpName",
IDP_LDAP_CONFIGURATIONS_ADD: "/idp/ldap/configurations/add-idp",
IDP_OPENID_CONFIGURATIONS: "/idp/openid/configurations",
IDP_OPENID_CONFIGURATIONS_VIEW: "/idp/openid/configurations/:idpName",
IDP_OPENID_CONFIGURATIONS_ADD: "/idp/openid/configurations/add-idp",
POLICIES: "/identity/policies",
POLICY_ADD: "/identity/add-policy",
POLICIES_VIEW: "/identity/policies/*",
@@ -430,6 +439,30 @@ export const IAM_PAGES_PERMISSIONS = {
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.IDP_LDAP_CONFIGURATIONS]: [
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.IDP_LDAP_CONFIGURATIONS_ADD]: [
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.IDP_LDAP_CONFIGURATIONS_VIEW]: [
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.IDP_OPENID_CONFIGURATIONS]: [
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.IDP_OPENID_CONFIGURATIONS_ADD]: [
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.IDP_OPENID_CONFIGURATIONS_VIEW]: [
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
};
export const S3_ALL_RESOURCES = "arn:aws:s3:::*";

View File

@@ -122,6 +122,24 @@ const AccountCreate = React.lazy(
const Users = React.lazy(() => import("./Users/Users"));
const Groups = React.lazy(() => import("./Groups/Groups"));
const IDPLDAPConfigurations = React.lazy(
() => import("./IDP/IDPLDAPConfigurations")
);
const IDPOpenIDConfigurations = React.lazy(
() => import("./IDP/IDPOpenIDConfigurations")
);
const AddIDPLDAPConfiguration = React.lazy(
() => import("./IDP/AddIDPLDAPConfiguration")
);
const AddIDPOpenIDConfiguration = React.lazy(
() => import("./IDP/AddIDPOpenIDConfiguration")
);
const IDPLDAPConfigurationDetails = React.lazy(
() => import("./IDP/IDPLDAPConfigurationDetails")
);
const IDPOpenIDConfigurationDetails = React.lazy(
() => import("./IDP/IDPOpenIDConfigurationDetails")
);
const TenantDetails = React.lazy(
() => import("./Tenants/TenantDetails/TenantDetails")
@@ -343,6 +361,30 @@ const Console = ({ classes }: IConsoleProps) => {
component: Policies,
path: IAM_PAGES.POLICIES,
},
{
component: IDPLDAPConfigurations,
path: IAM_PAGES.IDP_LDAP_CONFIGURATIONS,
},
{
component: IDPOpenIDConfigurations,
path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS,
},
{
component: AddIDPLDAPConfiguration,
path: IAM_PAGES.IDP_LDAP_CONFIGURATIONS_ADD,
},
{
component: AddIDPOpenIDConfiguration,
path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS_ADD,
},
{
component: IDPLDAPConfigurationDetails,
path: IAM_PAGES.IDP_LDAP_CONFIGURATIONS_VIEW,
},
{
component: IDPOpenIDConfigurationDetails,
path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS_VIEW,
},
{
component: Heal,
path: IAM_PAGES.TOOLS_HEAL,

View File

@@ -0,0 +1,219 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Box, Grid } from "@mui/material";
import {
formFieldStyles,
modalBasic,
} from "../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { Button } from "mds";
import { useNavigate } from "react-router-dom";
import { ErrorResponseHandler } from "../../../common/types";
import { useAppDispatch } from "../../../store";
import {
setErrorSnackMessage,
setServerNeedsRestart,
} from "../../../systemSlice";
import useApi from "../Common/Hooks/useApi";
import PageHeader from "../Common/PageHeader/PageHeader";
import BackLink from "../../../common/BackLink";
import PageLayout from "../Common/Layout/PageLayout";
import SectionTitle from "../Common/SectionTitle";
type AddIDPConfigurationProps = {
classes?: any;
icon: React.ReactNode;
helpBox: React.ReactNode;
header: string;
title: string;
backLink: string;
formFields: object;
endpoint: string;
};
const styles = (theme: Theme) =>
createStyles({
...formFieldStyles,
...modalBasic,
});
const AddIDPConfiguration = ({
classes,
icon,
helpBox,
header,
backLink,
title,
formFields,
endpoint,
}: AddIDPConfigurationProps) => {
const extraFormFields = {
name: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Config Name is required" : "";
},
label: "Name",
tooltip: "Name for identity provider configuration",
placeholder: "Name",
type: "text",
},
...formFields,
};
const navigate = useNavigate();
const dispatch = useAppDispatch();
const [fields, setFields] = useState<any>({});
const onSuccess = (res: any) => {
navigate(backLink);
dispatch(setServerNeedsRestart(res.restart === true));
};
const onError = (err: ErrorResponseHandler) =>
dispatch(setErrorSnackMessage(err));
const [loading, invokeApi] = useApi(onSuccess, onError);
const validSave = () => {
for (const [key, value] of Object.entries(extraFormFields)) {
if (
value.required &&
!(
fields[key] !== undefined &&
fields[key] !== null &&
fields[key] !== ""
)
) {
return false;
}
}
return true;
};
const resetForm = () => {
setFields({});
};
const addRecord = (event: React.FormEvent) => {
event.preventDefault();
const name = fields["name"];
let input = "";
for (const key of Object.keys(formFields)) {
if (fields[key]) {
input += `${key}=${fields[key]} `;
}
}
invokeApi("POST", endpoint, { name, input });
};
return (
<Grid item xs={12}>
<PageHeader label={<BackLink to={backLink} label={header} />} />
<PageLayout>
<Box
sx={{
display: "grid",
padding: "25px",
gap: "25px",
gridTemplateColumns: {
md: "2fr 1.2fr",
xs: "1fr",
},
border: "1px solid #eaeaea",
}}
>
<Box>
<SectionTitle icon={icon}>{title}</SectionTitle>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
addRecord(e);
}}
>
<Grid container item spacing="20" sx={{ marginTop: 1 }}>
<Grid xs={12} item>
{Object.entries(extraFormFields).map(([key, value]) => (
<Grid
item
xs={12}
className={classes.formFieldRow}
key={key}
>
<InputBoxWrapper
id={key}
required={value.required}
name={key}
label={value.label}
tooltip={value.tooltip}
error={value.hasError(fields[key], true)}
value={fields[key] ? fields[key] : ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFields({ ...fields, [key]: e.target.value })
}
placeholder={value.placeholder}
type={value.type}
/>
</Grid>
))}
<Grid item xs={12} textAlign={"right"}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginTop: "20px",
gap: "15px",
}}
>
<Button
id={"clear"}
type="button"
variant="regular"
onClick={resetForm}
label={"Clear"}
/>
<Button
id={"save-key"}
type="submit"
variant="callAction"
color="primary"
disabled={loading || !validSave()}
label={"Save"}
/>
</Box>
</Grid>
</Grid>
</Grid>
</form>
</Box>
{helpBox}
</Box>
</PageLayout>
</Grid>
);
};
export default withStyles(styles)(AddIDPConfiguration);

View File

@@ -0,0 +1,106 @@
import React, { Fragment } from "react";
import { Box } from "@mui/material";
import { HelpIconFilled } from "../../../icons";
interface IContent {
icon: React.ReactNode;
text: string;
iconDescription: string;
}
interface IAddIDPConfigurationHelpBoxProps {
helpText: string;
docLink: string;
docText: string;
contents: IContent[];
}
const FeatureItem = ({
icon,
description,
}: {
icon: any;
description: string;
}) => {
return (
<Box
sx={{
display: "flex",
"& .min-icon": {
marginRight: "10px",
height: "23px",
width: "23px",
marginBottom: "10px",
},
}}
>
{icon}{" "}
<div style={{ fontSize: "14px", fontStyle: "italic", color: "#5E5E5E" }}>
{description}
</div>
</Box>
);
};
const AddIDPConfigurationHelpBox = ({
helpText,
docLink,
docText,
contents,
}: IAddIDPConfigurationHelpBoxProps) => {
return (
<Box
sx={{
flex: 1,
border: "1px solid #eaeaea",
borderRadius: "2px",
display: "flex",
flexFlow: "column",
padding: "20px",
}}
>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
display: "flex",
alignItems: "center",
marginBottom: "16px",
paddingBottom: "20px",
"& .min-icon": {
height: "21px",
width: "21px",
marginRight: "15px",
},
}}
>
<HelpIconFilled />
<div>{helpText}</div>
</Box>
<Box sx={{ fontSize: "14px", marginBottom: "15px" }}>
{contents.map((content) => (
<Fragment>
{content.icon && (
<Box sx={{ paddingBottom: "20px" }}>
<FeatureItem
icon={content.icon}
description={content.iconDescription}
/>
</Box>
)}
<Box sx={{ paddingBottom: "20px" }}>{content.text}</Box>
</Fragment>
))}
<Box sx={{ paddingBottom: "20px" }}>
<a href={docLink} target="_blank" rel="noreferrer">
{docText}
</a>
</Box>
</Box>
</Box>
);
};
export default AddIDPConfigurationHelpBox;

View File

@@ -0,0 +1,86 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import LoginIcon from "@mui/icons-material/Login";
import {
formFieldStyles,
modalBasic,
} from "../Common/FormComponents/common/styleLibrary";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import AddIDPConfiguration from "./AddIDPConfiguration";
import { ldapFormFields } from "./utils";
import AddIDPConfigurationHelpBox from "./AddIDPConfigurationHelpbox";
type AddIDPLDAPConfigurationProps = {
classes?: any;
};
const styles = (theme: Theme) =>
createStyles({
...formFieldStyles,
formFieldRow: {
...formFieldStyles.formFieldRow,
},
...modalBasic,
});
const AddIDPLDAPConfiguration = ({ classes }: AddIDPLDAPConfigurationProps) => {
const helpBoxContents = [
{
text: "MinIO supports using an Active Directory or LDAP (AD/LDAP) service for external management of user identities. Configuring an external IDentity Provider (IDP) enables Single-Sign On (SSO) workflows, where applications authenticate against the external IDP before accessing MinIO.",
icon: <LoginIcon />,
iconDescription: "Create Configurations",
},
{
text: "MinIO queries the configured Active Directory / LDAP server to verify the credentials specified by the application and optionally return a list of groups in which the user has membership. MinIO supports two modes (Lookup-Bind Mode and Username-Bind Mode) for performing these queries",
icon: null,
iconDescription: "",
},
{
text: "MinIO recommends using Lookup-Bind mode as the preferred method for verifying AD/LDAP credentials. Username-Bind mode is a legacy method retained for backwards compatibility only.",
icon: null,
iconDescription: "",
},
];
return (
<AddIDPConfiguration
icon={<LoginIcon />}
helpBox={
<AddIDPConfigurationHelpBox
helpText={"Learn more about LDAP Configurations"}
contents={helpBoxContents}
docLink={
"https://min.io/docs/minio/linux/operations/external-iam.html?ref=con#minio-external-iam-ad-ldap"
}
docText={"Learn more about LDAP Configurations"}
/>
}
header={"LDAP Configurations"}
backLink={IAM_PAGES.IDP_LDAP_CONFIGURATIONS}
title={"Create LDAP Configuration"}
endpoint={"/api/v1/idp/ldap/"}
formFields={ldapFormFields}
/>
);
};
export default withStyles(styles)(AddIDPLDAPConfiguration);

View File

@@ -0,0 +1,71 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import { LockIcon } from "../../../icons";
import AddIDPConfiguration from "./AddIDPConfiguration";
import { openIDFormFields } from "./utils";
import AddIDPConfigurationHelpBox from "./AddIDPConfigurationHelpbox";
type AddIDPOpenIDConfigurationProps = {
classes?: any;
};
const styles = (theme: Theme) => createStyles({});
const AddIDPOpenIDConfiguration = ({
classes,
}: AddIDPOpenIDConfigurationProps) => {
const helpBoxContents = [
{
text: "MinIO supports using an OpenID Connect (OIDC) compatible IDentity Provider (IDP) such as Okta, KeyCloak, Dex, Google, or Facebook for external management of user identities.",
icon: <LockIcon />,
iconDescription: "Create Configurations",
},
{
text: "Configuring an external IDP enables Single-Sign On workflows, where applications authenticate against the external IDP before accessing MinIO.",
icon: null,
iconDescription: "",
},
];
return (
<AddIDPConfiguration
icon={<LockIcon />}
helpBox={
<AddIDPConfigurationHelpBox
helpText={"Learn more about OpenID Connect Configurations"}
contents={helpBoxContents}
docLink={
"https://min.io/docs/minio/linux/operations/external-iam.html?ref=con#minio-external-iam-oidc"
}
docText={"Learn more about OpenID Connect Configurations"}
/>
}
header={"OpenID Configurations"}
backLink={IAM_PAGES.IDP_OPENID_CONFIGURATIONS}
title={"Create OpenID Configuration"}
endpoint={"/api/v1/idp/openid/"}
formFields={openIDFormFields}
/>
);
};
export default withStyles(styles)(AddIDPOpenIDConfiguration);

View File

@@ -0,0 +1,86 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { DialogContentText } from "@mui/material";
import { ErrorResponseHandler } from "../../../common/types";
import useApi from "../Common/Hooks/useApi";
import ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog";
import { ConfirmDeleteIcon } from "../../../icons";
import {
setErrorSnackMessage,
setServerNeedsRestart,
} from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
interface IDeleteIDPConfigurationModalProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
idp: string;
idpType: string;
}
const DeleteIDPConfigurationModal = ({
closeDeleteModalAndRefresh,
deleteOpen,
idp,
idpType,
}: IDeleteIDPConfigurationModalProps) => {
const dispatch = useAppDispatch();
const onDelSuccess = (res: any) => {
closeDeleteModalAndRefresh(true);
dispatch(setServerNeedsRestart(res.restart === true));
};
const onDelError = (err: ErrorResponseHandler) =>
dispatch(setErrorSnackMessage(err));
const onClose = () => closeDeleteModalAndRefresh(false);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!idp) {
return null;
}
const onConfirmDelete = () => {
invokeDeleteApi("DELETE", `/api/v1/idp/${idpType}/${idp}`);
};
const displayName = idp === "_" ? "Default" : idp;
return (
<ConfirmDialog
title={`Delete ${displayName}`}
confirmText={"Delete"}
isOpen={deleteOpen}
titleIcon={<ConfirmDeleteIcon />}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmButtonProps={{
disabled: deleteLoading,
}}
confirmationContent={
<DialogContentText>
Are you sure you want to delete IDP <b>{displayName}</b>{" "}
configuration? <br />
</DialogContentText>
}
/>
);
};
export default DeleteIDPConfigurationModal;

View File

@@ -0,0 +1,364 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Box, Grid } from "@mui/material";
import {
buttonsStyles,
containerForHeader,
formFieldStyles,
hrClass,
modalBasic,
pageContentStyles,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import { RefreshIcon, TrashIcon } from "../../../icons";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { Button } from "mds";
import { useNavigate, useParams } from "react-router-dom";
import { ErrorResponseHandler } from "../../../common/types";
import { useAppDispatch } from "../../../store";
import {
setErrorSnackMessage,
setServerNeedsRestart,
} from "../../../systemSlice";
import useApi from "../Common/Hooks/useApi";
import api from "../../../common/api";
import PageLayout from "../Common/Layout/PageLayout";
import PageHeader from "../Common/PageHeader/PageHeader";
import BackLink from "../../../common/BackLink";
import ScreenTitle from "../Common/ScreenTitle/ScreenTitle";
import DeleteIDPConfigurationModal from "./DeleteIDPConfigurationModal";
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
type IDPConfigurationDetailsProps = {
classes?: any;
formFields: object;
endpoint: string;
backLink: string;
header: string;
idpType: string;
icon: React.ReactNode;
};
const styles = (theme: Theme) =>
createStyles({
...formFieldStyles,
formFieldRow: {
...formFieldStyles.formFieldRow,
},
...modalBasic,
pageContainer: {
height: "100%",
},
screenTitle: {
border: 0,
paddingTop: 0,
},
...pageContentStyles,
...searchField,
capitalize: {
textTransform: "capitalize",
},
...hrClass,
...buttonsStyles,
...containerForHeader(theme.spacing(4)),
});
const IDPConfigurationDetails = ({
classes,
formFields,
endpoint,
backLink,
header,
idpType,
icon,
}: IDPConfigurationDetailsProps) => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const params = useParams();
const configurationName = params.idpName;
const [loading, setLoading] = useState<boolean>(true);
const [isEnabled, setIsEnabled] = useState<boolean>(false);
const [fields, setFields] = useState<any>({});
const [originalFields, setOriginalFields] = useState<any>({});
const [record, setRecord] = useState<any>({});
const [editMode, setEditMode] = useState<boolean>(false);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const onSuccess = (res: any) => {
dispatch(setServerNeedsRestart(res.restart === true));
};
const onError = (err: ErrorResponseHandler) =>
dispatch(setErrorSnackMessage(err));
const [loadingSave, invokeApi] = useApi(onSuccess, onError);
const onEnabledSuccess = (res: any) => {
setIsEnabled(!isEnabled);
dispatch(setServerNeedsRestart(res.restart === true));
};
const onEnabledError = (err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
};
const [loadingEnabledSave, invokeEnabledApi] = useApi(
onEnabledSuccess,
onEnabledError
);
const toggleEditMode = () => {
if (editMode) {
parseFields(record);
}
setEditMode(!editMode);
};
const parseFields = (record: any) => {
let fields: any = {};
if (record.info) {
record.info.forEach((item: any) => {
if (item.key === "enable") {
setIsEnabled(item.value === "on");
}
fields[item.key] = item.value;
});
}
setFields(fields);
};
const parseOriginalFields = (record: any) => {
let fields: any = {};
if (record.info) {
record.info.forEach((item: any) => {
fields[item.key] = item.value;
});
}
setOriginalFields(fields);
};
useEffect(() => {
setLoading(true);
}, []);
useEffect(() => {
const loadRecord = () => {
api
.invoke("GET", `${endpoint}${configurationName}`)
.then((result: any) => {
if (result) {
setRecord(result);
parseFields(result);
parseOriginalFields(result);
}
setLoading(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoading(false);
});
};
if (loading) {
loadRecord();
}
}, [dispatch, loading, configurationName, endpoint]);
const validSave = () => {
for (const [key, value] of Object.entries(formFields)) {
if (
value.required &&
!(
fields[key] !== undefined &&
fields[key] !== null &&
fields[key] !== ""
)
) {
return false;
}
}
return true;
};
const resetForm = () => {
setFields({});
};
const saveRecord = (event: React.FormEvent) => {
event.preventDefault();
let input = "";
for (const key of Object.keys(formFields)) {
if (fields[key] || fields[key] !== originalFields[key]) {
input += `${key}=${fields[key]} `;
}
}
invokeApi("PUT", `${endpoint}${configurationName}`, { input });
setEditMode(false);
};
const closeDeleteModalAndRefresh = async (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
navigate(backLink);
}
};
const toggleConfiguration = (value: boolean) => {
const input = `enable=${value ? "on" : "off"}`;
invokeEnabledApi("PUT", `${endpoint}${configurationName}`, { input });
};
return (
<Grid item xs={12}>
{deleteOpen && configurationName && (
<DeleteIDPConfigurationModal
deleteOpen={deleteOpen}
idp={configurationName}
idpType={idpType}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<PageHeader
label={<BackLink to={backLink} label={header} />}
actions={
<FormSwitchWrapper
label={""}
indicatorLabels={["Enabled", "Disabled"]}
checked={isEnabled}
value={"is-configuration-enabled"}
id={"is-configuration-enabled"}
name={"is-configuration-enabled"}
onChange={(e) => toggleConfiguration(e.target.checked)}
description=""
disabled={loadingEnabledSave}
/>
}
/>
<PageLayout className={classes.pageContainer}>
<Grid item xs={12}>
<ScreenTitle
classes={{
screenTitle: classes.screenTitle,
}}
icon={icon}
title={configurationName === "_" ? "Default" : configurationName}
actions={
<Fragment>
{configurationName !== "_" && (
<Button
id={"delete-idp-config"}
onClick={() => {
setDeleteOpen(true);
}}
label={"Delete Configuration"}
icon={<TrashIcon />}
variant={"secondary"}
/>
)}
<Button
id={"refresh-idp-config"}
onClick={() => setLoading(true)}
label={"Refresh"}
icon={<RefreshIcon />}
/>
</Fragment>
}
/>
</Grid>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
saveRecord(e);
}}
>
<Grid container item spacing="20" sx={{ marginTop: 1 }}>
<Grid xs={12} item className={classes.fieldBox}>
{Object.entries(formFields).map(([key, value]) => (
<Grid item xs={12} className={classes.formFieldRow} key={key}>
<InputBoxWrapper
id={key}
required={value.required}
name={key}
label={value.label}
tooltip={value.tooltip}
error={value.hasError(fields[key], editMode)}
value={fields[key] ? fields[key] : ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFields({ ...fields, [key]: e.target.value })
}
placeholder={value.placeholder}
disabled={!editMode}
type={value.type}
/>
</Grid>
))}
<Grid item xs={12} textAlign={"right"}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginTop: "20px",
gap: "15px",
}}
>
<Button
id={"edit"}
type="button"
variant={editMode ? "regular" : "callAction"}
onClick={toggleEditMode}
label={editMode ? "Cancel" : "Edit"}
/>
{editMode && (
<Button
id={"clear"}
type="button"
variant="regular"
onClick={resetForm}
label={"Clear"}
/>
)}
{editMode && (
<Button
id={"save-key"}
type="submit"
variant="callAction"
color="primary"
disabled={loading || loadingSave || !validSave()}
label={"Save"}
/>
)}
</Box>
</Grid>
</Grid>
</Grid>
</form>
</PageLayout>
</Grid>
);
};
export default withStyles(styles)(IDPConfigurationDetails);

View File

@@ -0,0 +1,225 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { useAppDispatch } from "../../../store";
import { useNavigate } from "react-router-dom";
import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
} from "../../../common/SecureComponent/permissions";
import {
hasPermission,
SecureComponent,
} from "../../../common/SecureComponent";
import api from "../../../common/api";
import { ErrorResponseHandler } from "../../../common/types";
import { setErrorSnackMessage } from "../../../systemSlice";
import PageHeader from "../Common/PageHeader/PageHeader";
import PageLayout from "../Common/Layout/PageLayout";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import { Grid } from "@mui/material";
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
import { Button } from "mds";
import { AddIcon, RefreshIcon } from "../../../icons";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import DeleteIDPConfigurationModal from "./DeleteIDPConfigurationModal";
type IDPConfigurationsProps = {
classes?: any;
idpType: string;
};
const styles = (theme: Theme) =>
createStyles({
...containerForHeader(theme.spacing(4)),
});
const IDPConfigurations = ({ classes, idpType }: IDPConfigurationsProps) => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedIDP, setSelectedIDP] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [records, setRecords] = useState<[]>([]);
const deleteIDP = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
]);
const viewIDP = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
]);
const displayIDPs = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
]);
useEffect(() => {
fetchRecords();
}, []);
useEffect(() => {
if (loading) {
if (displayIDPs) {
api
.invoke("GET", `/api/v1/idp/${idpType}`)
.then((res) => {
setLoading(false);
setRecords(
res.results.map((r: any) => {
r.name = r.name === "_" ? "Default" : r.name;
r.enabled = r.enabled === true ? "Enabled" : "Disabled";
return r;
})
);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
dispatch(setErrorSnackMessage(err));
});
} else {
setLoading(false);
}
}
}, [loading, setLoading, setRecords, dispatch, displayIDPs, idpType]);
const fetchRecords = () => {
setLoading(true);
};
const confirmDeleteIDP = (idp: string) => {
setDeleteOpen(true);
idp = idp === "Default" ? "_" : idp;
setSelectedIDP(idp);
};
const viewAction = (idp: any) => {
let name = idp.name === "Default" ? "_" : idp.name;
navigate(`/idp/${idpType}/configurations/${name}`);
};
const closeDeleteModalAndRefresh = async (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
fetchRecords();
}
};
const tableActions = [
{
type: "view",
onClick: viewAction,
disableButtonFunction: () => !viewIDP,
},
{
type: "delete",
onClick: confirmDeleteIDP,
sendOnlyId: true,
disableButtonFunction: (idp: string) => !deleteIDP || idp === "Default",
},
];
return (
<Fragment>
{deleteOpen && (
<DeleteIDPConfigurationModal
deleteOpen={deleteOpen}
idp={selectedIDP}
idpType={idpType}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<PageHeader label={`${idpType.toUpperCase()} Configurations`} />
<PageLayout className={classes.pageContainer}>
<Grid container spacing={1}>
<Grid
item
xs={12}
display={"flex"}
alignItems={"center"}
justifyContent={"flex-end"}
sx={{
"& button": {
marginLeft: "8px",
},
}}
>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_CONFIG_UPDATE]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TooltipWrapper tooltip={"Refresh"}>
<Button
id={"refresh-keys"}
variant="regular"
icon={<RefreshIcon />}
onClick={() => setLoading(true)}
/>
</TooltipWrapper>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_CONFIG_UPDATE]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TooltipWrapper tooltip={`Create ${idpType} configuration`}>
<Button
id={"create-idp"}
label={"Create Configuration"}
variant={"callAction"}
icon={<AddIcon />}
onClick={() =>
navigate(`/idp/${idpType}/configurations/add-idp`)
}
/>
</TooltipWrapper>
</SecureComponent>
</Grid>
<Grid item xs={12} className={classes.tableBlock}>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_CONFIG_UPDATE]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Type", elementKey: "type" },
{ label: "Enabled", elementKey: "enabled" },
]}
isLoading={loading}
records={records}
entityName="Keys"
idField="name"
/>
</SecureComponent>
</Grid>
</Grid>
</PageLayout>
</Fragment>
);
};
export default withStyles(styles)(IDPConfigurations);

View File

@@ -0,0 +1,48 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { ldapFormFields } from "./utils";
import LoginIcon from "@mui/icons-material/Login";
import IDPConfigurationDetails from "./IDPConfigurationDetails";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
type IDPLDAPConfigurationDetailsProps = {
classes?: any;
};
const styles = (theme: Theme) => createStyles({});
const IDPLDAPConfigurationDetails = ({
classes,
}: IDPLDAPConfigurationDetailsProps) => {
return (
<IDPConfigurationDetails
backLink={IAM_PAGES.IDP_LDAP_CONFIGURATIONS}
header={"LDAP Configurations"}
endpoint={"/api/v1/idp/ldap/"}
idpType={"ldap"}
formFields={ldapFormFields}
icon={<LoginIcon width={40} />}
/>
);
};
export default withStyles(styles)(IDPLDAPConfigurationDetails);

View File

@@ -0,0 +1,34 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import IDPConfigurations from "./IDPConfigurations";
type IDPLDAPConfigurationsProps = {
classes?: any;
};
const styles = (theme: Theme) => createStyles({});
const IDPLDAPConfigurations = ({ classes }: IDPLDAPConfigurationsProps) => {
return <IDPConfigurations idpType={"ldap"} />;
};
export default withStyles(styles)(IDPLDAPConfigurations);

View File

@@ -0,0 +1,48 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import { LockIcon } from "../../../icons";
import { openIDFormFields } from "./utils";
import IDPConfigurationDetails from "./IDPConfigurationDetails";
type IDPOpenIDConfigurationDetailsProps = {
classes?: any;
};
const styles = (theme: Theme) => createStyles({});
const IDPOpenIDConfigurationDetails = ({
classes,
}: IDPOpenIDConfigurationDetailsProps) => {
return (
<IDPConfigurationDetails
backLink={IAM_PAGES.IDP_OPENID_CONFIGURATIONS}
header={"OpenID Configurations"}
endpoint={"/api/v1/idp/openid/"}
idpType={"openid"}
formFields={openIDFormFields}
icon={<LockIcon width={40} />}
/>
);
};
export default withStyles(styles)(IDPOpenIDConfigurationDetails);

View File

@@ -0,0 +1,34 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import IDPConfigurations from "./IDPConfigurations";
type IDPOpenIDConfigurationsProps = {
classes?: any;
};
const styles = (theme: Theme) => createStyles({});
const IDPOpenIDConfigurations = ({ classes }: IDPOpenIDConfigurationsProps) => {
return <IDPConfigurations idpType={"openid"} />;
};
export default withStyles(styles)(IDPOpenIDConfigurations);

View File

@@ -0,0 +1,176 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export const openIDFormFields = {
config_url: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Config URL is required" : "";
},
label: "Config URL",
tooltip: "Config URL for identity provider configuration",
placeholder:
"https://identity-provider-url/.well-known/openid-configuration",
type: "text",
},
client_id: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Client ID is required" : "";
},
label: "Client ID",
tooltip: "Identity provider Client ID",
placeholder: "Enter Client ID",
type: "text",
},
client_secret: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Client Secret is required" : "";
},
label: "Client Secret",
tooltip: "Identity provider Client Secret",
placeholder: "Enter Client Secret",
type: "password",
},
display_name: {
required: false,
label: "Display Name",
tooltip: "Display Name",
placeholder: "Enter Display Name",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
claim_name: {
required: false,
label: "Claim Name",
tooltip: "Claim from which MinIO will read the policy or role to use",
placeholder: "Enter Claim Name",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
claim_prefix: {
required: false,
label: "Claim Prefix",
tooltip: "Claim Prefix",
placeholder: "Enter Claim Prefix",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
scopes: {
required: false,
label: "Scopes",
tooltip: "Scopes",
placeholder: "openid,profile,email",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
redirect_uri: {
required: false,
label: "Redirect URI",
tooltip: "Redirect URI",
placeholder: "https://console-endpoint-url/oauth_callback",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
role_policy: {
required: false,
label: "Role Policy",
tooltip: "Role Policy",
placeholder: "readonly",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
};
export const ldapFormFields = {
server_addr: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Server Address is required" : "";
},
label: "Server Address",
tooltip: 'AD/LDAP server address e.g. "myldapserver.com:636"',
placeholder: "myldapserver.com:636",
type: "text",
},
lookup_bind_dn: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Lookup Bind DN is required" : "";
},
label: "Lookup Bind DN",
tooltip:
"DN for LDAP read-only service account used to perform DN and group lookups",
placeholder: "cn=admin,dc=min,dc=io",
type: "text",
},
lookup_bind_password: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Lookup Bind Password is required" : "";
},
label: "Lookup Bind Password",
tooltip:
"Password for LDAP read-only service account used to perform DN and group lookups",
placeholder: "admin",
type: "password",
},
user_dn_search_base_dn: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "User DN Search Base DN is required" : "";
},
label: "User DN Search Base",
tooltip: "Base LDAP DN to search for user DN",
placeholder: "DC=example,DC=net",
type: "text",
},
user_dn_search_filter: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "User DN Search Filter is required" : "";
},
label: "User DN Search Filter",
tooltip: "Search filter to lookup user DN",
placeholder: "(sAMAcountName=%s)",
type: "text",
},
display_name: {
required: false,
label: "Display Name",
tooltip: "Display Name",
placeholder: "Enter Display Name",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
group_search_base_dn: {
required: false,
hasError: (s: string, editMode: boolean) => "",
label: "Group Search Base DN",
tooltip: "Group Search Base DN",
placeholder: "ou=swengg,dc=min,dc=io",
type: "text",
},
group_search_filter: {
required: false,
hasError: (s: string, editMode: boolean) => "",
label: "Group Search Filter",
tooltip: "Group Search Filter",
placeholder: "(&(objectclass=groupofnames)(member=%d))",
type: "text",
},
};

View File

@@ -59,6 +59,7 @@ import {
import SettingsIcon from "../../icons/SettingsIcon";
import React from "react";
import LicenseBadge from "./Menu/LicenseBadge";
import { LockOpen, Login } from "@mui/icons-material";
export const validRoutes = (
features: string[] | null | undefined,
@@ -141,6 +142,20 @@ export const validRoutes = (
to: IAM_PAGES.POLICIES,
icon: AccessMenuIcon,
},
{
name: "OpenID",
component: NavLink,
id: "openID",
to: IAM_PAGES.IDP_OPENID_CONFIGURATIONS,
icon: LockOpen,
},
{
name: "LDAP",
component: NavLink,
id: "ldap",
to: IAM_PAGES.IDP_LDAP_CONFIGURATIONS,
icon: Login,
},
],
},