LDAP Page improvements (#2708)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
@@ -157,9 +157,7 @@ export const IAM_PAGES = {
|
||||
USER_SA_ACCOUNT_ADD: "/identity/users/new-user-sa/:userName",
|
||||
|
||||
/* IDP */
|
||||
IDP_LDAP_CONFIGURATIONS: "/identity/idp/ldap/configurations",
|
||||
IDP_LDAP_CONFIGURATIONS_VIEW: "/identity/idp/ldap/configurations/:idpName",
|
||||
IDP_LDAP_CONFIGURATIONS_ADD: "/identity/idp/ldap/configurations/add-idp",
|
||||
IDP_LDAP_CONFIGURATIONS: "/identity/ldap/configuration",
|
||||
|
||||
IDP_OPENID_CONFIGURATIONS: "/identity/idp/openid/configurations",
|
||||
IDP_OPENID_CONFIGURATIONS_VIEW:
|
||||
@@ -451,14 +449,6 @@ export const IAM_PAGES_PERMISSIONS = {
|
||||
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,
|
||||
|
||||
@@ -113,20 +113,14 @@ 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")
|
||||
() => import("./IDP/LDAP/IDPLDAPConfigurationDetails")
|
||||
);
|
||||
const IDPOpenIDConfigurationDetails = React.lazy(
|
||||
() => import("./IDP/IDPOpenIDConfigurationDetails")
|
||||
@@ -344,25 +338,17 @@ const Console = ({ classes }: IConsoleProps) => {
|
||||
path: IAM_PAGES.POLICIES,
|
||||
},
|
||||
{
|
||||
component: IDPLDAPConfigurations,
|
||||
component: IDPLDAPConfigurationDetails,
|
||||
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,
|
||||
|
||||
@@ -163,8 +163,16 @@ const ConfTargetGeneric = ({
|
||||
elements={holderItem ? holderItem.value : ""}
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
onChange={(value: string) => {
|
||||
setValueElement(field.name, value, item);
|
||||
onChange={(value: string | string[]) => {
|
||||
let valCh = "";
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
valCh = value.join(",");
|
||||
} else {
|
||||
valCh = value;
|
||||
}
|
||||
|
||||
setValueElement(field.name, valCh, item);
|
||||
}}
|
||||
tooltip={field.tooltip}
|
||||
commonPlaceholder={field.placeholder}
|
||||
|
||||
@@ -80,8 +80,8 @@ const AddIDPConfigurationHelpBox = ({
|
||||
<div>{helpText}</div>
|
||||
</Box>
|
||||
<Box sx={{ fontSize: "14px", marginBottom: "15px" }}>
|
||||
{contents.map((content) => (
|
||||
<Fragment>
|
||||
{contents.map((content, index) => (
|
||||
<Fragment key={`feature-item-${index}`}>
|
||||
{content.icon && (
|
||||
<Box sx={{ paddingBottom: "20px" }}>
|
||||
<FeatureItem
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
// 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 LoginIcon from "@mui/icons-material/Login";
|
||||
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
|
||||
import AddIDPConfiguration from "./AddIDPConfiguration";
|
||||
import { ldapFormFields, ldapHelpBoxContents } from "./utils";
|
||||
import AddIDPConfigurationHelpBox from "./AddIDPConfigurationHelpbox";
|
||||
|
||||
const AddIDPLDAPConfiguration = () => {
|
||||
return (
|
||||
<AddIDPConfiguration
|
||||
icon={<LoginIcon />}
|
||||
helpBox={
|
||||
<AddIDPConfigurationHelpBox
|
||||
helpText={"Learn more about LDAP Configurations"}
|
||||
contents={ldapHelpBoxContents}
|
||||
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 AddIDPLDAPConfiguration;
|
||||
@@ -168,6 +168,7 @@ const IDPConfigurationDetails = ({
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
loadRecord();
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
// 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 { ldapFormFields, ldapHelpBoxContents } from "./utils";
|
||||
import LoginIcon from "@mui/icons-material/Login";
|
||||
import IDPConfigurationDetails from "./IDPConfigurationDetails";
|
||||
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
|
||||
import AddIDPConfigurationHelpBox from "./AddIDPConfigurationHelpbox";
|
||||
|
||||
const IDPLDAPConfigurationDetails = () => {
|
||||
return (
|
||||
<IDPConfigurationDetails
|
||||
backLink={IAM_PAGES.IDP_LDAP_CONFIGURATIONS}
|
||||
header={"LDAP Configurations"}
|
||||
endpoint={"/api/v1/idp/ldap/"}
|
||||
idpType={"ldap"}
|
||||
helpBox={
|
||||
<AddIDPConfigurationHelpBox
|
||||
helpText={"Learn more about LDAP Configurations"}
|
||||
contents={ldapHelpBoxContents}
|
||||
docLink={
|
||||
"https://min.io/docs/minio/linux/operations/external-iam.html?ref=con#minio-external-iam-ad-ldap"
|
||||
}
|
||||
docText={"Learn more about LDAP Configurations"}
|
||||
/>
|
||||
}
|
||||
formFields={ldapFormFields}
|
||||
icon={<LoginIcon width={40} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default IDPLDAPConfigurationDetails;
|
||||
@@ -0,0 +1,436 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 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 {
|
||||
Box,
|
||||
Button,
|
||||
EditIcon,
|
||||
FormLayout,
|
||||
Grid,
|
||||
InputBox,
|
||||
Loader,
|
||||
PageLayout,
|
||||
RefreshIcon,
|
||||
} from "mds";
|
||||
import { ErrorResponseHandler } from "../../../../common/types";
|
||||
import { useAppDispatch } from "../../../../store";
|
||||
import {
|
||||
setErrorSnackMessage,
|
||||
setServerNeedsRestart,
|
||||
setSnackBarMessage,
|
||||
} from "../../../../systemSlice";
|
||||
import api from "../../../../common/api";
|
||||
import ScreenTitle from "../../Common/ScreenTitle/ScreenTitle";
|
||||
import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
|
||||
import LabelValuePair from "../../Common/UsageBarWrapper/LabelValuePair";
|
||||
import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
|
||||
import { ldapFormFields, ldapHelpBoxContents } from "../utils";
|
||||
import AddIDPConfigurationHelpBox from "../AddIDPConfigurationHelpbox";
|
||||
import LDAPEntitiesQuery from "./LDAPEntitiesQuery";
|
||||
import ResetConfigurationModal from "../../EventDestinations/CustomForms/ResetConfigurationModal";
|
||||
import { IConfigurationSys, IElementValue } from "../../Configurations/types";
|
||||
import { TabPanel } from "../../../shared/tabs";
|
||||
import TabSelector from "../../Common/TabSelector/TabSelector";
|
||||
|
||||
const enabledConfigLDAP = [
|
||||
"server_addr",
|
||||
"lookup_bind_dn",
|
||||
"lookup_bind_password",
|
||||
"user_dn_search_base_dn",
|
||||
"user_dn_search_filter",
|
||||
];
|
||||
|
||||
const IDPLDAPConfigurationDetails = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const formFields = ldapFormFields;
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [isEnabled, setIsEnabled] = useState<boolean>(false);
|
||||
const [hasConfiguration, setHasConfiguration] = useState<boolean>(false);
|
||||
const [fields, setFields] = useState<any>({});
|
||||
const [record, setRecord] = useState<IElementValue[] | null>(null);
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const [resetOpen, setResetOpen] = useState<boolean>(false);
|
||||
const [curTab, setCurTab] = useState<number>(0);
|
||||
|
||||
const toggleEditMode = () => {
|
||||
if (editMode && record) {
|
||||
parseFields(record);
|
||||
}
|
||||
setEditMode(!editMode);
|
||||
};
|
||||
|
||||
const parseFields = (record: IElementValue[]) => {
|
||||
let fields: any = {};
|
||||
if (record && record.length > 0) {
|
||||
const enabled = record.find((item: any) => item.key === "enable");
|
||||
|
||||
let totalCoincidences = 0;
|
||||
|
||||
record.forEach((item: any) => {
|
||||
fields[item.key] = item.value;
|
||||
|
||||
if (
|
||||
enabledConfigLDAP.includes(item.key) &&
|
||||
item.value &&
|
||||
item.value !== "" &&
|
||||
item.value !== "off"
|
||||
) {
|
||||
totalCoincidences++;
|
||||
}
|
||||
});
|
||||
|
||||
const hasConfig = totalCoincidences === enabledConfigLDAP.length;
|
||||
|
||||
if ((!enabled || enabled.value === "on") && hasConfig) {
|
||||
setIsEnabled(true);
|
||||
} else {
|
||||
setIsEnabled(false);
|
||||
}
|
||||
|
||||
setHasConfiguration(hasConfig);
|
||||
}
|
||||
setFields(fields);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadRecord = () => {
|
||||
api
|
||||
.invoke("GET", `/api/v1/configs/identity_ldap`)
|
||||
.then((res: IConfigurationSys[]) => {
|
||||
if (res.length > 0) {
|
||||
setRecord(res[0].key_values);
|
||||
parseFields(res[0].key_values);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
setLoading(false);
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
loadRecord();
|
||||
}
|
||||
}, [dispatch, loading]);
|
||||
|
||||
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 saveRecord = () => {
|
||||
const keyVals = Object.keys(formFields).map((key) => {
|
||||
return {
|
||||
key,
|
||||
value: fields[key],
|
||||
};
|
||||
});
|
||||
|
||||
api
|
||||
.invoke("PUT", `/api/v1/configs/identity_ldap`, {
|
||||
key_values: keyVals,
|
||||
})
|
||||
.then((res) => {
|
||||
setEditMode(false);
|
||||
setRecord(keyVals);
|
||||
parseFields(keyVals);
|
||||
dispatch(setServerNeedsRestart(res.restart));
|
||||
|
||||
if (!res.restart) {
|
||||
dispatch(setSnackBarMessage("Configuration saved successfully"));
|
||||
}
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
});
|
||||
};
|
||||
|
||||
const closeDeleteModalAndRefresh = async (refresh: boolean) => {
|
||||
setResetOpen(false);
|
||||
|
||||
if (refresh) {
|
||||
dispatch(setServerNeedsRestart(refresh));
|
||||
setRecord(null);
|
||||
setFields({});
|
||||
setIsEnabled(false);
|
||||
setHasConfiguration(false);
|
||||
setEditMode(false);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
TODO: Review enable / disable functionality for LDAP and enable this module
|
||||
|
||||
const toggleConfiguration = (value: boolean) => {
|
||||
const input: any[] = [];
|
||||
|
||||
const payload = {
|
||||
key_values: [
|
||||
...input,
|
||||
{
|
||||
key: "enable",
|
||||
value: value ? "on" : "off",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
api
|
||||
.invoke("PUT", `/api/v1/configs/identity_ldap`, payload)
|
||||
.then((res) => {
|
||||
setIsEnabled(!isEnabled);
|
||||
dispatch(setServerNeedsRestart(res.restart));
|
||||
if (!res.restart) {
|
||||
dispatch(setSnackBarMessage("Configuration saved successfully"));
|
||||
}
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
});
|
||||
};*/
|
||||
|
||||
const renderFormField = (key: string, value: any) => {
|
||||
switch (value.type) {
|
||||
case "toggle":
|
||||
return (
|
||||
<Box className={"inputItem"}>
|
||||
<FormSwitchWrapper
|
||||
key={key}
|
||||
indicatorLabels={["Enabled", "Disabled"]}
|
||||
checked={fields[key] === "on"}
|
||||
value={"is-field-enabled"}
|
||||
id={"is-field-enabled"}
|
||||
name={"is-field-enabled"}
|
||||
label={value.label}
|
||||
tooltip={value.tooltip}
|
||||
onChange={(e) =>
|
||||
setFields({ ...fields, [key]: e.target.checked ? "on" : "off" })
|
||||
}
|
||||
description=""
|
||||
disabled={!editMode}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<InputBox
|
||||
key={key}
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid item xs={12}>
|
||||
{resetOpen && (
|
||||
<ResetConfigurationModal
|
||||
configurationName={"identity_ldap"}
|
||||
closeResetModalAndRefresh={closeDeleteModalAndRefresh}
|
||||
resetOpen={resetOpen}
|
||||
/>
|
||||
)}
|
||||
<PageHeaderWrapper label={"LDAP"} />
|
||||
<PageLayout variant={"constrained"}>
|
||||
<TabSelector
|
||||
selectedTab={curTab}
|
||||
onChange={(newValue: number) => {
|
||||
setCurTab(newValue);
|
||||
setEditMode(false);
|
||||
}}
|
||||
tabOptions={[
|
||||
{ label: "Configuration" },
|
||||
{
|
||||
label: "Entities",
|
||||
disabled: !hasConfiguration,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<TabPanel index={0} value={curTab}>
|
||||
<ScreenTitle
|
||||
title={editMode ? "Edit Configuration" : ""}
|
||||
actions={
|
||||
!editMode ? (
|
||||
<Fragment>
|
||||
<Button
|
||||
id={"edit"}
|
||||
type="button"
|
||||
variant={"callAction"}
|
||||
icon={<EditIcon />}
|
||||
onClick={toggleEditMode}
|
||||
label={"Edit Configuration"}
|
||||
disabled={loading}
|
||||
/>
|
||||
{/*<Button
|
||||
id={"is-configuration-enabled"}
|
||||
onClick={() => toggleConfiguration(!isEnabled)}
|
||||
label={isEnabled ? "Disable LDAP" : "Enable LDAP"}
|
||||
disabled={loadingEnabledSave}
|
||||
variant={isEnabled ? "secondary" : "regular"}
|
||||
/>*/}
|
||||
<Button
|
||||
id={"refresh-idp-config"}
|
||||
onClick={() => setLoading(true)}
|
||||
label={"Refresh"}
|
||||
icon={<RefreshIcon />}
|
||||
/>
|
||||
</Fragment>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<br />
|
||||
{loading ? (
|
||||
<Box
|
||||
sx={{ display: "flex", justifyContent: "center", marginTop: 10 }}
|
||||
>
|
||||
<Loader />
|
||||
</Box>
|
||||
) : (
|
||||
<Fragment>
|
||||
{editMode ? (
|
||||
<Fragment>
|
||||
<FormLayout
|
||||
helpBox={
|
||||
<AddIDPConfigurationHelpBox
|
||||
helpText={"Learn more about LDAP Configurations"}
|
||||
contents={ldapHelpBoxContents}
|
||||
docLink={
|
||||
"https://min.io/docs/minio/linux/operations/external-iam.html?ref=con#minio-external-iam-ad-ldap"
|
||||
}
|
||||
docText={"Learn more about LDAP Configurations"}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{Object.entries(formFields).map(([key, value]) =>
|
||||
renderFormField(key, value)
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
marginTop: "20px",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
{editMode && (
|
||||
<Button
|
||||
id={"clear"}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => setResetOpen(true)}
|
||||
label={"Reset Configuration"}
|
||||
/>
|
||||
)}
|
||||
{editMode && (
|
||||
<Button
|
||||
id={"cancel"}
|
||||
type="button"
|
||||
variant="regular"
|
||||
onClick={toggleEditMode}
|
||||
label={"Cancel"}
|
||||
/>
|
||||
)}
|
||||
{editMode && (
|
||||
<Button
|
||||
id={"save-key"}
|
||||
type="submit"
|
||||
variant="callAction"
|
||||
color="primary"
|
||||
disabled={loading || !validSave()}
|
||||
label={"Save"}
|
||||
onClick={saveRecord}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</FormLayout>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr",
|
||||
gridAutoFlow: "dense",
|
||||
gap: 3,
|
||||
padding: "15px",
|
||||
border: "1px solid #eaeaea",
|
||||
[`@media (min-width: 576px)`]: {
|
||||
gridTemplateColumns: "2fr 1fr",
|
||||
gridAutoFlow: "row",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LabelValuePair
|
||||
label={"LDAP Enabled"}
|
||||
value={isEnabled ? "Yes" : "No"}
|
||||
/>
|
||||
{hasConfiguration && (
|
||||
<Fragment>
|
||||
{Object.entries(formFields).map(([key, value]) => (
|
||||
<LabelValuePair
|
||||
key={key}
|
||||
label={value.label}
|
||||
value={fields[key] ? fields[key] : ""}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
)}
|
||||
</Box>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel index={1} value={curTab}>
|
||||
{hasConfiguration && (
|
||||
<Box>
|
||||
<LDAPEntitiesQuery />
|
||||
</Box>
|
||||
)}
|
||||
</TabPanel>
|
||||
</PageLayout>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default IDPLDAPConfigurationDetails;
|
||||
360
portal-ui/src/screens/Console/IDP/LDAP/LDAPEntitiesQuery.tsx
Normal file
360
portal-ui/src/screens/Console/IDP/LDAP/LDAPEntitiesQuery.tsx
Normal file
@@ -0,0 +1,360 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 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, useState } from "react";
|
||||
import api from "../../../../common/api";
|
||||
import { ErrorResponseHandler } from "../../../../common/types";
|
||||
import { setErrorSnackMessage } from "../../../../systemSlice";
|
||||
import { AppState, useAppDispatch } from "../../../../store";
|
||||
import {
|
||||
AddIcon,
|
||||
Box,
|
||||
Button,
|
||||
Grid,
|
||||
InputBox,
|
||||
Loader,
|
||||
RemoveIcon,
|
||||
SectionTitle,
|
||||
UptimeIcon,
|
||||
} from "mds";
|
||||
import PolicySelectors from "../../Policies/PolicySelectors";
|
||||
import { useSelector } from "react-redux";
|
||||
import { LDAPEntitiesResponse } from "./types";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
const LDAPEntitiesQuery = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [users, setUsers] = useState<string[]>([""]);
|
||||
const [groups, setGroups] = useState<string[]>([""]);
|
||||
const [results, setResults] = useState<LDAPEntitiesResponse | null>(null);
|
||||
|
||||
const selectedPolicies = useSelector(
|
||||
(state: AppState) => state.createUser.selectedPolicies
|
||||
);
|
||||
|
||||
const searchEntities = () => {
|
||||
setLoading(true);
|
||||
|
||||
let data: any = {};
|
||||
|
||||
let cleanPolicies = selectedPolicies.filter((pol) => pol !== "");
|
||||
let cleanUsers = users.filter((usr) => usr !== "");
|
||||
let cleanGroups = groups.filter((grp) => grp !== "");
|
||||
|
||||
if (cleanPolicies.length > 0) {
|
||||
data["policies"] = cleanPolicies;
|
||||
}
|
||||
|
||||
if (cleanUsers.length > 0) {
|
||||
data["users"] = cleanUsers;
|
||||
}
|
||||
|
||||
if (cleanGroups.length > 0) {
|
||||
data["groups"] = cleanGroups;
|
||||
}
|
||||
|
||||
api
|
||||
.invoke("POST", "/api/v1/ldap-entities", data)
|
||||
.then((result: LDAPEntitiesResponse) => {
|
||||
setResults(result);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const alterUsersList = (addItem: boolean, index: number) => {
|
||||
if (addItem) {
|
||||
const alterUsers = [...users, ""];
|
||||
setUsers(alterUsers);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredUsers = users.filter((_, indx) => indx !== index);
|
||||
|
||||
setUsers(filteredUsers);
|
||||
};
|
||||
|
||||
const alterGroupsList = (addItem: boolean, index: number) => {
|
||||
if (addItem) {
|
||||
const alterGroups = [...groups, ""];
|
||||
setGroups(alterGroups);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredGroups = groups.filter((_, indx) => indx !== index);
|
||||
|
||||
setGroups(filteredGroups);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ marginTop: 15, paddingTop: 0 }} withBorders>
|
||||
<Grid container sx={{ marginTop: 5 }}>
|
||||
<Grid item sm={12} md={6} lg={5} sx={{ padding: 10, paddingTop: 0 }}>
|
||||
<SectionTitle separator>Query Filters</SectionTitle>
|
||||
|
||||
<Box sx={{ padding: "0 10px" }}>
|
||||
<h4>Users</h4>
|
||||
<Box
|
||||
sx={{
|
||||
overflowY: "auto",
|
||||
minHeight: 220,
|
||||
maxHeight: 250,
|
||||
"& > div > div": {
|
||||
width: "100%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{users.map((userDat, index) => {
|
||||
return (
|
||||
<InputBox
|
||||
id={`search-user-${index}`}
|
||||
key={`search-user-${index}`}
|
||||
value={userDat}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const usersElements = [...users];
|
||||
usersElements[index] = e.target.value;
|
||||
setUsers(usersElements);
|
||||
}}
|
||||
overlayIcon={
|
||||
users.length === index + 1 ? <AddIcon /> : <RemoveIcon />
|
||||
}
|
||||
overlayAction={() => {
|
||||
alterUsersList(users.length === index + 1, index);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
<h4>Groups</h4>
|
||||
<Box
|
||||
sx={{
|
||||
overflowY: "auto",
|
||||
minHeight: 220,
|
||||
maxHeight: 250,
|
||||
"& > div > div": {
|
||||
width: "100%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{groups.map((groupDat, index) => {
|
||||
return (
|
||||
<InputBox
|
||||
id={`search-group-${index}`}
|
||||
key={`search-group-${index}`}
|
||||
value={groupDat}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const groupsElements = [...groups];
|
||||
groupsElements[index] = e.target.value;
|
||||
setGroups(groupsElements);
|
||||
}}
|
||||
overlayIcon={
|
||||
groups.length === index + 1 ? <AddIcon /> : <RemoveIcon />
|
||||
}
|
||||
overlayAction={() => {
|
||||
alterGroupsList(groups.length === index + 1, index);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
<h4>Policies</h4>
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: 220,
|
||||
maxHeight: "calc(100vh - 740px)",
|
||||
}}
|
||||
>
|
||||
<PolicySelectors selectedPolicy={selectedPolicies} noTitle />
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item sm={12} md={6} lg={7} sx={{ padding: 10, paddingTop: 0 }}>
|
||||
{loading ? (
|
||||
<Box sx={{ textAlign: "center" }}>
|
||||
<Loader />
|
||||
</Box>
|
||||
) : (
|
||||
<Fragment>
|
||||
<SectionTitle
|
||||
separator
|
||||
sx={{ marginBottom: 15 }}
|
||||
actions={
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{results?.timestamp ? (
|
||||
<Fragment>
|
||||
<UptimeIcon
|
||||
style={{ width: 18, height: 18, marginRight: 5 }}
|
||||
/>
|
||||
{DateTime.fromISO(results.timestamp).toFormat(
|
||||
"D HH:mm:ss"
|
||||
)}
|
||||
</Fragment>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
Results
|
||||
</SectionTitle>
|
||||
{results ? (
|
||||
<Box>
|
||||
{!results.groups && !results.users && !results.policies && (
|
||||
<Box sx={{ textAlign: "center" }}>
|
||||
<h4>No Results Available</h4>
|
||||
</Box>
|
||||
)}
|
||||
{!!results.groups && (
|
||||
<Box className={"resultElement"}>
|
||||
<SectionTitle separator sx={{ fontSize: 12 }}>
|
||||
Group Mappings
|
||||
</SectionTitle>
|
||||
<Box sx={{ padding: "0 15px" }}>
|
||||
{results.groups.map((groupData, index) => {
|
||||
return (
|
||||
<Fragment key={`policy-res-${index}`}>
|
||||
<h4>{groupData.group}</h4>
|
||||
{groupData.policies && (
|
||||
<Fragment>
|
||||
Policies:
|
||||
<ul>
|
||||
{groupData.policies.map(
|
||||
(policy, index2) => (
|
||||
<li key={`policy-group-${index2}`}>
|
||||
{policy}
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{!!results.users && (
|
||||
<Box className={"resultElement"}>
|
||||
<SectionTitle separator sx={{ fontSize: 12 }}>
|
||||
User Mappings
|
||||
</SectionTitle>
|
||||
<Box sx={{ padding: "0 15px" }}>
|
||||
{results.users.map((groupData, index) => {
|
||||
return (
|
||||
<Fragment key={`users-res-${index}`}>
|
||||
<h4>{groupData.user}</h4>
|
||||
{groupData.policies && (
|
||||
<Fragment>
|
||||
Policies:
|
||||
<ul>
|
||||
{groupData.policies.map(
|
||||
(policy, index2) => (
|
||||
<li key={`policy-users-${index2}`}>
|
||||
{policy}
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{!!results.policies && (
|
||||
<Box className={"resultElement"}>
|
||||
<SectionTitle separator sx={{ fontSize: 12 }}>
|
||||
Policy Mappings
|
||||
</SectionTitle>
|
||||
<Box sx={{ padding: "0 15px" }}>
|
||||
{results.policies.map((groupData, index) => {
|
||||
return (
|
||||
<Fragment key={`policy-map-${index}`}>
|
||||
<h4>{groupData.policy}</h4>
|
||||
{groupData.groups && (
|
||||
<Fragment>
|
||||
Groups:
|
||||
<ul>
|
||||
{groupData.groups.map((group, index2) => (
|
||||
<li key={`policy-map-group-${index}`}>
|
||||
{group}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Fragment>
|
||||
)}
|
||||
{groupData.users && (
|
||||
<Fragment>
|
||||
Users:
|
||||
<ul>
|
||||
{groupData.users.map((user, index3) => (
|
||||
<li key={`policy-map-user-${index}`}>
|
||||
{user}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ textAlign: "center" }}>No query results yet</Box>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container>
|
||||
<Grid item xs={12} sx={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<Button
|
||||
id={"search-entity"}
|
||||
type={"button"}
|
||||
variant={"callAction"}
|
||||
onClick={searchEntities}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LDAPEntitiesQuery;
|
||||
@@ -1,5 +1,5 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
// Copyright (c) 2023 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
|
||||
@@ -14,11 +14,31 @@
|
||||
// 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 IDPConfigurations from "./IDPConfigurations";
|
||||
export interface LDAPEntitiesRequest {
|
||||
users?: string[];
|
||||
groups?: string[];
|
||||
policies?: string[];
|
||||
}
|
||||
|
||||
const IDPLDAPConfigurations = () => {
|
||||
return <IDPConfigurations idpType={"ldap"} />;
|
||||
};
|
||||
export interface LDAPEntitiesResponse {
|
||||
timestamp: string;
|
||||
users?: LDAPUsersResponse[];
|
||||
groups?: LDAPGroupsResponse[];
|
||||
policies?: LDAPPoliciesResponse[];
|
||||
}
|
||||
|
||||
export default IDPLDAPConfigurations;
|
||||
export interface LDAPUsersResponse {
|
||||
user: string;
|
||||
policies: string[];
|
||||
}
|
||||
|
||||
export interface LDAPGroupsResponse {
|
||||
group: string;
|
||||
policies: string[];
|
||||
}
|
||||
|
||||
export interface LDAPPoliciesResponse {
|
||||
policy: string;
|
||||
users: string[];
|
||||
groups: string[];
|
||||
}
|
||||
@@ -147,6 +147,16 @@ export const openIDFormFields = {
|
||||
};
|
||||
|
||||
export const ldapFormFields = {
|
||||
server_insecure: {
|
||||
required: true,
|
||||
hasError: (s: string, editMode: boolean) => {
|
||||
return !s && editMode ? "Server Address is required" : "";
|
||||
},
|
||||
label: "Server Insecure",
|
||||
tooltip: "disable SSL certificate verification ",
|
||||
placeholder: "myldapserver.com:636",
|
||||
type: "toggle",
|
||||
},
|
||||
server_addr: {
|
||||
required: true,
|
||||
hasError: (s: string, editMode: boolean) => {
|
||||
@@ -199,14 +209,6 @@ export const ldapFormFields = {
|
||||
placeholder: "(sAMAcountName=%s)",
|
||||
type: "text",
|
||||
},
|
||||
display_name: {
|
||||
required: false,
|
||||
label: "Display Name",
|
||||
tooltip: "",
|
||||
placeholder: "Enter Display Name",
|
||||
type: "text",
|
||||
hasError: (s: string, editMode: boolean) => "",
|
||||
},
|
||||
group_search_base_dn: {
|
||||
required: false,
|
||||
hasError: (s: string, editMode: boolean) => "",
|
||||
|
||||
@@ -42,6 +42,7 @@ import { useSelector } from "react-redux";
|
||||
interface ISelectPolicyProps {
|
||||
classes: any;
|
||||
selectedPolicy?: string[];
|
||||
noTitle?: boolean;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
@@ -75,10 +76,7 @@ const styles = (theme: Theme) =>
|
||||
...selectorsCommon,
|
||||
});
|
||||
|
||||
const PolicySelectors = ({
|
||||
classes,
|
||||
selectedPolicy = [],
|
||||
}: ISelectPolicyProps) => {
|
||||
const PolicySelectors = ({ classes, noTitle = false }: ISelectPolicyProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
// Local State
|
||||
const [records, setRecords] = useState<any[]>([]);
|
||||
@@ -147,7 +145,9 @@ const PolicySelectors = ({
|
||||
{records.length > 0 ? (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12} className={classes.filterBox}>
|
||||
<span className={classes.fieldLabel}>Assign Policies</span>
|
||||
{!noTitle && (
|
||||
<span className={classes.fieldLabel}>Assign Policies</span>
|
||||
)}
|
||||
<div className={classes.searchBox}>
|
||||
<SearchBox
|
||||
placeholder="Start typing to search for a Policy"
|
||||
|
||||
@@ -244,6 +244,7 @@ func getEntitiesResult(ctx context.Context, client MinioAdmin, users, groups, po
|
||||
}
|
||||
|
||||
var result models.LdapEntities
|
||||
|
||||
var usersEntity []*models.LdapUserPolicyEntity
|
||||
var groupsEntity []*models.LdapGroupPolicyEntity
|
||||
var policiesEntity []*models.LdapPolicyEntity
|
||||
|
||||
Reference in New Issue
Block a user