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;