github.com/minio/console@v1.4.1/web-app/src/screens/Console/IDP/LDAP/IDPLDAPConfigurationDetails.tsx (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2023 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  import React, { Fragment, useEffect, useState } from "react";
    18  import {
    19    Box,
    20    Button,
    21    ConsoleIcon,
    22    EditIcon,
    23    FormLayout,
    24    Grid,
    25    HelpBox,
    26    InputBox,
    27    Loader,
    28    PageLayout,
    29    RefreshIcon,
    30    Switch,
    31    Tabs,
    32    Tooltip,
    33    ValuePair,
    34    WarnIcon,
    35    ScreenTitle,
    36  } from "mds";
    37  import { api } from "api";
    38  import { ConfigurationKV } from "api/consoleApi";
    39  import { errorToHandler } from "api/errors";
    40  import { useAppDispatch } from "../../../../store";
    41  import {
    42    setErrorSnackMessage,
    43    setHelpName,
    44    setServerNeedsRestart,
    45    setSnackBarMessage,
    46  } from "../../../../systemSlice";
    47  import { ldapFormFields, ldapHelpBoxContents } from "../utils";
    48  import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
    49  import AddIDPConfigurationHelpBox from "../AddIDPConfigurationHelpbox";
    50  import LDAPEntitiesQuery from "./LDAPEntitiesQuery";
    51  import ResetConfigurationModal from "../../EventDestinations/CustomForms/ResetConfigurationModal";
    52  import HelpMenu from "../../HelpMenu";
    53  
    54  const enabledConfigLDAP = [
    55    "server_addr",
    56    "lookup_bind_dn",
    57    "user_dn_search_base_dn",
    58    "user_dn_search_filter",
    59  ];
    60  
    61  const IDPLDAPConfigurationDetails = () => {
    62    const dispatch = useAppDispatch();
    63  
    64    const formFields = ldapFormFields;
    65  
    66    const [loading, setLoading] = useState<boolean>(true);
    67    const [isEnabled, setIsEnabled] = useState<boolean>(false);
    68    const [hasConfiguration, setHasConfiguration] = useState<boolean>(false);
    69    const [fields, setFields] = useState<any>({});
    70    const [overrideFields, setOverrideFields] = useState<any>({});
    71    const [record, setRecord] = useState<ConfigurationKV[] | undefined>(
    72      undefined,
    73    );
    74    const [editMode, setEditMode] = useState<boolean>(false);
    75    const [resetOpen, setResetOpen] = useState<boolean>(false);
    76    const [curTab, setCurTab] = useState<string>("configuration");
    77    const [envOverride, setEnvOverride] = useState<boolean>(false);
    78  
    79    const toggleEditMode = () => {
    80      if (editMode && record) {
    81        parseFields(record);
    82      }
    83      setEditMode(!editMode);
    84    };
    85  
    86    const parseFields = (record: ConfigurationKV[]) => {
    87      let fields: any = {};
    88      let ovrFlds: any = {};
    89      if (record && record.length > 0) {
    90        const enabled = record.find((item: any) => item.key === "enable");
    91  
    92        let totalCoincidences = 0;
    93        let totalOverride = 0;
    94  
    95        record.forEach((item: any) => {
    96          if (item.env_override) {
    97            fields[item.key] = item.env_override.value;
    98            ovrFlds[item.key] = item.env_override.name;
    99          } else {
   100            fields[item.key] = item.value;
   101          }
   102  
   103          if (
   104            enabledConfigLDAP.includes(item.key) &&
   105            ((item.value && item.value !== "" && item.value !== "off") ||
   106              (item.env_override &&
   107                item.env_override.value !== "" &&
   108                item.env_override.value !== "off"))
   109          ) {
   110            totalCoincidences++;
   111          }
   112  
   113          if (enabledConfigLDAP.includes(item.key) && item.env_override) {
   114            totalOverride++;
   115          }
   116        });
   117  
   118        const hasConfig = totalCoincidences !== 0;
   119  
   120        if (hasConfig && ((enabled && enabled.value !== "off") || !enabled)) {
   121          setIsEnabled(true);
   122        } else {
   123          setIsEnabled(false);
   124        }
   125  
   126        if (totalOverride !== 0) {
   127          setEnvOverride(true);
   128        }
   129  
   130        setHasConfiguration(hasConfig);
   131      }
   132      setOverrideFields(ovrFlds);
   133      setFields(fields);
   134    };
   135  
   136    useEffect(() => {
   137      const loadRecord = () => {
   138        api.configs
   139          .configInfo("identity_ldap")
   140          .then((res) => {
   141            if (res.data.length > 0) {
   142              setRecord(res.data[0].key_values);
   143              parseFields(res.data[0].key_values || []);
   144            }
   145            setLoading(false);
   146          })
   147          .catch((err) => {
   148            setLoading(false);
   149            dispatch(setErrorSnackMessage(errorToHandler(err.error)));
   150          });
   151      };
   152  
   153      if (loading) {
   154        loadRecord();
   155      }
   156    }, [dispatch, loading]);
   157  
   158    const validSave = () => {
   159      for (const [key, value] of Object.entries(formFields)) {
   160        if (
   161          value.required &&
   162          !(
   163            fields[key] !== undefined &&
   164            fields[key] !== null &&
   165            fields[key] !== ""
   166          )
   167        ) {
   168          return false;
   169        }
   170      }
   171      return true;
   172    };
   173  
   174    const saveRecord = () => {
   175      const keyVals = Object.keys(formFields).map((key) => {
   176        return {
   177          key,
   178          value: fields[key],
   179        };
   180      });
   181  
   182      api.configs
   183        .setConfig("identity_ldap", {
   184          key_values: keyVals,
   185        })
   186        .then((res) => {
   187          setEditMode(false);
   188          setRecord(keyVals);
   189          parseFields(keyVals);
   190          dispatch(setServerNeedsRestart(res.data.restart || false));
   191          setFields({ ...fields, lookup_bind_password: "" });
   192  
   193          if (!res.data.restart) {
   194            dispatch(setSnackBarMessage("Configuration saved successfully"));
   195          }
   196        })
   197        .catch((err) => {
   198          dispatch(setErrorSnackMessage(errorToHandler(err.error)));
   199        });
   200    };
   201  
   202    const closeDeleteModalAndRefresh = async (refresh: boolean) => {
   203      setResetOpen(false);
   204  
   205      if (refresh) {
   206        dispatch(setServerNeedsRestart(refresh));
   207        setRecord(undefined);
   208        setFields({});
   209        setIsEnabled(false);
   210        setHasConfiguration(false);
   211        setEditMode(false);
   212      }
   213    };
   214  
   215    const toggleConfiguration = (value: boolean) => {
   216      const payload = {
   217        key_values: [
   218          {
   219            key: "enable",
   220            value: value ? "on" : "off",
   221          },
   222        ],
   223      };
   224  
   225      api.configs
   226        .setConfig("identity_ldap", payload)
   227        .then((res) => {
   228          setIsEnabled(!isEnabled);
   229          dispatch(setServerNeedsRestart(res.data.restart || false));
   230          if (!res.data.restart) {
   231            dispatch(setSnackBarMessage("Configuration saved successfully"));
   232          }
   233        })
   234        .catch((err) => {
   235          dispatch(setErrorSnackMessage(errorToHandler(err.error)));
   236        });
   237    };
   238  
   239    const renderFormField = (key: string, value: any) => {
   240      switch (value.type) {
   241        case "toggle":
   242          return (
   243            <Switch
   244              key={key}
   245              indicatorLabels={["Enabled", "Disabled"]}
   246              checked={fields[key] === "on"}
   247              value={"is-field-enabled"}
   248              id={"is-field-enabled"}
   249              name={"is-field-enabled"}
   250              label={value.label}
   251              tooltip={value.tooltip}
   252              onChange={(e) =>
   253                setFields({ ...fields, [key]: e.target.checked ? "on" : "off" })
   254              }
   255              description=""
   256              disabled={!editMode}
   257            />
   258          );
   259        default:
   260          return (
   261            <InputBox
   262              key={key}
   263              id={key}
   264              required={value.required}
   265              name={key}
   266              label={value.label}
   267              tooltip={value.tooltip}
   268              error={value.hasError(fields[key], editMode)}
   269              value={fields[key] ? fields[key] : ""}
   270              onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
   271                setFields({ ...fields, [key]: e.target.value })
   272              }
   273              placeholder={value.placeholder}
   274              disabled={!editMode}
   275              type={value.type}
   276            />
   277          );
   278      }
   279    };
   280  
   281    useEffect(() => {
   282      dispatch(setHelpName("LDAP"));
   283      // eslint-disable-next-line react-hooks/exhaustive-deps
   284    }, []);
   285  
   286    return (
   287      <Grid item xs={12}>
   288        {resetOpen && (
   289          <ResetConfigurationModal
   290            configurationName={"identity_ldap"}
   291            closeResetModalAndRefresh={closeDeleteModalAndRefresh}
   292            resetOpen={resetOpen}
   293          />
   294        )}
   295        <PageHeaderWrapper label={"LDAP"} actions={<HelpMenu />} />
   296        <PageLayout variant={"constrained"}>
   297          <Tabs
   298            horizontal
   299            options={[
   300              {
   301                tabConfig: { id: "configuration", label: "Configuration" },
   302                content: (
   303                  <Fragment>
   304                    <ScreenTitle
   305                      icon={null}
   306                      title={editMode ? "Edit Configuration" : ""}
   307                      actions={
   308                        !editMode ? (
   309                          <Fragment>
   310                            <Tooltip
   311                              tooltip={
   312                                envOverride
   313                                  ? "Configuration cannot be edited in this module as LDAP environment variables are set for this MinIO instance."
   314                                  : ""
   315                              }
   316                            >
   317                              <Button
   318                                id={"edit"}
   319                                type="button"
   320                                variant={"callAction"}
   321                                icon={<EditIcon />}
   322                                onClick={toggleEditMode}
   323                                label={"Edit Configuration"}
   324                                disabled={loading || envOverride}
   325                              />
   326                            </Tooltip>
   327                            {hasConfiguration && (
   328                              <Tooltip
   329                                tooltip={
   330                                  envOverride
   331                                    ? "Configuration cannot be disabled / enabled in this module as LDAP environment variables are set for this MinIO instance."
   332                                    : ""
   333                                }
   334                              >
   335                                <Button
   336                                  id={"is-configuration-enabled"}
   337                                  onClick={() => toggleConfiguration(!isEnabled)}
   338                                  label={
   339                                    isEnabled ? "Disable LDAP" : "Enable LDAP"
   340                                  }
   341                                  variant={isEnabled ? "secondary" : "regular"}
   342                                  disabled={envOverride}
   343                                />
   344                              </Tooltip>
   345                            )}
   346                            <Button
   347                              id={"refresh-idp-config"}
   348                              onClick={() => setLoading(true)}
   349                              label={"Refresh"}
   350                              icon={<RefreshIcon />}
   351                            />
   352                          </Fragment>
   353                        ) : null
   354                      }
   355                    />
   356                    <br />
   357                    {loading ? (
   358                      <Box
   359                        sx={{
   360                          display: "flex",
   361                          justifyContent: "center",
   362                          marginTop: 10,
   363                        }}
   364                      >
   365                        <Loader />
   366                      </Box>
   367                    ) : (
   368                      <Fragment>
   369                        {editMode ? (
   370                          <Fragment>
   371                            <FormLayout
   372                              helpBox={
   373                                <AddIDPConfigurationHelpBox
   374                                  helpText={
   375                                    "Learn more about LDAP Configurations"
   376                                  }
   377                                  contents={ldapHelpBoxContents}
   378                                  docLink={
   379                                    "https://min.io/docs/minio/linux/operations/external-iam.html?ref=con#minio-external-iam-ad-ldap"
   380                                  }
   381                                  docText={"Learn more about LDAP Configurations"}
   382                                />
   383                              }
   384                            >
   385                              {editMode && hasConfiguration ? (
   386                                <Box sx={{ marginBottom: 15 }}>
   387                                  <HelpBox
   388                                    title={
   389                                      <Box
   390                                        style={{
   391                                          display: "flex",
   392                                          justifyContent: "space-between",
   393                                          alignItems: "center",
   394                                          flexGrow: 1,
   395                                        }}
   396                                      >
   397                                        Lookup Bind Password must be re-entered to
   398                                        change LDAP configurations
   399                                      </Box>
   400                                    }
   401                                    iconComponent={<WarnIcon />}
   402                                    help={null}
   403                                  />
   404                                </Box>
   405                              ) : null}
   406                              {Object.entries(formFields).map(([key, value]) =>
   407                                renderFormField(key, value),
   408                              )}
   409                              <Box
   410                                sx={{
   411                                  display: "flex",
   412                                  alignItems: "center",
   413                                  justifyContent: "flex-end",
   414                                  marginTop: "20px",
   415                                  gap: "15px",
   416                                }}
   417                              >
   418                                {editMode && hasConfiguration && (
   419                                  <Button
   420                                    id={"clear"}
   421                                    type="button"
   422                                    variant="secondary"
   423                                    onClick={() => setResetOpen(true)}
   424                                    label={"Reset Configuration"}
   425                                  />
   426                                )}
   427                                <Button
   428                                  id={"cancel"}
   429                                  type="button"
   430                                  variant="regular"
   431                                  onClick={toggleEditMode}
   432                                  label={"Cancel"}
   433                                />
   434                                <Button
   435                                  id={"save-key"}
   436                                  type="submit"
   437                                  variant="callAction"
   438                                  color="primary"
   439                                  disabled={loading || !validSave()}
   440                                  label={"Save"}
   441                                  onClick={saveRecord}
   442                                />
   443                              </Box>
   444                            </FormLayout>
   445                          </Fragment>
   446                        ) : (
   447                          <Fragment>
   448                            <Box
   449                              sx={{
   450                                display: "grid",
   451                                gridTemplateColumns: "1fr",
   452                                gridAutoFlow: "dense",
   453                                gap: 3,
   454                                padding: "15px",
   455                                border: "1px solid #eaeaea",
   456                                [`@media (min-width: 576px)`]: {
   457                                  gridTemplateColumns: "2fr 1fr",
   458                                  gridAutoFlow: "row",
   459                                },
   460                              }}
   461                            >
   462                              <ValuePair
   463                                label={"LDAP Enabled"}
   464                                value={isEnabled ? "Yes" : "No"}
   465                              />
   466                              {hasConfiguration && (
   467                                <Fragment>
   468                                  {Object.entries(formFields).map(
   469                                    ([key, value]) => {
   470                                      if (!value.editOnly) {
   471                                        let label: React.ReactNode = value.label;
   472                                        let val: React.ReactNode = fields[key]
   473                                          ? fields[key]
   474                                          : "";
   475  
   476                                        if (overrideFields[key]) {
   477                                          label = (
   478                                            <Box
   479                                              sx={{
   480                                                display: "flex",
   481                                                alignItems: "center",
   482                                                gap: 5,
   483                                                "& .min-icon": {
   484                                                  height: 20,
   485                                                  width: 20,
   486                                                },
   487                                                "& span": {
   488                                                  height: 20,
   489                                                  display: "flex",
   490                                                  alignItems: "center",
   491                                                },
   492                                              }}
   493                                            >
   494                                              <span>{value.label}</span>
   495                                              <Tooltip
   496                                                tooltip={`This value is set from the ${overrideFields[key]} environment variable`}
   497                                                placement={"right"}
   498                                              >
   499                                                <span className={"muted"}>
   500                                                  <ConsoleIcon />
   501                                                </span>
   502                                              </Tooltip>
   503                                            </Box>
   504                                          );
   505  
   506                                          val = (
   507                                            <i>
   508                                              <span className={"muted"}>
   509                                                {val}
   510                                              </span>
   511                                            </i>
   512                                          );
   513                                        }
   514                                        return (
   515                                          <ValuePair
   516                                            key={key}
   517                                            label={label}
   518                                            value={val}
   519                                          />
   520                                        );
   521                                      }
   522                                      return null;
   523                                    },
   524                                  )}
   525                                </Fragment>
   526                              )}
   527                            </Box>
   528                          </Fragment>
   529                        )}
   530                      </Fragment>
   531                    )}
   532                  </Fragment>
   533                ),
   534              },
   535              {
   536                tabConfig: {
   537                  id: "entities",
   538                  label: "Entities",
   539                  disabled: !hasConfiguration || !isEnabled,
   540                },
   541                content: (
   542                  <Fragment>
   543                    {hasConfiguration && (
   544                      <Box>
   545                        <LDAPEntitiesQuery />
   546                      </Box>
   547                    )}
   548                  </Fragment>
   549                ),
   550              },
   551            ]}
   552            currentTabOrPath={curTab}
   553            onTabClick={(newTab) => {
   554              setCurTab(newTab);
   555              setEditMode(false);
   556            }}
   557          />
   558        </PageLayout>
   559      </Grid>
   560    );
   561  };
   562  
   563  export default IDPLDAPConfigurationDetails;