#50175 Issue 50041 - CLI and WebUI - Add memberOf plugin functionality
Closed 3 years ago by spichugi. Opened 5 years ago by spichugi.
spichugi/389-ds-base plugin_ui_cli  into  master

@@ -44,7 +44,8 @@ 

          "eqeqeq": "off",

          "import/no-webpack-loader-syntax": "off",

          "object-property-newline": "off",

-         "react/jsx-no-bind": "off"

+         "react/jsx-no-bind": "off",

+         "max-len": ["error", { "code": 100 }]

      },

      "globals": {

          "require": false,

@@ -14,13 +14,13 @@ 

      "@babel/core": "^7.0.0",

      "@babel/preset-env": "^7.0.0",

      "@babel/preset-react": "^7.0.0",

+     "ajv": "^6.0.0",

      "babel-eslint": "^9.0.0",

      "babel-loader": "^8.0.0",

      "chrome-remote-interface": "^0.25.5",

      "compression-webpack-plugin": "^1.1.11",

      "copy-webpack-plugin": "^4.5.2",

      "css-loader": "^0.28.11",

-     "style-loader": "^0.23.1",

      "eslint": "^5.4.0",

      "eslint-config-standard": "^11.0.0",

      "eslint-config-standard-react": "^6.0.0",
@@ -34,17 +34,20 @@ 

      "extract-text-webpack-plugin": "^4.0.0-beta.0",

      "htmlparser": "^1.7.7",

      "jed": "^1.1.1",

-     "ajv": "^6.0.0",

      "sass-loader": "^7.0.3",

      "sizzle": "^2.3.3",

      "stdio": "^0.2.7",

+     "style-loader": "^0.23.1",

      "webpack": "^4.17.1",

      "webpack-cli": "^3.1.0"

    },

    "dependencies": {

      "patternfly": "3.58.0",

      "patternfly-react": "2.24.5",

+     "bootstrap": "4.2.1",

+     "node-sass": "4.11.0",

      "react-bootstrap": "0.32.4",

+     "react-bootstrap-typeahead": "3.2.4",

      "react": "16.6.1",

      "react-dom": "16.6.1",

      "prop-types": "15.6.2",

@@ -1310,7 +1310,7 @@ 

  .ds-td {

      padding-left: 10px !important;

      padding-top: 8px !important;

-     padding-bottom: 0px ~important;

+     padding-bottom: 0px !important;

      valign: middle !important;

  }

  
@@ -1454,6 +1454,11 @@ 

      bottom: 10px;

  }

  

+ .ds-plugin-button {

+     margin-right: 5px;

+     margin-left: 5px;

+ }

+ 

  .ds-plugin-spinner {

      margin-top: 20px;

      margin-bottom: 10px;
@@ -1494,3 +1499,12 @@ 

  .control-label {

      text-align: left !important;

  }

+ 

+ .rbt-token {

+     background-color: #ededed;

+     color: #363636;

+ }

+ 

+ .rbt-input-multi {

+     height: auto !important;

+ }

@@ -30,6 +30,7 @@ 

    <link href="static/jquery.timepicker.min.css" type="text/css" rel="stylesheet">

    <link href="static/style.min.css" rel="stylesheet">

    <link href="static/patternfly-additions.css" rel="stylesheet" type="text/css">

+   <link href="static/Typeahead.css" rel="stylesheet" type="text/css">

    <link rel="stylesheet" href="../base1/patternfly.css">

    <link href="css/ds.css" type="text/css" rel="stylesheet">

  </head>

@@ -26,7 +26,6 @@ 

                  >

                      <Icon

                          type="fa"

-                         size="1,5x"

                          name={open ? "caret-down" : "caret-right"}

                      />{" "}

                      {open ? textOpened : textClosed}

@@ -1,24 +1,975 @@ 

+ import cockpit from "cockpit";

  import React from "react";

- import { noop } from "patternfly-react";

+ import {

+     Icon,

+     Modal,

+     Button,

+     Row,

+     Col,

+     Form,

+     noop,

+     FormGroup,

+     FormControl,

+     Checkbox,

+     ControlLabel

+ } from "patternfly-react";

+ import { Typeahead } from "react-bootstrap-typeahead";

  import PropTypes from "prop-types";

  import PluginBasicConfig from "./pluginBasicConfig.jsx";

+ import { log_cmd } from "../tools.jsx";

  import "../../css/ds.css";

  

  class MemberOf extends React.Component {

+     componentWillMount(prevProps) {

+         this.updateFields();

+     }

+ 

+     componentDidUpdate(prevProps) {

+         if (this.props.rows !== prevProps.rows) {

+             this.updateFields();

+         }

+     }

+ 

+     constructor(props) {

+         super(props);

+ 

+         this.updateFields = this.updateFields.bind(this);

+         this.handleFieldChange = this.handleFieldChange.bind(this);

+         this.handleCheckboxChange = this.handleCheckboxChange.bind(this);

+         this.openModal = this.openModal.bind(this);

+         this.closeModal = this.closeModal.bind(this);

+         this.addConfig = this.addConfig.bind(this);

+         this.editConfig = this.editConfig.bind(this);

+         this.deleteConfig = this.deleteConfig.bind(this);

+         this.cmdOperation = this.cmdOperation.bind(this);

+         this.runFixup = this.runFixup.bind(this);

+         this.toggleFixupModal = this.toggleFixupModal.bind(this);

+ 

+         this.state = {

+             memberOfAttr: [],

+             memberOfGroupAttr: [],

+             memberOfEntryScope: "",

+             memberOfEntryScopeExcludeSubtree: "",

+             memberOfAutoAddOC: "",

+             memberOfAllBackends: false,

+             memberOfSkipNested: false,

+             memberOfConfigEntry: "",

+             configEntryModalShow: false,

+             fixupModalShow: false,

+ 

+             configDN: "",

+             configAttr: [],

+             configGroupAttr: [],

+             configEntryScope: "",

+             configEntryScopeExcludeSubtree: "",

+             configAutoAddOC: "",

+             configAllBackends: false,

+             configSkipNested: false,

+             newEntry: true,

+ 

+             fixupDN: "",

+             fixupFilter: ""

+         };

+     }

+ 

+     toggleFixupModal() {

+         this.setState(prevState => ({

+             fixupModalShow: !prevState.fixupModalShow,

+             fixupDN: "",

+             fixupFilter: ""

+         }));

+     }

+ 

+     runFixup() {

+         if (!this.state.fixupDN) {

+             this.props.addNotification("warning", "Fixup DN is required.");

+         } else {

+             let cmd = [

+                 "dsconf",

+                 "-j",

+                 "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",

+                 "plugin",

+                 "memberof",

+                 "fixup",

+                 this.state.fixupDN

+             ];

+ 

+             if (this.state.fixupFilter) {

+                 cmd = [...cmd, "--filter", this.state.fixupFilter];

+             }

+ 

+             this.props.toggleLoadingHandler();

+             log_cmd("runFixup", "Run fixup MemberOf Plugin ", cmd);

+             cockpit

+                     .spawn(cmd, {

+                         superuser: true,

+                         err: "message"

+                     })

+                     .done(content => {

+                         this.props.addNotification(

+                             "success",

+                             `Fixup task for ${this.state.fixupDN} was successfull`

+                         );

+                         this.props.toggleLoadingHandler();

+                         this.setState({

+                             fixupModalShow: false

+                         });

+                     })

+                     .fail(err => {

+                         this.props.addNotification(

+                             "error",

+                             `Fixup task for ${this.state.fixupDN} has failed ${err}`

+                         );

+                         this.props.toggleLoadingHandler();

+                         this.setState({

+                             fixupModalShow: false

+                         });

+                     });

+         }

+     }

+ 

+     openModal() {

+         if (!this.state.memberOfConfigEntry) {

+             this.setState({

+                 configEntryModalShow: true,

+                 newEntry: true,

+                 configDN: "",

+                 configAttr: [],

+                 configGroupAttr: [],

+                 configEntryScope: "",

+                 configEntryScopeExcludeSubtree: "",

+                 configAutoAddOC: "",

+                 configAllBackends: false,

+                 configSkipNested: false

+             });

+         } else {

+             let configAttrObjectList = [];

+             let configGroupAttrObjectList = [];

+             let cmd = [

+                 "dsconf",

+                 "-j",

+                 "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",

+                 "plugin",

+                 "memberof",

+                 "config-entry",

+                 "show",

+                 this.state.memberOfConfigEntry

+             ];

+ 

+             this.props.toggleLoadingHandler();

+             log_cmd("openMemberOfModal", "Fetch the MemberOf Plugin config entry", cmd);

+             cockpit

+                     .spawn(cmd, {

+                         superuser: true,

+                         err: "message"

+                     })

+                     .done(content => {

+                         let configEntry = JSON.parse(content).attrs;

+                         this.setState({

+                             configEntryModalShow: true,

+                             newEntry: false,

+                             configDN: this.state.memberOfConfigEntry,

+                             configAutoAddOC:

+                             configEntry["memberofautoaddoc"] === undefined

+                                 ? ""

+                                 : configEntry["memberofautoaddoc"][0],

+                             configAllBackends: !(

+                                 configEntry["memberofallbackends"] === undefined ||

+                             configEntry["memberofallbackends"][0] == "off"

+                             ),

+                             configSkipNested: !(

+                                 configEntry["memberofskipnested"] === undefined ||

+                             configEntry["memberofskipnested"][0] == "off"

+                             ),

+                             configConfigEntry:

+                             configEntry["nsslapd-pluginConfigArea"] === undefined

+                                 ? ""

+                                 : configEntry["nsslapd-pluginConfigArea"][0],

+                             configEntryScope:

+                             configEntry["memberofentryscope"] === undefined

+                                 ? ""

+                                 : configEntry["memberofentryscope"][0],

+                             configEntryScopeExcludeSubtree:

+                             configEntry["memberofentryscopeexcludesubtree"] === undefined

+                                 ? ""

+                                 : configEntry["memberofentryscopeexcludesubtree"][0]

+                         });

+                         if (configEntry["memberofattr"] === undefined) {

+                             this.setState({ configAttr: [] });

+                         } else {

+                             for (let value of configEntry["memberofattr"]) {

+                                 configAttrObjectList = [

+                                     ...configAttrObjectList,

+                                     { id: value, label: value }

+                                 ];

+                             }

+                             this.setState({ configAttr: configAttrObjectList });

+                         }

+                         if (configEntry["memberofgroupattr"] === undefined) {

+                             this.setState({ configGroupAttr: [] });

+                         } else {

+                             for (let value of configEntry["memberofgroupattr"]) {

+                                 configGroupAttrObjectList = [

+                                     ...configGroupAttrObjectList,

+                                     { id: value, label: value }

+                                 ];

+                             }

+                             this.setState({ configGroupAttr: configGroupAttrObjectList });

+                             this.props.toggleLoadingHandler();

+                         }

+                     })

+                     .fail(_ => {

+                         this.setState({

+                             configEntryModalShow: true,

+                             newEntry: true,

+                             configDN: this.state.memberOfConfigEntry,

+                             configAttr: [],

+                             configGroupAttr: [],

+                             configEntryScope: "",

+                             configEntryScopeExcludeSubtree: "",

+                             configAutoAddOC: "",

+                             configAllBackends: false,

+                             configSkipNested: false

+                         });

+                         this.props.toggleLoadingHandler();

+                     });

+         }

+     }

+ 

+     closeModal() {

+         this.setState({ configEntryModalShow: false });

+     }

+ 

+     cmdOperation(action) {

+         const {

+             configDN,

+             configAttr,

+             configGroupAttr,

+             configEntryScope,

+             configEntryScopeExcludeSubtree,

+             configAutoAddOC,

+             configAllBackends,

+             configSkipNested

+         } = this.state;

+ 

+         if (configAttr.length == 0 || configGroupAttr.length == 0) {

+             this.props.addNotification(

+                 "warning",

+                 "Config Attribute and Group Attribute are required."

+             );

+         } else {

+             let cmd = [

+                 "dsconf",

+                 "-j",

+                 "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",

+                 "plugin",

+                 "memberof",

+                 "config-entry",

+                 action,

+                 configDN,

+                 "--scope",

+                 configEntryScope || action == "add" ? configEntryScope : "delete",

+                 "--exclude",

+                 configEntryScopeExcludeSubtree || action == "add"

+                     ? configEntryScopeExcludeSubtree

+                     : "delete",

+                 "--autoaddoc",

+                 configAutoAddOC || action == "add" ? configAutoAddOC : "delete",

+                 "--allbackends",

+                 configAllBackends ? "on" : "off",

+                 "--skipnested",

+                 configSkipNested ? "on" : "off"

+             ];

+ 

+             // Delete attributes if the user set an empty value to the field

+             cmd = [...cmd, "--attr"];

+             if (configAttr.length != 0) {

+                 for (let value of configAttr) {

+                     cmd = [...cmd, value.label];

+                 }

+             }

+             cmd = [...cmd, "--groupattr"];

+             if (configGroupAttr.length != 0) {

+                 for (let value of configGroupAttr) {

+                     cmd = [...cmd, value.label];

+                 }

+             }

+ 

+             this.props.toggleLoadingHandler();

+             log_cmd("memberOfOperation", `Do the ${action} operation on the MemberOf Plugin`, cmd);

+             cockpit

+                     .spawn(cmd, {

+                         superuser: true,

+                         err: "message"

+                     })

+                     .done(content => {

+                         console.info("memberOfOperation", "Result", content);

+                         this.props.addNotification(

+                             "success",

+                             `Config entry ${configDN} was successfully ${action}ed`

+                         );

+                         this.props.pluginListHandler();

+                         this.closeModal();

+                         this.props.toggleLoadingHandler();

+                     })

+                     .fail(err => {

+                         this.props.addNotification(

+                             "error",

+                             `Error during the config entry ${action} operation - ${err}`

+                         );

+                         this.props.pluginListHandler();

+                         this.closeModal();

+                         this.props.toggleLoadingHandler();

+                     });

+         }

+     }

+ 

+     deleteConfig() {

+         let cmd = [

+             "dsconf",

+             "-j",

+             "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",

+             "plugin",

+             "memberof",

+             "config-entry",

+             "delete",

+             this.state.configDN

+         ];

+ 

+         this.props.toggleLoadingHandler();

+         log_cmd("deleteConfig", "Delete the MemberOf Plugin config entry", cmd);

+         cockpit

+                 .spawn(cmd, {

+                     superuser: true,

+                     err: "message"

+                 })

+                 .done(content => {

+                     console.info("deleteConfig", "Result", content);

+                     this.props.addNotification(

+                         "success",

+                         `Config entry ${this.state.configDN} was successfully deleted`

+                     );

+                     this.props.pluginListHandler();

+                     this.closeModal();

+                     this.props.toggleLoadingHandler();

+                 })

+                 .fail(err => {

+                     this.props.addNotification(

+                         "error",

+                         `Error during the config entry removal operation - ${err}`

+                     );

+                     this.props.pluginListHandler();

+                     this.closeModal();

+                     this.props.toggleLoadingHandler();

+                 });

+     }

+ 

+     addConfig() {

+         this.cmdOperation("add");

+     }

+ 

+     editConfig() {

+         this.cmdOperation("edit");

+     }

+ 

+     handleCheckboxChange(e) {

+         this.setState({

+             [e.target.id]: e.target.checked

+         });

+     }

+ 

+     handleFieldChange(e) {

+         this.setState({

+             [e.target.id]: e.target.value

+         });

+     }

+ 

+     updateFields() {

+         let memberOfAttrObjectList = [];

+         let memberOfGroupAttrObjectList = [];

+ 

+         if (this.props.rows.length > 0) {

+             const pluginRow = this.props.rows.find(row => row.cn[0] === "MemberOf Plugin");

+ 

+             this.setState({

+                 memberOfAutoAddOC:

+                     pluginRow["memberofautoaddoc"] === undefined

+                         ? ""

+                         : pluginRow["memberofautoaddoc"][0],

+                 memberOfAllBackends: !(

+                     pluginRow["memberofallbackends"] === undefined ||

+                     pluginRow["memberofallbackends"][0] == "off"

+                 ),

+                 memberOfSkipNested: !(

+                     pluginRow["memberofskipnested"] === undefined ||

+                     pluginRow["memberofskipnested"][0] == "off"

+                 ),

+                 memberOfConfigEntry:

+                     pluginRow["nsslapd-pluginConfigArea"] === undefined

+                         ? ""

+                         : pluginRow["nsslapd-pluginConfigArea"][0],

+                 memberOfEntryScope:

+                     pluginRow["memberofentryscope"] === undefined

+                         ? ""

+                         : pluginRow["memberofentryscope"][0],

+                 memberOfEntryScopeExcludeSubtree:

+                     pluginRow["memberofentryscopeexcludesubtree"] === undefined

+                         ? ""

+                         : pluginRow["memberofentryscopeexcludesubtree"][0]

+             });

+             if (pluginRow["memberofattr"] === undefined) {

+                 this.setState({ memberOfAttr: [] });

+             } else {

+                 for (let value of pluginRow["memberofattr"]) {

+                     memberOfAttrObjectList = [

+                         ...memberOfAttrObjectList,

+                         { id: value, label: value }

+                     ];

+                 }

+                 this.setState({ memberOfAttr: memberOfAttrObjectList });

+             }

+             if (pluginRow["memberofgroupattr"] === undefined) {

+                 this.setState({ memberOfGroupAttr: [] });

+             } else {

+                 for (let value of pluginRow["memberofgroupattr"]) {

+                     memberOfGroupAttrObjectList = [

+                         ...memberOfGroupAttrObjectList,

+                         { id: value, label: value }

+                     ];

+                 }

+                 this.setState({ memberOfGroupAttr: memberOfGroupAttrObjectList });

+             }

+         }

+     }

+ 

      render() {

+         const {

+             memberOfAttr,

+             memberOfGroupAttr,

+             memberOfEntryScope,

+             memberOfEntryScopeExcludeSubtree,

+             memberOfAutoAddOC,

+             memberOfAllBackends,

+             memberOfSkipNested,

+             memberOfConfigEntry,

+             configDN,

+             configEntryModalShow,

+             configAttr,

+             configGroupAttr,

+             configEntryScope,

+             configEntryScopeExcludeSubtree,

+             configAutoAddOC,

+             configAllBackends,

+             configSkipNested,

+             newEntry,

+             fixupModalShow,

+             fixupDN,

+             fixupFilter

+         } = this.state;

+ 

+         let specificPluginCMD = [

+             "dsconf",

+             "-j",

+             "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",

+             "plugin",

+             "memberof",

+             "edit",

+             "--scope",

+             memberOfEntryScope || "delete",

+             "--exclude",

+             memberOfEntryScopeExcludeSubtree || "delete",

+             "--autoaddoc",

+             memberOfAutoAddOC || "delete",

+             "--config-entry",

+             memberOfConfigEntry || "delete",

+             "--allbackends",

+             memberOfAllBackends ? "on" : "off",

+             "--skipnested",

+             memberOfSkipNested ? "on" : "off"

+         ];

+ 

+         // Delete attributes if the user set an empty value to the field

+         specificPluginCMD = [...specificPluginCMD, "--attr"];

+         if (memberOfAttr.length != 0) {

+             for (let value of memberOfAttr) {

+                 specificPluginCMD = [...specificPluginCMD, value.label];

+             }

+         } else {

+             specificPluginCMD = [...specificPluginCMD, "delete"];

+         }

+ 

+         specificPluginCMD = [...specificPluginCMD, "--groupattr"];

+         if (memberOfGroupAttr.length != 0) {

+             for (let value of memberOfGroupAttr) {

+                 specificPluginCMD = [...specificPluginCMD, value.label];

+             }

+         } else {

+             specificPluginCMD = [...specificPluginCMD, "delete"];

+         }

+ 

          return (

              <div>

+                 <Modal show={fixupModalShow} onHide={this.toggleFixupModal}>

+                     <div className="ds-no-horizontal-scrollbar">

+                         <Modal.Header>

+                             <button

+                                 className="close"

+                                 onClick={this.toggleFixupModal}

+                                 aria-hidden="true"

+                                 aria-label="Close"

+                             >

+                                 <Icon type="pf" name="close" />

+                             </button>

+                             <Modal.Title>Fixup MemberOf Task</Modal.Title>

+                         </Modal.Header>

+                         <Modal.Body>

+                             <Row>

+                                 <Col sm={12}>

+                                     <Form horizontal>

+                                         <FormGroup controlId="fixupDN" key="fixupDN">

+                                             <Col sm={3}>

+                                                 <ControlLabel>Base DN</ControlLabel>

+                                             </Col>

+                                             <Col sm={9}>

+                                                 <FormControl

+                                                     type="text"

+                                                     value={fixupDN}

+                                                     onChange={this.handleFieldChange}

+                                                 />

+                                             </Col>

+                                         </FormGroup>

+                                         <FormGroup controlId="fixupFilter" key="fixupFilter">

+                                             <Col sm={3}>

+                                                 <ControlLabel>Filter DN</ControlLabel>

+                                             </Col>

+                                             <Col sm={9}>

+                                                 <FormControl

+                                                     type="text"

+                                                     value={fixupFilter}

+                                                     onChange={this.handleFieldChange}

+                                                 />

+                                             </Col>

+                                         </FormGroup>

+                                     </Form>

+                                 </Col>

+                             </Row>

+                         </Modal.Body>

+                         <Modal.Footer>

+                             <Button

+                                 bsStyle="default"

+                                 className="btn-cancel"

+                                 onClick={this.toggleFixupModal}

+                             >

+                                 Cancel

+                             </Button>

+                             <Button bsStyle="primary" onClick={this.runFixup}>

+                                 Run

+                             </Button>

+                         </Modal.Footer>

+                     </div>

+                 </Modal>

+                 <Modal show={configEntryModalShow} onHide={this.closeModal}>

+                     <div className="ds-no-horizontal-scrollbar">

+                         <Modal.Header>

+                             <button

+                                 className="close"

+                                 onClick={this.closeModal}

+                                 aria-hidden="true"

+                                 aria-label="Close"

+                             >

+                                 <Icon type="pf" name="close" />

+                             </button>

+                             <Modal.Title>Manage MemberOf Plugin Shared Config Entry</Modal.Title>

+                         </Modal.Header>

+                         <Modal.Body>

+                             <Row>

+                                 <Col sm={12}>

+                                     <Form horizontal>

+                                         <FormGroup controlId="configDN">

+                                             <Col sm={3}>

+                                                 <ControlLabel>Config DN</ControlLabel>

+                                             </Col>

+                                             <Col sm={9}>

+                                                 <FormControl

+                                                     type="text"

+                                                     value={configDN}

+                                                     onChange={this.handleFieldChange}

+                                                     disabled={!newEntry}

+                                                 />

+                                             </Col>

+                                         </FormGroup>

+                                         <FormGroup

+                                             key="configAttr"

+                                             controlId="configAttr"

+                                             disabled={false}

+                                         >

+                                             <Col componentClass={ControlLabel} sm={3}>

+                                                 Attribute

+                                             </Col>

+                                             <Col sm={9}>

+                                                 <Typeahead

+                                                     allowNew

+                                                     multiple

+                                                     onChange={values => {

+                                                         this.setState({

+                                                             configAttr: values

+                                                         });

+                                                     }}

+                                                     selected={configAttr}

+                                                     newSelectionPrefix="Add a member: "

+                                                     options={[

+                                                         {

+                                                             id: "memberOf",

+                                                             label: "memberOf"

+                                                         }

+                                                     ]}

+                                                     placeholder="Type a member attribute..."

+                                                 />

+                                             </Col>

+                                         </FormGroup>

+                                         <FormGroup

+                                             key="configGroupAttr"

+                                             controlId="configGroupAttr"

+                                             disabled={false}

+                                         >

+                                             <Col componentClass={ControlLabel} sm={3}>

+                                                 Group Attribute

+                                             </Col>

+                                             <Col sm={9}>

+                                                 <Typeahead

+                                                     allowNew

+                                                     multiple

+                                                     onChange={values => {

+                                                         this.setState({

+                                                             configGroupAttr: values

+                                                         });

+                                                     }}

+                                                     selected={configGroupAttr}

+                                                     newSelectionPrefix="Add a group member: "

+                                                     options={[

+                                                         {

+                                                             id: "member",

+                                                             label: "member"

+                                                         }

+                                                     ]}

+                                                     placeholder="Type a member group attribute..."

+                                                 />

+                                             </Col>

+                                         </FormGroup>

+                                     </Form>

+                                 </Col>

+                             </Row>

+                             <Row>

+                                 <Col sm={12}>

+                                     <Form horizontal>

+                                         <FormGroup

+                                             key="configEntryScope"

+                                             controlId="configEntryScope"

+                                             disabled={false}

+                                         >

+                                             <Col componentClass={ControlLabel} sm={3}>

+                                                 Entry Scope

+                                             </Col>

+                                             <Col sm={6}>

+                                                 <FormControl

+                                                     type="text"

+                                                     value={configEntryScope}

+                                                     onChange={this.handleFieldChange}

+                                                 />

+                                             </Col>

+                                             <Col sm={3}>

+                                                 <Checkbox

+                                                     id="configAllBackends"

+                                                     checked={configAllBackends}

+                                                     onChange={this.handleCheckboxChange}

+                                                 >

+                                                     All Backends

+                                                 </Checkbox>

+                                             </Col>

+                                         </FormGroup>

+                                         <FormGroup

+                                             key="configEntryScopeExcludeSubtree"

+                                             controlId="configEntryScopeExcludeSubtree"

+                                             disabled={false}

+                                         >

+                                             <Col componentClass={ControlLabel} sm={3}>

+                                                 Entry Scope Exclude Subtree

+                                             </Col>

+                                             <Col sm={6}>

+                                                 <FormControl

+                                                     type="text"

+                                                     value={configEntryScopeExcludeSubtree}

+                                                     onChange={this.handleFieldChange}

+                                                 />

+                                             </Col>

+                                             <Col sm={3}>

+                                                 <Checkbox

+                                                     id="configSkipNested"

+                                                     checked={configSkipNested}

+                                                     onChange={this.handleCheckboxChange}

+                                                 >

+                                                     Skip Nested

+                                                 </Checkbox>

+                                             </Col>

+                                         </FormGroup>

+                                     </Form>

+                                 </Col>

+                             </Row>

+                             <Row>

+                                 <Col sm={12}>

+                                     <Form horizontal>

+                                         <FormGroup controlId="configAutoAddOC" disabled={false}>

+                                             <Col sm={3}>

+                                                 <ControlLabel>Auto Add OC</ControlLabel>

+                                             </Col>

+                                             <Col sm={9}>

+                                                 <FormControl

+                                                     type="text"

+                                                     value={configAutoAddOC}

+                                                     onChange={this.handleFieldChange}

+                                                 />

+                                             </Col>

+                                         </FormGroup>

+                                     </Form>

+                                 </Col>

+                             </Row>

+                         </Modal.Body>

+                         <Modal.Footer>

+                             <Button

+                                 bsStyle="default"

+                                 className="btn-cancel"

+                                 onClick={this.closeModal}

+                             >

+                                 Cancel

+                             </Button>

+                             <Button

+                                 bsStyle="primary"

+                                 onClick={this.deleteConfig}

+                                 disabled={newEntry}

+                             >

+                                 Delete

+                             </Button>

+                             <Button bsStyle="primary" onClick={this.editConfig} disabled={newEntry}>

+                                 Save

+                             </Button>

+                             <Button bsStyle="primary" onClick={this.addConfig} disabled={!newEntry}>

+                                 Add

+                             </Button>

+                         </Modal.Footer>

+                     </div>

+                 </Modal>

                  <PluginBasicConfig

                      rows={this.props.rows}

                      serverId={this.props.serverId}

                      cn="MemberOf Plugin"

                      pluginName="MemberOf"

                      cmdName="memberof"

+                     specificPluginCMD={specificPluginCMD}

                      savePluginHandler={this.props.savePluginHandler}

                      pluginListHandler={this.props.pluginListHandler}

                      addNotification={this.props.addNotification}

                      toggleLoadingHandler={this.props.toggleLoadingHandler}

-                 />

+                 >

+                     <Row>

+                         <Col sm={9}>

+                             <Form horizontal>

+                                 <FormGroup

+                                     key="memberOfAttr"

+                                     controlId="memberOfAttr"

+                                     disabled={false}

+                                 >

+                                     <Col componentClass={ControlLabel} sm={3}>

+                                         Attribute

+                                     </Col>

+                                     <Col sm={9}>

+                                         <Typeahead

+                                             allowNew

+                                             multiple

+                                             onChange={values => {

+                                                 this.setState({

+                                                     memberOfAttr: values

+                                                 });

+                                             }}

+                                             selected={memberOfAttr}

+                                             newSelectionPrefix="Add a member: "

+                                             options={[

+                                                 {

+                                                     id: "member",

+                                                     label: "member"

+                                                 },

+                                                 {

+                                                     id: "memberCertificate",

+                                                     label: "memberCertificate"

+                                                 },

+                                                 {

+                                                     id: "uniqueMember",

+                                                     label: "uniqueMember"

+                                                 }

+                                             ]}

+                                             placeholder="Type a member attribute..."

+                                         />

+                                     </Col>

+                                 </FormGroup>

+                                 <FormGroup

+                                     key="memberOfGroupAttr"

+                                     controlId="memberOfGroupAttr"

+                                     disabled={false}

+                                 >

+                                     <Col componentClass={ControlLabel} sm={3}>

+                                         Group Attribute

+                                     </Col>

+                                     <Col sm={9}>

+                                         <Typeahead

+                                             allowNew

+                                             multiple

+                                             onChange={values => {

+                                                 this.setState({

+                                                     memberOfGroupAttr: values

+                                                 });

+                                             }}

+                                             selected={memberOfGroupAttr}

+                                             newSelectionPrefix="Add a group member: "

+                                             options={[

+                                                 {

+                                                     id: "groupOfNames",

+                                                     label: "groupOfNames"

+                                                 },

+                                                 {

+                                                     id: "groupOfURLs",

+                                                     label: "groupOfURLs"

+                                                 },

+                                                 {

+                                                     id: "groupOfUniqueNames",

+                                                     label: "groupOfUniqueNames"

+                                                 },

+                                                 {

+                                                     id: "groupOfCertificates",

+                                                     label: "groupOfCertificates"

+                                                 }

+                                             ]}

+                                             placeholder="Type a member group attribute..."

+                                         />

+                                     </Col>

+                                 </FormGroup>

+                             </Form>

+                         </Col>

+                     </Row>

+                     <Row>

+                         <Col sm={9}>

+                             <Form horizontal>

+                                 <FormGroup

+                                     key="memberOfEntryScope"

+                                     controlId="memberOfEntryScope"

+                                     disabled={false}

+                                 >

+                                     <Col componentClass={ControlLabel} sm={3}>

+                                         Entry Scope

+                                     </Col>

+                                     <Col sm={6}>

+                                         <FormControl

+                                             type="text"

+                                             value={memberOfEntryScope}

+                                             onChange={this.handleFieldChange}

+                                         />

+                                     </Col>

+                                     <Col sm={3}>

+                                         <Checkbox

+                                             id="memberOfAllBackends"

+                                             checked={memberOfAllBackends}

+                                             onChange={this.handleCheckboxChange}

+                                         >

+                                             All Backends

+                                         </Checkbox>

+                                     </Col>

+                                 </FormGroup>

+                                 <FormGroup

+                                     key="memberOfEntryScopeExcludeSubtree"

+                                     controlId="memberOfEntryScopeExcludeSubtree"

+                                     disabled={false}

+                                 >

+                                     <Col componentClass={ControlLabel} sm={3}>

+                                         Entry Scope Exclude Subtree

+                                     </Col>

+                                     <Col sm={6}>

+                                         <FormControl

+                                             type="text"

+                                             value={memberOfEntryScopeExcludeSubtree}

+                                             onChange={this.handleFieldChange}

+                                         />

+                                     </Col>

+                                     <Col sm={3}>

+                                         <Checkbox

+                                             id="memberOfSkipNested"

+                                             checked={memberOfSkipNested}

+                                             onChange={this.handleCheckboxChange}

+                                         >

+                                             Skip Nested

+                                         </Checkbox>

+                                     </Col>

+                                 </FormGroup>

+                             </Form>

+                         </Col>

+                     </Row>

+                     <Row>

+                         <Col sm={9}>

+                             <Form horizontal>

+                                 <FormGroup

+                                     key="memberOfConfigEntry"

+                                     controlId="memberOfConfigEntry"

+                                 >

+                                     <Col componentClass={ControlLabel} sm={3}>

+                                         Shared Config Entry

+                                     </Col>

+                                     <Col sm={6}>

+                                         <FormControl

+                                             type="text"

+                                             value={memberOfConfigEntry}

+                                             onChange={this.handleFieldChange}

+                                         />

+                                     </Col>

+                                     <Col sm={3}>

+                                         <Button

+                                             bsSize="large"

+                                             bsStyle="primary"

+                                             onClick={this.openModal}

+                                         >

+                                             Manage

+                                         </Button>

+                                     </Col>

+                                 </FormGroup>

+                             </Form>

+                         </Col>

+                     </Row>

+                     <Row>

+                         <Col sm={9}>

+                             <Form horizontal>

+                                 <FormGroup controlId="memberOfAutoAddOC" disabled={false}>

+                                     <Col sm={3}>

+                                         <ControlLabel>Auto Add OC</ControlLabel>

+                                     </Col>

+                                     <Col sm={9}>

+                                         <FormControl

+                                             type="text"

+                                             value={memberOfAutoAddOC}

+                                             onChange={this.handleFieldChange}

+                                         />

+                                     </Col>

+                                 </FormGroup>

+                             </Form>

+                         </Col>

+                     </Row>

+                     <Row>

+                         <Col sm={9}>

+                             <Button

+                                 bsSize="large"

+                                 bsStyle="primary"

+                                 onClick={this.toggleFixupModal}

+                             >

+                                 Run Fixup Task

+                             </Button>

+                         </Col>

+                     </Row>

+                 </PluginBasicConfig>

              </div>

          );

      }

@@ -43,7 +43,9 @@ 

              currentPluginId: "",

              currentPluginVendor: "",

              currentPluginVersion: "",

-             currentPluginDescription: ""

+             currentPluginDescription: "",

+             currentPluginDependsOnType: "",

+             currentPluginDependsOnNamed: ""

          };

      }

  
@@ -62,9 +64,7 @@ 

              addNotification,

              toggleLoadingHandler

          } = this.props;

-         const new_status = this.state.currentPluginEnabled

-             ? "disable"

-             : "enable";

+         const new_status = this.state.currentPluginEnabled ? "disable" : "enable";

          const cmd = [

              "dsconf",

              "-j",
@@ -76,11 +76,7 @@ 

  

          toggleLoadingHandler();

          this.setState({ disableSwitch: true });

-         log_cmd(

-             "handleSwitchChange",

-             "Switch plugin states from the plugin tab",

-             cmd

-         );

+         log_cmd("handleSwitchChange", "Switch plugin states from the plugin tab", cmd);

          cockpit

                  .spawn(cmd, { superuser: true, err: "message" })

                  .done(content => {
@@ -88,7 +84,8 @@ 

                      pluginListHandler();

                      addNotification(

                          "success",

-                         `${pluginName} plugin was successfully ${new_status}d`

+                         `${pluginName} plugin was successfully ${new_status}d.

+                         Please, restart the instance.`

                      );

                      toggleLoadingHandler();

                  })
@@ -104,9 +101,7 @@ 

  

      updateFields() {

          if (this.props.rows.length > 0) {

-             const pluginRow = this.props.rows.find(

-                 row => row.cn[0] === this.props.cn

-             );

+             const pluginRow = this.props.rows.find(row => row.cn[0] === this.props.cn);

  

              this.setState({

                  currentPluginType: pluginRow["nsslapd-pluginType"][0],
@@ -115,8 +110,15 @@ 

                  currentPluginId: pluginRow["nsslapd-pluginId"][0],

                  currentPluginVendor: pluginRow["nsslapd-pluginVendor"][0],

                  currentPluginVersion: pluginRow["nsslapd-pluginVersion"][0],

-                 currentPluginDescription:

-                     pluginRow["nsslapd-pluginDescription"][0]

+                 currentPluginDescription: pluginRow["nsslapd-pluginDescription"][0],

+                 currentPluginDependsOnType:

+                     pluginRow["nsslapd-plugin-depends-on-type"] === undefined

+                         ? ""

+                         : pluginRow["nsslapd-plugin-depends-on-type"][0],

+                 currentPluginDependsOnNamed:

+                     pluginRow["nsslapd-plugin-depends-on-named"] === undefined

+                         ? ""

+                         : pluginRow["nsslapd-plugin-depends-on-named"][0]

              });

          }

          this.updateSwitch();
@@ -124,9 +126,7 @@ 

  

      updateSwitch() {

          if (this.props.rows.length > 0) {

-             const pluginRow = this.props.rows.find(

-                 row => row.cn[0] === this.props.cn

-             );

+             const pluginRow = this.props.rows.find(row => row.cn[0] === this.props.cn);

  

              var pluginEnabled;

              if (pluginRow["nsslapd-pluginEnabled"][0] === "on") {
@@ -158,19 +158,21 @@ 

              currentPluginVendor,

              currentPluginVersion,

              currentPluginDescription,

+             currentPluginDependsOnType,

+             currentPluginDependsOnNamed,

              disableSwitch

          } = this.state;

  

          const modalFieldsCol1 = {

              currentPluginType: this.state.currentPluginType,

              currentPluginPath: this.state.currentPluginPath,

-             currentPluginInitfunc: this.state.currentPluginInitfunc,

-             currentPluginId: this.state.currentPluginId

+             currentPluginInitfunc: this.state.currentPluginInitfunc

          };

          const modalFieldsCol2 = {

              currentPluginVendor: this.state.currentPluginVendor,

              currentPluginVersion: this.state.currentPluginVersion,

-             currentPluginDescription: this.state.currentPluginDescription

+             currentPluginDescription: this.state.currentPluginDescription,

+             currentPluginId: this.state.currentPluginId

          };

          return (

              <div>
@@ -184,11 +186,10 @@ 

                              </h3>

                          </Col>

                          <Col smOffset={1} sm={3}>

-                             <FormGroup

-                                 key="switchPluginStatus"

-                                 controlId="switchPluginStatus"

-                             >

-                                 <ControlLabel className="toolbar-pf-find ds-float-left ds-right-indent">

+                             <FormGroup key="switchPluginStatus" controlId="switchPluginStatus">

+                                 <ControlLabel

+                                     className="toolbar-pf-find ds-float-left ds-right-indent"

+                                 >

                                      Status

                                  </ControlLabel>

                                  <Switch
@@ -196,11 +197,7 @@ 

                                      title="normal"

                                      id="bsSize-example"

                                      value={currentPluginEnabled}

-                                     onChange={() =>

-                                         this.handleSwitchChange(

-                                             currentPluginEnabled

-                                         )

-                                     }

+                                     onChange={() => this.handleSwitchChange(currentPluginEnabled)}

                                      animate={false}

                                      disabled={disableSwitch}

                                  />
@@ -213,94 +210,100 @@ 

                      <Row>

                          <Col sm={4}>

                              <Form horizontal>

-                                 {Object.entries(modalFieldsCol1).map(

-                                     ([id, value]) => (

-                                         <FormGroup

-                                             key={id}

-                                             controlId={id}

-                                             disabled={false}

-                                         >

-                                             <Col

-                                                 componentClass={ControlLabel}

-                                                 sm={4}

-                                             >

-                                                 Plugin{" "}

-                                                 {id.replace(

-                                                     "currentPlugin",

-                                                     ""

-                                                 )}

-                                             </Col>

-                                             <Col sm={8}>

-                                                 <FormControl

-                                                     type="text"

-                                                     value={value}

-                                                     onChange={

-                                                         this.handleFieldChange

-                                                     }

-                                                 />

-                                             </Col>

-                                         </FormGroup>

-                                     )

-                                 )}

+                                 {Object.entries(modalFieldsCol1).map(([id, value]) => (

+                                     <FormGroup key={id} controlId={id} disabled={false}>

+                                         <Col componentClass={ControlLabel} sm={6}>

+                                             {this.props.memberOfAttr} Plugin{" "}

+                                             {id.replace("currentPlugin", "")}

+                                         </Col>

+                                         <Col sm={6}>

+                                             <FormControl

+                                                 type="text"

+                                                 value={value}

+                                                 onChange={this.handleFieldChange}

+                                             />

+                                         </Col>

+                                     </FormGroup>

+                                 ))}

+                                 <FormGroup

+                                     key="currentPluginDependsOnType"

+                                     controlId="currentPluginDependsOnType"

+                                     disabled={false}

+                                 >

+                                     <Col componentClass={ControlLabel} sm={6}>

+                                         Plugin Depends On Type

+                                     </Col>

+                                     <Col sm={6}>

+                                         <FormControl

+                                             type="text"

+                                             value={this.state.currentPluginDependsOnType}

+                                             onChange={this.handleFieldChange}

+                                         />

+                                     </Col>

+                                 </FormGroup>

+                                 <FormGroup

+                                     key="currentPluginDependsOnNamed"

+                                     controlId="currentPluginDependsOnNamed"

+                                     disabled={false}

+                                 >

+                                     <Col componentClass={ControlLabel} sm={6}>

+                                         Plugin Depends On Named

+                                     </Col>

+                                     <Col sm={6}>

+                                         <FormControl

+                                             type="text"

+                                             value={this.state.currentPluginDependsOnNamed}

+                                             onChange={this.handleFieldChange}

+                                         />

+                                     </Col>

+                                 </FormGroup>

                              </Form>

                          </Col>

                          <Col sm={4}>

                              <Form horizontal>

-                                 {Object.entries(modalFieldsCol2).map(

-                                     ([id, value]) => (

-                                         <FormGroup

-                                             key={id}

-                                             controlId={id}

-                                             disabled={false}

-                                         >

-                                             <Col

-                                                 componentClass={ControlLabel}

-                                                 sm={4}

-                                             >

-                                                 Plugin{" "}

-                                                 {id.replace(

-                                                     "currentPlugin",

-                                                     ""

-                                                 )}

-                                             </Col>

-                                             <Col sm={8}>

-                                                 <FormControl

-                                                     type="text"

-                                                     value={value}

-                                                     onChange={

-                                                         this.handleFieldChange

-                                                     }

-                                                 />

-                                             </Col>

-                                         </FormGroup>

-                                     )

-                                 )}

+                                 {Object.entries(modalFieldsCol2).map(([id, value]) => (

+                                     <FormGroup key={id} controlId={id} disabled={false}>

+                                         <Col componentClass={ControlLabel} sm={5}>

+                                             Plugin {id.replace("currentPlugin", "")}

+                                         </Col>

+                                         <Col sm={7}>

+                                             <FormControl

+                                                 type="text"

+                                                 value={value}

+                                                 onChange={this.handleFieldChange}

+                                             />

+                                         </Col>

+                                     </FormGroup>

+                                 ))}

                              </Form>

                          </Col>

                      </Row>

-                     <Row>

-                         <Col smOffset={7} sm={1}>

-                             <Button

-                                 bsSize="large"

-                                 bsStyle="primary"

-                                 onClick={() =>

-                                     this.props.savePluginHandler({

-                                         name: this.props.cn,

-                                         type: currentPluginType,

-                                         path: currentPluginPath,

-                                         initfunc: currentPluginInitfunc,

-                                         id: currentPluginId,

-                                         vendor: currentPluginVendor,

-                                         version: currentPluginVersion,

-                                         description: currentPluginDescription

-                                     })

-                                 }

-                             >

-                                 Save Config

-                             </Button>

-                         </Col>

-                     </Row>

                  </CustomCollapse>

+                 <Row>

+                     <Col smOffset={7} sm={1}>

+                         <Button

+                             bsSize="large"

+                             bsStyle="primary"

+                             onClick={() =>

+                                 this.props.savePluginHandler({

+                                     name: this.props.cn,

+                                     type: currentPluginType,

+                                     path: currentPluginPath,

+                                     initfunc: currentPluginInitfunc,

+                                     id: currentPluginId,

+                                     vendor: currentPluginVendor,

+                                     version: currentPluginVersion,

+                                     description: currentPluginDescription,

+                                     dependsOnType: currentPluginDependsOnType,

+                                     dependsOnNamed: currentPluginDependsOnNamed,

+                                     specificPluginCMD: this.props.specificPluginCMD

+                                 })

+                             }

+                         >

+                             Save Config

+                         </Button>

+                     </Col>

+                 </Row>

              </div>

          );

      }
@@ -313,6 +316,7 @@ 

      cn: PropTypes.string,

      pluginName: PropTypes.string,

      cmdName: PropTypes.string,

+     specificPluginCMD: PropTypes.array,

      savePluginHandler: PropTypes.func,

      pluginListHandler: PropTypes.func,

      addNotification: PropTypes.func,
@@ -325,6 +329,7 @@ 

      cn: "",

      pluginName: "",

      cmdName: "",

+     specificPluginCMD: [],

      savePluginHandler: noop,

      pluginListHandler: noop,

      addNotification: noop,

@@ -16,18 +16,14 @@ 

  

  class PluginEditModal extends React.Component {

      render() {

-         const modalFields = {

-             currentPluginType: this.props.currentPluginType,

-             currentPluginPath: this.props.currentPluginPath,

-             currentPluginInitfunc: this.props.currentPluginInitfunc,

-             currentPluginId: this.props.currentPluginId,

-             currentPluginVendor: this.props.currentPluginVendor,

-             currentPluginVersion: this.props.currentPluginVersion,

-             currentPluginDescription: this.props.currentPluginDescription

-         };

          const {

              showModal,

              closeHandler,

+             handleChange,

+             handleSwitchChange,

+             savePluginHandler

+         } = this.props;

+         const {

              currentPluginName,

              currentPluginEnabled,

              currentPluginType,
@@ -37,10 +33,19 @@ 

              currentPluginVendor,

              currentPluginVersion,

              currentPluginDescription,

-             handleChange,

-             handleSwitchChange,

-             savePluginHandler

-         } = this.props;

+             currentPluginDependsOnType,

+             currentPluginDependsOnNamed

+         } = this.props.pluginData;

+         const modalFields = {

+             currentPluginType: currentPluginType,

+             currentPluginPath: currentPluginPath,

+             currentPluginInitfunc: currentPluginInitfunc,

+             currentPluginId: currentPluginId,

+             currentPluginVendor: currentPluginVendor,

+             currentPluginVersion: currentPluginVersion,

+             currentPluginDescription: currentPluginDescription

+         };

+ 

          return (

              <Modal show={showModal} onHide={closeHandler}>

                  <div className="ds-no-horizontal-scrollbar">
@@ -53,9 +58,7 @@ 

                          >

                              <Icon type="pf" name="close" />

                          </button>

-                         <Modal.Title>

-                             Edit Plugin - {currentPluginName}

-                         </Modal.Title>

+                         <Modal.Title>Edit Plugin - {currentPluginName}</Modal.Title>

                      </Modal.Header>

                      <Modal.Body>

                          <Form horizontal>
@@ -64,34 +67,26 @@ 

                                  controlId="currentPluginEnabled"

                                  disabled={false}

                              >

-                                 <Col componentClass={ControlLabel} sm={4}>

+                                 <Col componentClass={ControlLabel} sm={6}>

                                      Plugin Status

                                  </Col>

-                                 <Col sm={7}>

+                                 <Col sm={6}>

                                      <Switch

                                          bsSize="normal"

                                          title="normal"

                                          id="pluginEnableSwitch"

                                          value={currentPluginEnabled}

-                                         onChange={() =>

-                                             handleSwitchChange(

-                                                 currentPluginEnabled

-                                             )

-                                         }

+                                         onChange={() => handleSwitchChange(currentPluginEnabled)}

                                          animate={false}

                                      />

                                  </Col>

                              </FormGroup>

                              {Object.entries(modalFields).map(([id, value]) => (

-                                 <FormGroup

-                                     key={id}

-                                     controlId={id}

-                                     disabled={false}

-                                 >

-                                     <Col componentClass={ControlLabel} sm={4}>

+                                 <FormGroup key={id} controlId={id} disabled={false}>

+                                     <Col componentClass={ControlLabel} sm={6}>

                                          Plugin {id.replace("currentPlugin", "")}

                                      </Col>

-                                     <Col sm={7}>

+                                     <Col sm={6}>

                                          <FormControl

                                              type="text"

                                              value={value}
@@ -100,29 +95,61 @@ 

                                      </Col>

                                  </FormGroup>

                              ))}

+                             <FormGroup

+                                 key="currentPluginDependsOnType"

+                                 controlId="currentPluginDependsOnType"

+                                 disabled={false}

+                             >

+                                 <Col componentClass={ControlLabel} sm={6}>

+                                     Plugin Depends On Type

+                                 </Col>

+                                 <Col sm={6}>

+                                     <FormControl

+                                         type="text"

+                                         value={currentPluginDependsOnType}

+                                         onChange={handleChange}

+                                     />

+                                 </Col>

+                             </FormGroup>

+                             <FormGroup

+                                 key="currentPluginDependsOnNamed"

+                                 controlId="currentPluginDependsOnNamed"

+                                 disabled={false}

+                             >

+                                 <Col componentClass={ControlLabel} sm={6}>

+                                     Plugin Depends On Type

+                                 </Col>

+                                 <Col sm={6}>

+                                     <FormControl

+                                         type="text"

+                                         value={currentPluginDependsOnNamed}

+                                         onChange={handleChange}

+                                     />

+                                 </Col>

+                             </FormGroup>

                          </Form>

                      </Modal.Body>

-                     <Modal.Footer className="ds-modal-footer">

-                         <Button

-                             bsStyle="default"

-                             className="btn-cancel"

-                             onClick={closeHandler}

-                         >

+                     <Modal.Footer>

+                         <Button bsStyle="default" className="btn-cancel" onClick={closeHandler}>

                              Cancel

                          </Button>

                          <Button

                              bsStyle="primary"

-                             onClick={() => savePluginHandler({

-                                 name: currentPluginName,

-                                 enabled: currentPluginEnabled,

-                                 type: currentPluginType,

-                                 path: currentPluginPath,

-                                 initfunc: currentPluginInitfunc,

-                                 id: currentPluginId,

-                                 vendor: currentPluginVendor,

-                                 version: currentPluginVersion,

-                                 description: currentPluginDescription

-                             })}

+                             onClick={() =>

+                                 savePluginHandler({

+                                     name: currentPluginName,

+                                     enabled: currentPluginEnabled,

+                                     type: currentPluginType,

+                                     path: currentPluginPath,

+                                     initfunc: currentPluginInitfunc,

+                                     id: currentPluginId,

+                                     vendor: currentPluginVendor,

+                                     version: currentPluginVersion,

+                                     description: currentPluginDescription,

+                                     dependsOnType: currentPluginDependsOnType,

+                                     dependsOnNamed: currentPluginDependsOnNamed

+                                 })

+                             }

                          >

                              Save

                          </Button>
@@ -136,15 +163,19 @@ 

  PluginEditModal.propTypes = {

      handleChange: PropTypes.func,

      handleSwitchChange: PropTypes.func,

-     currentPluginName: PropTypes.string,

-     currentPluginType: PropTypes.string,

-     currentPluginEnabled: PropTypes.bool,

-     currentPluginPath: PropTypes.string,

-     currentPluginInitfunc: PropTypes.string,

-     currentPluginId: PropTypes.string,

-     currentPluginVendor: PropTypes.string,

-     currentPluginVersion: PropTypes.string,

-     currentPluginDescription: PropTypes.string,

+     pluginData: PropTypes.exact({

+         currentPluginName: PropTypes.string,

+         currentPluginType: PropTypes.string,

+         currentPluginEnabled: PropTypes.bool,

+         currentPluginPath: PropTypes.string,

+         currentPluginInitfunc: PropTypes.string,

+         currentPluginId: PropTypes.string,

+         currentPluginVendor: PropTypes.string,

+         currentPluginVersion: PropTypes.string,

+         currentPluginDescription: PropTypes.string,

+         currentPluginDependsOnType: PropTypes.string,

+         currentPluginDependsOnNamed: PropTypes.string

+     }),

      closeHandler: PropTypes.func,

      savePluginHandler: PropTypes.func,

      showModal: PropTypes.bool
@@ -153,15 +184,19 @@ 

  PluginEditModal.defaultProps = {

      handleChange: noop,

      handleSwitchChange: noop,

-     currentPluginName: "",

-     currentPluginType: "",

-     currentPluginEnabled: false,

-     currentPluginPath: "",

-     currentPluginInitfunc: "",

-     currentPluginId: "",

-     currentPluginVendor: "",

-     currentPluginVersion: "",

-     currentPluginDescription: "",

+     pluginData: {

+         currentPluginName: "",

+         currentPluginType: "",

+         currentPluginEnabled: false,

+         currentPluginPath: "",

+         currentPluginInitfunc: "",

+         currentPluginId: "",

+         currentPluginVendor: "",

+         currentPluginVersion: "",

+         currentPluginDescription: "",

+         currentPluginDependsOnType: "",

+         currentPluginDependsOnNamed: ""

+     },

      closeHandler: noop,

      savePluginHandler: noop,

      showModal: false

@@ -69,7 +69,9 @@ 

              currentPluginId: "",

              currentPluginVendor: "",

              currentPluginVersion: "",

-             currentPluginDescription: ""

+             currentPluginDescription: "",

+             currentPluginDependsOnType: "",

+             currentPluginDependsOnNamed: ""

          };

      }

  
@@ -147,6 +149,14 @@ 

              currentPluginVendor: rowData["nsslapd-pluginVendor"][0],

              currentPluginVersion: rowData["nsslapd-pluginVersion"][0],

              currentPluginDescription: rowData["nsslapd-pluginDescription"][0],

+             currentPluginDependsOnType:

+                 rowData["nsslapd-plugin-depends-on-type"] === undefined

+                     ? ""

+                     : rowData["nsslapd-plugin-depends-on-type"][0],

+             currentPluginDependsOnNamed:

+                 rowData["nsslapd-plugin-depends-on-named"] === undefined

+                     ? ""

+                     : rowData["nsslapd-plugin-depends-on-named"][0],

              showPluginModal: true

          });

      }
@@ -179,7 +189,9 @@ 

      }

  

      savePlugin(data) {

-         cmd = [

+         let nothingToSetErr = false;

+         let basicPluginSuccess = false;

+         let cmd = [

              "dsconf",

              "-j",

              "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
@@ -187,19 +199,23 @@ 

              "edit",

              data.name,

              "--type",

-             data.type,

+             data.type || "delete",

              "--path",

-             data.path,

+             data.path || "delete",

              "--initfunc",

-             data.initfunc,

+             data.initfunc || "delete",

              "--id",

-             data.id,

+             data.id || "delete",

              "--vendor",

-             data.vendor,

+             data.vendor || "delete",

              "--version",

-             data.version,

+             data.version || "delete",

              "--description",

-             data.description

+             data.description || "delete",

+             "--depends-on-type",

+             data.dependsOnType || "delete",

+             "--depends-on-named",

+             data.dependsOnNamed || "delete"

          ];

  

          if ("enabled" in data) {
@@ -207,26 +223,80 @@ 

          }

  

          this.toggleLoading();

-         log_cmd("savePlugin", "Edit the plugin from the modal form", cmd);

+ 

+         log_cmd("savePlugin", "Edit the plugin", cmd);

          cockpit

                  .spawn(cmd, { superuser: true, err: "message" })

                  .done(content => {

                      console.info("savePlugin", "Result", content);

+                     basicPluginSuccess = true;

                      this.addNotification(

                          "success",

                          `Plugin ${data.name} was successfully modified`

                      );

-                     this.pluginList();

                      this.closePluginModal();

                      this.toggleLoading();

                  })

                  .fail(err => {

-                     this.addNotification(

-                         "error",

-                         `Error during plugin ${data.name} modification - ${err}`

-                     );

+                     if (err.message.indexOf("nothing to set") >= 0) {

+                         nothingToSetErr = true;

+                     } else {

+                         this.addNotification(

+                             "error",

+                             `${err.message} error during ${data.name} modification`

+                         );

+                     }

                      this.closePluginModal();

                      this.toggleLoading();

+                 })

+                 .always(() => {

+                     if ("specificPluginCMD" in data) {

+                         this.toggleLoading();

+                         log_cmd(

+                             "savePlugin",

+                             "Edit the plugin from the plugin config tab",

+                             data.specificPluginCMD

+                         );

+                         cockpit

+                                 .spawn(data.specificPluginCMD, {

+                                     superuser: true,

+                                     err: "message"

+                                 })

+                                 .done(content => {

+                                     // Notify success only one time

+                                     if (!basicPluginSuccess) {

+                                         this.addNotification(

+                                             "success",

+                                             `Plugin ${data.name} was successfully modified`

+                                         );

+                                     }

+                                     this.pluginList();

+                                     this.toggleLoading();

+                                     console.info("savePlugin", "Result", content);

+                                 })

+                                 .fail(err => {

+                                     if (

+                                         (err.message.indexOf(

+                                             "nothing to set"

+                                         ) >= 0 &&

+                                     nothingToSetErr) ||

+                                 err.message.indexOf("nothing to set") < 0

+                                     ) {

+                                         if (basicPluginSuccess) {

+                                             this.addNotification(

+                                                 "success",

+                                                 `Plugin ${data.name} was successfully modified`

+                                             );

+                                             this.pluginList();

+                                         }

+                                         this.addNotification(

+                                             "error",

+                                             `${err.message} error during ${data.name} modification`

+                                         );

+                                     }

+                                     this.toggleLoading();

+                                 });

+                     }

                  });

      }

  
@@ -235,10 +305,7 @@ 

              allPlugins: {

                  name: "All Plugins",

                  component: (

-                     <PluginTable

-                         rows={this.state.rows}

-                         loadModalHandler={this.openPluginModal}

-                     />

+                     <PluginTable rows={this.state.rows} loadModalHandler={this.openPluginModal} />

                  )

              },

              accountPolicy: {
@@ -424,24 +491,20 @@ 

                      <Row className="clearfix">

                          <Col sm={2}>

                              <Nav bsStyle="pills" stacked>

-                                 {Object.entries(selectPlugins).map(

-                                     ([id, item]) => (

-                                         <NavItem key={id} eventKey={id}>

-                                             {item.name}

-                                         </NavItem>

-                                     )

-                                 )}

+                                 {Object.entries(selectPlugins).map(([id, item]) => (

+                                     <NavItem key={id} eventKey={id}>

+                                         {item.name}

+                                     </NavItem>

+                                 ))}

                              </Nav>

                          </Col>

                          <Col sm={10}>

                              <Tab.Content animation={false}>

-                                 {Object.entries(selectPlugins).map(

-                                     ([id, item]) => (

-                                         <Tab.Pane key={id} eventKey={id}>

-                                             {item.component}

-                                         </Tab.Pane>

-                                     )

-                                 )}

+                                 {Object.entries(selectPlugins).map(([id, item]) => (

+                                     <Tab.Pane key={id} eventKey={id}>

+                                         {item.component}

+                                     </Tab.Pane>

+                                 ))}

                              </Tab.Content>

                          </Col>

                      </Row>
@@ -449,17 +512,19 @@ 

                  <PluginEditModal

                      handleChange={this.handleFieldChange}

                      handleSwitchChange={this.handleSwitchChange}

-                     currentPluginName={this.state.currentPluginName}

-                     currentPluginType={this.state.currentPluginType}

-                     currentPluginEnabled={this.state.currentPluginEnabled}

-                     currentPluginPath={this.state.currentPluginPath}

-                     currentPluginInitfunc={this.state.currentPluginInitfunc}

-                     currentPluginId={this.state.currentPluginId}

-                     currentPluginVendor={this.state.currentPluginVendor}

-                     currentPluginVersion={this.state.currentPluginVersion}

-                     currentPluginDescription={

-                         this.state.currentPluginDescription

-                     }

+                     pluginData={{

+                         currentPluginName: this.state.currentPluginName,

+                         currentPluginType: this.state.currentPluginType,

+                         currentPluginEnabled: this.state.currentPluginEnabled,

+                         currentPluginPath: this.state.currentPluginPath,

+                         currentPluginInitfunc: this.state.currentPluginInitfunc,

+                         currentPluginId: this.state.currentPluginId,

+                         currentPluginVendor: this.state.currentPluginVendor,

+                         currentPluginVersion: this.state.currentPluginVersion,

+                         currentPluginDescription: this.state.currentPluginDescription,

+                         currentPluginDependsOnType: this.state.currentPluginDependsOnType,

+                         currentPluginDependsOnNamed: this.state.currentPluginDependsOnNamed

+                     }}

                      closeHandler={this.closePluginModal}

                      showModal={this.state.showPluginModal}

                      savePluginHandler={this.savePlugin}

@@ -0,0 +1,256 @@ 

+ .rbt {

+   outline: none;

+ }

+ 

+ .rbt-menu {

+   margin-bottom: 2px;

+ }

+ 

+ .rbt-menu > li a {

+   overflow: hidden;

+   text-overflow: ellipsis;

+ }

+ 

+ .rbt-menu > li a:focus {

+   outline: none;

+ }

+ 

+ .rbt-menu-pagination-option {

+   text-align: center;

+ }

+ 

+ .rbt .rbt-input-main::-ms-clear {

+   display: none;

+ }

+ 

+ .rbt-input-multi {

+   cursor: text;

+   overflow: hidden;

+   position: relative;

+   height: auto;

+ }

+ 

+ .rbt-input-multi.focus {

+   -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);

+   -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);

+   box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);

+   border-color: #66afe9;

+   outline: 0;

+ }

+ 

+ .rbt-input-multi.form-control[disabled] {

+   background-color: #e9ecef;

+   opacity: 1;

+ }

+ 

+ .rbt-input-multi input::-moz-placeholder {

+   color: #999;

+   opacity: 1;

+ }

+ 

+ .rbt-input-multi input:-ms-input-placeholder {

+   color: #999;

+ }

+ 

+ .rbt-input-multi input::-webkit-input-placeholder {

+   color: #999;

+ }

+ 

+ .rbt-input-multi .rbt-input-wrapper {

+   margin-bottom: -4px;

+   margin-top: -1px;

+   overflow: hidden;

+ }

+ 

+ .rbt-input-multi .rbt-input-main {

+   height: 20px;

+   margin: 1px 0 4px;

+ }

+ 

+ .rbt-input-multi .rbt-input-hint-container {

+   display: inline-block;

+ }

+ 

+ .rbt-input-multi.input-lg .rbt-input-main, .rbt-input-multi.form-control-lg .rbt-input-main {

+   height: 24px;

+ }

+ 

+ .rbt-input-multi.input-sm .rbt-input-main, .rbt-input-multi.form-control-sm .rbt-input-main {

+   height: 18px;

+ }

+ 

+ .rbt-close {

+   z-index: 1;

+ }

+ 

+ .rbt-close-lg {

+   font-size: 24px;

+ }

+ 

+ .rbt-token {

+   background-color: #e7f4ff;

+   border: 0;

+   border-radius: 2px;

+   color: #1f8dd6;

+   display: inline-block;

+   line-height: 1em;

+   margin: 0 3px 3px 0;

+   padding: 4px 7px;

+   position: relative;

+ }

+ 

+ .rbt-token-disabled {

+   background-color: #ddd;

+   color: #888;

+   pointer-events: none;

+ }

+ 

+ .rbt-token-removeable {

+   cursor: pointer;

+   padding-right: 21px;

+ }

+ 

+ .rbt-token-active {

+   background-color: #1f8dd6;

+   color: #fff;

+   outline: none;

+   text-decoration: none;

+ }

+ 

+ .rbt-token .rbt-token-remove-button {

+   bottom: 0;

+   color: inherit;

+   font-size: inherit;

+   font-weight: normal;

+   opacity: 1;

+   outline: none;

+   padding: 3px 7px;

+   position: absolute;

+   right: 0;

+   text-shadow: none;

+   top: -2px;

+ }

+ 

+ .rbt-loader {

+   -webkit-animation: loader-animation 600ms infinite linear;

+   -o-animation: loader-animation 600ms infinite linear;

+   animation: loader-animation 600ms infinite linear;

+   border: 1px solid #d5d5d5;

+   border-radius: 50%;

+   border-top-color: #1f8dd6;

+   display: block;

+   height: 16px;

+   width: 16px;

+ }

+ 

+ .rbt-loader-lg {

+   height: 20px;

+   width: 20px;

+ }

+ 

+ .rbt-aux {

+   display: -webkit-box;

+   display: -moz-box;

+   display: -ms-flexbox;

+   display: -webkit-flex;

+   display: flex;

+   align-items: center;

+   bottom: 0;

+   justify-content: center;

+   pointer-events: none;

+   /* Don't block clicks on the input */

+   position: absolute;

+   right: 0;

+   top: 0;

+   width: 34px;

+ }

+ 

+ .rbt-aux-lg {

+   width: 46px;

+ }

+ 

+ .rbt-aux .rbt-close {

+   margin-top: -4px;

+   pointer-events: auto;

+   /* Override pointer-events: none; above */

+ }

+ 

+ .has-aux .rbt-input {

+   padding-right: 34px;

+ }

+ 

+ .rbt-highlight-text {

+   background-color: inherit;

+   color: inherit;

+   font-weight: bold;

+   padding: 0;

+ }

+ 

+ /* Input Groups */

+ .input-group > .rbt {

+   -webkit-box-flex: 1;

+   -moz-box-flex: 1;

+   -webkit-flex: 1;

+   -ms-flex: 1;

+   flex: 1;

+ }

+ 

+ .input-group > .rbt .rbt-input-hint-container {

+   display: -webkit-box;

+   display: -moz-box;

+   display: -ms-flexbox;

+   display: -webkit-flex;

+   display: flex;

+ }

+ 

+ .input-group > .rbt .rbt-input-hint {

+   z-index: 5;

+ }

+ 

+ .input-group > .rbt .rbt-aux {

+   z-index: 5;

+ }

+ 

+ .input-group > .rbt:not(:first-child) .form-control {

+   border-top-left-radius: 0;

+   border-bottom-left-radius: 0;

+ }

+ 

+ .input-group > .rbt:not(:last-child) .form-control {

+   border-top-right-radius: 0;

+   border-bottom-right-radius: 0;

+ }

+ 

+ /* Validation States */

+ .has-error .rbt-input-multi.focus {

+   border-color: #843534;

+   -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;

+   -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;

+   box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;

+ }

+ 

+ .has-warning .rbt-input-multi.focus {

+   border-color: #66512c;

+   -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;

+   -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;

+   box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;

+ }

+ 

+ .has-success .rbt-input-multi.focus {

+   border-color: #2b542c;

+   -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;

+   -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;

+   box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;

+ }

+ 

+ @keyframes loader-animation {

+   to {

+     transform: rotate(1turn);

+   }

+ }

+ 

+ @-webkit-keyframes loader-animation {

+   to {

+     -webkit-transform: rotate(1turn);

+   }

+ }

@@ -6,24 +6,22 @@ 

  const CompressionPlugin = require("compression-webpack-plugin");

  

  var externals = {

-     "cockpit": "cockpit",

+     cockpit: "cockpit"

  };

  

  /* These can be overridden, typically from the Makefile.am */

  const srcdir = (process.env.SRCDIR || __dirname) + path.sep + "src";

- const builddir = (process.env.SRCDIR || __dirname);

+ const builddir = process.env.SRCDIR || __dirname;

  const distdir = builddir + path.sep + "dist";

  const section = process.env.ONLYDIR || null;

- const nodedir = path.resolve((process.env.SRCDIR || __dirname), "node_modules");

+ const nodedir = path.resolve(process.env.SRCDIR || __dirname, "node_modules");

  

  /* A standard nodejs and webpack pattern */

- var production = process.env.NODE_ENV === 'production';

+ var production = process.env.NODE_ENV === "production";

  

  var info = {

      entries: {

-         "index": [

-             "./index.es6"

-         ]

+         index: ["./index.es6"]

      },

      files: [

          "backend.html",
@@ -46,14 +44,14 @@ 

          "servers.html",

          "servers.js",

          "static",

-         "manifest.json",

-     ],

+         "manifest.json"

+     ]

  };

  

  var output = {

      path: distdir,

      filename: "[name].js",

-     sourceMapFilename: "[file].map",

+     sourceMapFilename: "[file].map"

  };

  

  /*
@@ -67,8 +65,7 @@ 

  function vpath(/* ... */) {

      var filename = Array.prototype.join.call(arguments, path.sep);

      var expanded = builddir + path.sep + filename;

-     if (fs.existsSync(expanded))

-         return expanded;

+     if (fs.existsSync(expanded)) return expanded;

      expanded = srcdir + path.sep + filename;

      return expanded;

  }
@@ -81,10 +78,8 @@ 

      }

  

      info.entries[key] = info.entries[key].map(function(value) {

-         if (value.indexOf("/") === -1)

-             return value;

-         else

-             return vpath(value);

+         if (value.indexOf("/") === -1) return value;

+         else return vpath(value);

      });

  });

  
@@ -96,26 +91,25 @@ 

  });

  info.files = files;

  

- var plugins = [

-     new copy(info.files),

-     new extract("[name].css")

- ];

+ var plugins = [new copy(info.files), new extract("[name].css")];

  

  /* Only minimize when in production mode */

  if (production) {

      /* Rename output files when minimizing */

      output.filename = "[name].min.js";

  

-     plugins.unshift(new CompressionPlugin({

-         asset: "[path].gz[query]",

-         test: /\.(js|html)$/,

-         minRatio: 0.9,

-         deleteOriginalAssets: true

-     }));

+     plugins.unshift(

+         new CompressionPlugin({

+             asset: "[path].gz[query]",

+             test: /\.(js|html)$/,

+             minRatio: 0.9,

+             deleteOriginalAssets: true

+         })

+     );

  }

  

  module.exports = {

-     mode: production ? 'production' : 'development',

+     mode: production ? "production" : "development",

      entry: info.entries,

      externals: externals,

      output: output,
@@ -123,42 +117,44 @@ 

      module: {

          rules: [

              {

-                 enforce: 'pre',

+                 enforce: "pre",

                  exclude: /node_modules/,

-                 loader: 'eslint-loader',

+                 loader: "eslint-loader",

                  test: /\.jsx$/

              },

              {

-                 enforce: 'pre',

+                 enforce: "pre",

                  exclude: /node_modules/,

-                 loader: 'eslint-loader',

+                 loader: "eslint-loader",

                  test: /\.es6$/

              },

              {

                  exclude: /node_modules/,

-                 loader: 'babel-loader',

+                 loader: "babel-loader",

                  test: /\.js$/

              },

              {

                  exclude: /node_modules/,

-                 loader: 'babel-loader',

+                 loader: "babel-loader",

                  test: /\.jsx$/

              },

              {

                  exclude: /node_modules/,

-                 loader: 'babel-loader',

+                 loader: "babel-loader",

                  test: /\.es6$/

              },

              {

+                   // Transform our own .css files with PostCSS and CSS-modules

+                 test: /\.css$/,

                  exclude: /node_modules/,

-                 loader: extract.extract('css-loader!sass-loader'),

-                 test: /\.scss$/

+                 use: ['style-loader', 'css-loader'],

              },

              {

                  test: /\.css$/,

-                 use: [ 'style-loader', 'css-loader' ]

+                 include: /node_modules/,

+                 use: ['style-loader', 'css-loader'],

              }

          ]

      },

      plugins: plugins

- }

+ };

@@ -270,7 +270,14 @@ 

          :param *args: tuples of key,value to replace.

          :type *args: (str, str)

          """

-         mods = list(map(lambda t: (ldap.MOD_REPLACE, ensure_str(t[0]), ensure_bytes(t[1])), args))

+ 

+         mods = []

+         for arg in args:

+             if isinstance(arg[1], list):

+                 value = ensure_list_bytes(arg[1])

+             else:

+                 value = [ensure_bytes(arg[1])]

+             mods.append((ldap.MOD_REPLACE, ensure_str(arg[0]), value))

          return self._instance.modify_ext_s(self._dn, mods, serverctrls=self._server_controls, clientctrls=self._client_controls)

  

      # This needs to work on key + val, and key
@@ -371,7 +378,7 @@ 

      def apply_mods(self, mods):

          """Perform modification operation using several mods at once

  

-         :param mods: [(action, key, value),]

+         :param mods: [(action, key, value),] or [(ldap.MOD_DELETE, key),]

          :type mods: list of tuples

          :raises: ValueError - if a provided mod op is invalid

          """
@@ -381,25 +388,25 @@ 

              if len(mod) < 2:

                  # Error

                  raise ValueError('Not enough arguments in the mod op')

-             elif len(mod) == 2:  # no action

-                 action = ldap.MOD_REPLACE

-                 key, value = mod

+             elif len(mod) == 2:  # delete all attributes action

+                 action, key = mod

+                 if action != ldap.MOD_DELETE:

+                     raise ValueError('Not enough arguments in the mod op')

+                 mod_list.append((action, key, None))

              elif len(mod) == 3:

                  action, key, value = mod

                  if action != ldap.MOD_REPLACE and \

                     action != ldap.MOD_ADD and \

                     action != ldap.MOD_DELETE:

                      raise ValueError('Invalid mod action(%s)' % str(action))

+                 if isinstance(value, list):

+                     value = ensure_list_bytes(value)

+                 else:

+                     value = [ensure_bytes(value)]

+                 mod_list.append((action, key, value))

              else:

                  # Error too many items

                  raise ValueError('Too many arguments in the mod op')

- 

-             if isinstance(value, list):

-                 value = ensure_list_bytes(value)

-             else:

-                 value = [ensure_bytes(value)]

- 

-             mod_list.append((action, key, value))

          return self._instance.modify_ext_s(self._dn, mod_list, serverctrls=self._server_controls, clientctrls=self._client_controls)

  

      @classmethod

@@ -5,6 +5,67 @@ 

  # License: GPL (version 3 or any later version).

  # See LICENSE for details.

  # --- END COPYRIGHT BLOCK ---

+ import ldap

+ from lib389 import ensure_list_str

+ 

+ 

+ def _args_to_attrs(args, arg_to_attr):

+     attrs = {}

+     for arg in vars(args):

+         val = getattr(args, arg)

+         if arg in arg_to_attr and val is not None:

+             attrs[arg_to_attr[arg]] = val

+         elif arg == 'DN':

+             # Extract RDN from the DN

+             attribute = ldap.dn.str2dn(val)[0][0][0]

+             value = ldap.dn.str2dn(val)[0][0][1]

+             attrs[attribute] = value

+     return attrs

+ 

+ 

+ def generic_object_add(dsldap_object, log, args, arg_to_attr, props={}):

+     """Create an entry using DSLdapObject interface

+ 

+     dsldap_object should be a single instance of DSLdapObject with a set dn

+     """

+ 

+     log = log.getChild('generic_object_add')

+     # Gather the attributes

+     attrs = _args_to_attrs(args, arg_to_attr)

+     # Update the parameters (which should have at least 'cn') with arg attributes

+     props.update({attr: value for (attr, value) in attrs.items() if value != ""})

+     new_object = dsldap_object.create(properties=props)

+     log.info("Successfully created the %s", new_object.dn)

+ 

+ 

+ def generic_object_edit(dsldap_object, log, args, arg_to_attr):

+     """Create an entry using DSLdapObject interface

+ 

+     dsldap_object should be a single instance of DSLdapObject with a set dn

+     """

+ 

+     log = log.getChild('generic_object_edit')

+     # Gather the attributes

+     attrs = _args_to_attrs(args, arg_to_attr)

+     existing_attributes = dsldap_object.get_all_attrs()

+ 

+     modlist = []

+     for attr, value in attrs.items():

+         # Delete the attribute only if the user set it to 'delete' value

+         if value in ("delete", ["delete"]):

+             if attr in existing_attributes:

+                 modlist.append((ldap.MOD_DELETE, attr))

+         else:

+             if not isinstance(value, list):

+                 value = [value]

+             if not (attr in existing_attributes and value == ensure_list_str(existing_attributes[attr])):

+                 modlist.append((ldap.MOD_REPLACE, attr, value))

+     if len(modlist) > 0:

+         dsldap_object.apply_mods(modlist)

+         log.info("Successfully changed the %s", dsldap_object.dn)

+     else:

+         raise ValueError("There is nothing to set in the %s plugin entry" % dsldap_object.dn)

+ 

  

  def generic_show(inst, basedn, log, args):

      """Display plugin configuration."""
@@ -15,27 +76,27 @@ 

  def generic_enable(inst, basedn, log, args):

      plugin = args.plugin_cls(inst)

      if plugin.status():

-         print("Plugin '%s' already enabled" % plugin.rdn)

+         print("Plugin '%s' already enabled", plugin.rdn)

      else:

          plugin.enable()

-         print("Enabled plugin '%s'" % plugin.rdn)

+         print("Enabled plugin '%s'", plugin.rdn)

  

  

  def generic_disable(inst, basedn, log, args):

      plugin = args.plugin_cls(inst)

      if not plugin.status():

-         print("Plugin '%s' already disabled " % plugin.rdn)

+         print("Plugin '%s' already disabled", plugin.rdn)

      else:

          plugin.disable()

-         print("Disabled plugin '%s'" % plugin.rdn)

+         print("Disabled plugin '%s'", plugin.rdn)

  

  

  def generic_status(inst, basedn, log, args):

      plugin = args.plugin_cls(inst)

      if plugin.status() is True:

-         print("Plugin '%s' is enabled" % plugin.rdn)

+         print("Plugin '%s' is enabled", plugin.rdn)

      else:

-         print("Plugin '%s' is disabled" % plugin.rdn)

+         print("Plugin '%s' is disabled", plugin.rdn)

  

  

  def add_generic_plugin_parsers(subparser, plugin_cls):

@@ -170,7 +170,7 @@ 

  

      be = Backend(inst)

      be.create(properties=props)

-     print("The database was sucessfully created")

+     print("The database was successfully created")

  

  

  def _recursively_del_backends(be):
@@ -197,7 +197,7 @@ 

      _recursively_del_backends(be)

      be.delete()

  

-     print("The database, and any sub-suffixes, were sucessfully deleted")

+     print("The database, and any sub-suffixes, were successfully deleted")

  

  

  def backend_import(inst, basedn, log, args):
@@ -307,7 +307,7 @@ 

          be.enable()

      if args.disable:

          be.disable()

-     print("The backend configuration was sucessfully updated")

+     print("The backend configuration was successfully updated")

  

  

  def db_config_get(inst, basedn, log, args):

@@ -13,6 +13,7 @@ 

      _generic_get,

      _get_arg,

  )

+ from lib389.cli_conf import generic_object_edit

  from lib389.cli_conf.plugins import memberof as cli_memberof

  from lib389.cli_conf.plugins import usn as cli_usn

  from lib389.cli_conf.plugins import rootdn_ac as cli_rootdn_ac
@@ -31,6 +32,19 @@ 

  MANY = Plugins

  RDN = 'cn'

  

+ arg_to_attr = {

+     'initfunc': 'nsslapd-pluginInitfunc',

+     'enabled': 'nsslapd-pluginEnabled',

+     'path': 'nsslapd-pluginPath',

+     'type': 'nsslapd-pluginType',

+     'id': 'nsslapd-pluginId',

+     'version': 'nsslapd-pluginVersion',

+     'vendor': 'nsslapd-pluginVendor',

+     'description': 'nsslapd-pluginDescription',

+     'depends_on_type': 'nsslapd-plugin-depends-on-type',

+     'depends_on_named': 'nsslapd-plugin-depends-on-named'

+ }

+ 

  

  def plugin_list(inst, basedn, log, args):

      plugin_log = log.getChild('plugin_list')
@@ -77,25 +91,7 @@ 

      rdn = _get_arg(args.selector, msg="Enter %s to retrieve" % RDN)

      plugins = Plugins(inst)

      plugin = plugins.get(rdn)

- 

-     if args.enabled is not None and args.enabled.lower() not in ["on", "off"]:

-         raise ValueError("Plugin enabled argument should be 'on' or 'off'")

- 

-     plugin_args = {'nsslapd-pluginInitfunc': args.initfunc,

-                    'nsslapd-pluginEnabled': args.enabled,

-                    'nsslapd-pluginPath': args.path,

-                    'nsslapd-pluginType': args.type,

-                    'nsslapd-pluginId': args.id,

-                    'nsslapd-pluginVersion': args.version,

-                    'nsslapd-pluginVendor': args.vendor,

-                    'nsslapd-pluginDescription': args.description}

-     mods = vaidate_args(plugin, plugin_args)

- 

-     if len(mods) > 0:

-         plugin.replace_many(*mods)

-         log.info("Successfully changed the plugin %s", rdn)

-     else:

-         raise ValueError("Nothing to change")

+     generic_object_edit(plugin, log, args, arg_to_attr)

  

  

  def create_parser(subparsers):
@@ -128,11 +124,17 @@ 

      edit_parser.set_defaults(func=plugin_edit)

      edit_parser.add_argument('selector', nargs='?', help='The plugin to edit')

      edit_parser.add_argument('--type', help='The type of plugin.')

-     edit_parser.add_argument('--enabled',

-                              help='Identifies whether or not the plugin is enabled. It should have "on" or "off" values.')

+     edit_parser.add_argument('--enabled', choices=['on', 'off'],

+                              help='Identifies whether or not the plugin is enabled.')

      edit_parser.add_argument('--path', help='The plugin library name (without the library suffix).')

      edit_parser.add_argument('--initfunc', help='An initialization function of the plugin.')

      edit_parser.add_argument('--id', help='The plugin ID.')

      edit_parser.add_argument('--vendor', help='The vendor of plugin.')

      edit_parser.add_argument('--version', help='The version of plugin.')

      edit_parser.add_argument('--description', help='The description of the plugin.')

+     edit_parser.add_argument('--depends-on-type',

+                              help='All plug-ins with a type value which matches one of the values '

+                                   'in the following valid range will be started by the server prior to this plug-in.')

+     edit_parser.add_argument('--depends-on-named',

+                              help='The plug-in name matching one of the following values will be '

+                                   'started by the server prior to this plug-in')

@@ -7,199 +7,92 @@ 

  # --- END COPYRIGHT BLOCK ---

  

  import ldap

- 

- from lib389.plugins import MemberOfPlugin

- from lib389.cli_conf import add_generic_plugin_parsers

- 

- 

- def manage_attr(inst, basedn, log, args):

-     if args.value is not None:

-         set_attr(inst, basedn, log, args)

-     else:

-         display_attr(inst, basedn, log, args)

- 

- def display_attr(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     log.info(plugin.get_attr_formatted())

- 

- def set_attr(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     try:

-         plugin.set_attr(args.value)

-     except ldap.UNWILLING_TO_PERFORM:

-         log.error('Error: Illegal value "{}". Failed to set.'.format(args.value))

-     else:

-         log.info('memberOfAttr set to "{}"'.format(args.value))

- 

- def display_groupattr(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     log.info(plugin.get_groupattr_formatted())

- 

- def add_groupattr(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     try:

-         plugin.add_groupattr(args.value)

-     except ldap.UNWILLING_TO_PERFORM:

-         log.error('Error: Illegal value "{}". Failed to add.'.format(args.value))

-     except ldap.TYPE_OR_VALUE_EXISTS:

-         log.info('Value "{}" already exists.'.format(args.value))

-     else:

-         log.info('successfully added memberOfGroupAttr value "{}"'.format(args.value))

- 

- def remove_groupattr(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     try:

-         plugin.remove_groupattr(args.value)

-     except ldap.UNWILLING_TO_PERFORM:

-         log.error("Error: Failed to delete. memberOfGroupAttr is required.")

-     except ldap.NO_SUCH_ATTRIBUTE:

-         log.error('Error: Failed to delete. No value "{0}" found.'.format(args.value))

-     else:

-         log.info('successfully removed memberOfGroupAttr value "{}"'.format(args.value))

- 

- def display_allbackends(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     val = plugin.get_allbackends_formatted()

-     if not val:

-         log.info("memberOfAllBackends is not set")

+ from lib389.plugins import MemberOfPlugin, Plugins, MemberOfSharedConfig

+ from lib389.cli_conf import add_generic_plugin_parsers, generic_object_edit, generic_object_add

+ 

+ arg_to_attr = {

+     'initfunc': 'nsslapd-pluginInitfunc',

+     'attr': 'memberOfAttr',

+     'groupattr': 'memberOfGroupAttr',

+     'allbackends': 'memberOfAllBackends',

+     'skipnested': 'memberOfSkipNested',

+     'scope': 'memberOfEntryScope',

+     'exclude': 'memberOfEntryScopeExcludeSubtree',

+     'autoaddoc': 'memberOfAutoAddOC',

+     'config_entry': 'nsslapd-pluginConfigArea'

+ }

+ 

+ 

+ def memberof_edit(inst, basedn, log, args):

+     log = log.getChild('memberof_edit')

+     plugins = Plugins(inst)

+     plugin = plugins.get("MemberOf Plugin")

+     generic_object_edit(plugin, log, args, arg_to_attr)

+ 

+ 

+ def memberof_add_config(inst, basedn, log, args):

+     log = log.getChild('memberof_add_config')

+     targetdn = args.DN

+     config = MemberOfSharedConfig(inst, targetdn)

+     generic_object_add(config, log, args, arg_to_attr)

+     plugins = Plugins(inst)

+     plugin = plugins.get("MemberOf Plugin")

+     plugin.replace('nsslapd-pluginConfigArea', config.dn)

+     log.info('MemberOf attribute nsslapd-pluginConfigArea (config-entry) '

+              'was set in the main plugin config')

+ 

+ 

+ def memberof_edit_config(inst, basedn, log, args):

+     log = log.getChild('memberof_edit_config')

+     targetdn = args.DN

+     config = MemberOfSharedConfig(inst, targetdn)

+     generic_object_edit(config, log, args, arg_to_attr)

+ 

+ 

+ def memberof_show_config(inst, basedn, log, args):

+     log = log.getChild('memberof_show_config')

+     targetdn = args.DN

+     config = MemberOfSharedConfig(inst, targetdn)

+ 

+     if not config.exists():

+         raise ldap.NO_SUCH_OBJECT("Entry %s doesn't exists" % targetdn)

+     if args and args.json:

+         o_str = config.get_all_attrs_json()

+         print(o_str)

      else:

-         log.info(val)

+         print(config.display())

  

- def enable_allbackends(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     plugin.enable_allbackends()

-     log.info("memberOfAllBackends enabled successfully")

- 

- def disable_allbackends(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     plugin.disable_allbackends()

-     log.info("memberOfAllBackends disabled successfully")

  

- def display_skipnested(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     val = plugin.get_skipnested_formatted()

-     if not val:

-         log.info("memberOfSkipNested is not set")

-     else:

-         log.info(val)

- 

- def enable_skipnested(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     plugin.enable_skipnested()

-     log.info("memberOfSkipNested set successfully")

+ def memberof_del_config(inst, basedn, log, args):

+     log = log.getChild('memberof_del_config')

+     targetdn = args.DN

+     config = MemberOfSharedConfig(inst, targetdn)

+     config.delete()

+     log.info("Successfully deleted the %s", targetdn)

  

- def disable_skipnested(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     plugin.disable_skipnested()

-     log.info("memberOfSkipNested unset successfully")

- 

- def manage_autoaddoc(inst, basedn, log, args):

-     if args.value == "del":

-         remove_autoaddoc(inst, basedn, log, args)

-     elif args.value is not None:

-         set_autoaddoc(inst, basedn, log, args)

-     else:

-         display_autoaddoc(inst, basedn, log, args)

- 

- def display_autoaddoc(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     val = plugin.get_autoaddoc_formatted()

-     if not val:

-         log.info("memberOfAutoAddOc is not set")

-     else:

-         log.info(val)

- 

- def set_autoaddoc(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     d = {'nsmemberof': 'nsMemberOf', 'inetuser': 'inetUser', 'inetadmin': 'inetAdmin'}

-     plugin.set_autoaddoc(d[args.value])

-     log.info('memberOfAutoAddOc set to "{}"'.format(d[args.value]))

- 

- def remove_autoaddoc(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     if not plugin.get_autoaddoc():

-         log.info("memberOfAutoAddOc was not set")

-     else:

-         plugin.remove_autoaddoc()

-         log.info("memberOfAutoAddOc attribute deleted")

- 

- def display_scope(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     val = plugin.get_entryscope_formatted()

-     if not val:

-         log.info("memberOfEntryScope is not set")

-     else:

-         log.info(val)

- 

- def add_scope(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     try:

-         plugin.add_entryscope(args.value)

-     except ldap.UNWILLING_TO_PERFORM as ex:

-         if "is also listed as an exclude suffix" in ex.args[0]['info']:

-             log.error('Error: Include suffix ({0}) is also listed as an exclude suffix.'.format(args.value))

-         else:

-             log.error('Error: Invalid DN "{}". Failed to add.'.format(args.value))

-     except ldap.TYPE_OR_VALUE_EXISTS:

-         log.info('Value "{}" already exists.'.format(args.value))

-     else:

-         log.info('successfully added memberOfEntryScope value "{}"'.format(args.value))

- 

- def remove_scope(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     try:

-         plugin.remove_entryscope(args.value)

-     except ldap.NO_SUCH_ATTRIBUTE:

-         log.error('Error: Failed to delete. No value "{0}" found.'.format(args.value))

-     else:

-         log.info('successfully removed memberOfEntryScope value "{}"'.format(args.value))

- 

- def remove_all_scope(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     plugin.remove_all_entryscope()

-     log.info('successfully removed all memberOfEntryScope values')

- 

- def display_excludescope(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     val = plugin.get_excludescope_formatted()

-     if not val:

-         log.info("memberOfEntryScopeExcludeSubtree is not set")

-     else:

-         log.info(val)

- 

- def add_excludescope(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     try:

-         plugin.add_excludescope(args.value)

-     except ldap.UNWILLING_TO_PERFORM as ex:

-         if "is also listed as an exclude suffix" in ex.args[0]['info']:

-             log.error('Error: Suffix ({0}) is listed in entry scope.'.format(args.value))

-         else:

-             log.error('Error: Invalid DN "{}". Failed to add.'.format(args.value))

-     except ldap.TYPE_OR_VALUE_EXISTS:

-         log.info('Value "{}" already exists.'.format(args.value))

-     else:

-         log.info('successfully added memberOfEntryScopeExcludeSubtree value "{}"'.format(args.value))

- 

- def remove_excludescope(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     try:

-         plugin.remove_excludescope(args.value)

-     except ldap.NO_SUCH_ATTRIBUTE:

-         log.error('Error: Failed to delete. No value "{0}" found.'.format(args.value))

-     else:

-         log.info('successfully removed memberOfEntryScopeExcludeSubtree value "{}"'.format(args.value))

- 

- def remove_all_excludescope(inst, basedn, log, args):

-     plugin = MemberOfPlugin(inst)

-     plugin.remove_all_excludescope()

-     log.info('successfully removed all memberOfEntryScopeExcludeSubtree values')

  

  def fixup(inst, basedn, log, args):

      plugin = MemberOfPlugin(inst)

      log.info('Attempting to add task entry... This will fail if MemberOf plug-in is not enabled.')

-     fixup_task = plugin.fixup(args.basedn, args.filter)

-     log.info('Successfully added task entry ' + fixup_task.dn)

+     assert plugin.status(), "'%s' is disabled. Fix up task can't be executed" % plugin.rdn

+     fixup_task = plugin.fixup(args.DN, args.filter)

+     fixup_task.wait()

+     exitcode = fixup_task.get_exit_code()

+     assert exitcode == 0, 'MemberOf fixup task for %s has failed. Please, check logs'

+     log.info('Successfully added task entry for %s', args.DN)

+ 

+ 

+ def _add_parser_args(parser):

+     parser.add_argument('--attr', nargs='+', help='The value to set as memberOfAttr')

+     parser.add_argument('--groupattr', nargs='+', help='The value to set as memberOfGroupAttr')

+     parser.add_argument('--allbackends', choices=['on', 'off'], type=str.lower,

+                         help='The value to set as memberOfAllBackends')

+     parser.add_argument('--skipnested', choices=['on', 'off'], type=str.lower,

+                         help='The value to set as memberOfSkipNested')

+     parser.add_argument('--scope', help='The value to set as memberOfEntryScope')

+     parser.add_argument('--exclude', help='The value to set as memberOfEntryScopeExcludeSubtree')

+     parser.add_argument('--autoaddoc', type=str.lower, help='The value to set as memberOfAutoAddOC')

+ 

  

  def create_parser(subparsers):

      memberof_parser = subparsers.add_parser('memberof', help='Manage and configure MemberOf plugin')
@@ -208,69 +101,32 @@ 

  

      add_generic_plugin_parsers(subcommands, MemberOfPlugin)

  

-     attr_parser = subcommands.add_parser('attr', help='get or set memberofattr')

-     attr_parser.set_defaults(func=manage_attr)

-     attr_parser.add_argument('value', nargs='?', help='The value to set as memberofattr')

- 

-     groupattr_parser = subcommands.add_parser('groupattr', help='get or manage memberofgroupattr')

-     groupattr_parser.set_defaults(func=display_groupattr)

-     groupattr_subcommands = groupattr_parser.add_subparsers(help='action')

-     add_groupattr_parser = groupattr_subcommands.add_parser('add', help='add memberofgroupattr value')

-     add_groupattr_parser.set_defaults(func=add_groupattr)

-     add_groupattr_parser.add_argument('value', help='The value to add in memberofgroupattr')

-     del_groupattr_parser = groupattr_subcommands.add_parser('del', help='remove memberofgroupattr value')

-     del_groupattr_parser.set_defaults(func=remove_groupattr)

-     del_groupattr_parser.add_argument('value', help='The value to remove from memberofgroupattr')

- 

-     allbackends_parser = subcommands.add_parser('allbackends', help='get or manage memberofallbackends')

-     allbackends_parser.set_defaults(func=display_allbackends)

-     allbackends_subcommands = allbackends_parser.add_subparsers(help='action')

-     on_allbackends_parser = allbackends_subcommands.add_parser('on', help='enable all backends for memberof')

-     on_allbackends_parser.set_defaults(func=enable_allbackends)

-     off_allbackends_parser = allbackends_subcommands.add_parser('off', help='disable all backends for memberof')

-     off_allbackends_parser.set_defaults(func=disable_allbackends)

- 

-     skipnested_parser = subcommands.add_parser('skipnested', help='get or manage memberofskipnested')

-     skipnested_parser.set_defaults(func=display_skipnested)

-     skipnested_subcommands = skipnested_parser.add_subparsers(help='action')

-     on_skipnested_parser = skipnested_subcommands.add_parser('on', help='skip nested groups for memberof')

-     on_skipnested_parser.set_defaults(func=enable_skipnested)

-     off_skipnested_parser = skipnested_subcommands.add_parser('off', help="don't skip nested groups for memberof")

-     off_skipnested_parser.set_defaults(func=disable_skipnested)

- 

-     autoaddoc_parser = subcommands.add_parser('autoaddoc', help='get or set memberofautoaddoc')

-     autoaddoc_parser.set_defaults(func=manage_autoaddoc)

-     autoaddoc_parser.add_argument('value', nargs='?', choices=['nsmemberof', 'inetuser', 'inetadmin', 'del'],

-                                    type=str.lower, help='The value to set as memberofautoaddoc or del to remove the attribute')

- 

-     scope_parser = subcommands.add_parser('scope', help='get or manage memberofentryscope')

-     scope_parser.set_defaults(func=display_scope)

-     scope_subcommands = scope_parser.add_subparsers(help='action')

-     add_scope_parser = scope_subcommands.add_parser('add', help='add memberofentryscope value')

-     add_scope_parser.set_defaults(func=add_scope)

-     add_scope_parser.add_argument('value', help='The value to add in memberofentryscope')

-     del_scope_parser = scope_subcommands.add_parser('del', help='remove memberofentryscope value')

-     del_scope_parser.set_defaults(func=remove_scope)

-     del_scope_parser.add_argument('value', help='The value to remove from memberofentryscope')

-     delall_scope_parser = scope_subcommands.add_parser('delall', help='remove all memberofentryscope values')

-     delall_scope_parser.set_defaults(func=remove_all_scope)

- 

-     exclude_parser = subcommands.add_parser('exclude', help='get or manage memberofentryscopeexcludesubtree')

-     exclude_parser.set_defaults(func=display_excludescope)

-     exclude_subcommands = exclude_parser.add_subparsers(help='action')

-     add_exclude_parser = exclude_subcommands.add_parser('add', help='add memberofentryscopeexcludesubtree value')

-     add_exclude_parser.set_defaults(func=add_excludescope)

-     add_exclude_parser.add_argument('value', help='The value to add in memberofentryscopeexcludesubtree')

-     del_exclude_parser = exclude_subcommands.add_parser('del', help='remove memberofentryscopeexcludesubtree value')

-     del_exclude_parser.set_defaults(func=remove_excludescope)

-     del_exclude_parser.add_argument('value', help='The value to remove from memberofentryscopeexcludesubtree')

-     delall_exclude_parser = exclude_subcommands.add_parser('delall', help='remove all memberofentryscopeexcludesubtree values')

-     delall_exclude_parser.set_defaults(func=remove_all_excludescope)

- 

-     fixup_parser = subcommands.add_parser('fixup', help='run the fix-up task for memberof plugin')

+     edit_parser = subcommands.add_parser('edit', help='Edit the plugin')

+     edit_parser.set_defaults(func=memberof_edit)

+     _add_parser_args(edit_parser)

+     edit_parser.add_argument('--config-entry', help='The value to set as nsslapd-pluginConfigArea')

+ 

+     config_parser = subcommands.add_parser('config-entry', help='Manage the config entry')

+     config_subcommands = config_parser.add_subparsers(help='action')

+     add_config_parser = config_subcommands.add_parser('add', help='Add the config entry')

+     add_config_parser.set_defaults(func=memberof_add_config)

+     add_config_parser.add_argument('DN', help='The config entry full DN')

+     _add_parser_args(add_config_parser)

+     edit_config_parser = config_subcommands.add_parser('edit', help='Edit the config entry')

+     edit_config_parser.set_defaults(func=memberof_edit_config)

+     edit_config_parser.add_argument('DN', help='The config entry full DN')

+     _add_parser_args(edit_config_parser)

+     show_config_parser = config_subcommands.add_parser('show', help='Display the config entry')

+     show_config_parser.set_defaults(func=memberof_show_config)

+     show_config_parser.add_argument('DN', help='The config entry full DN')

+     del_config_parser = config_subcommands.add_parser('delete', help='Delete the config entry')

+     del_config_parser.set_defaults(func=memberof_del_config)

+     del_config_parser.add_argument('DN', help='The config entry full DN')

+ 

+     fixup_parser = subcommands.add_parser('fixup', help='Run the fix-up task for memberOf plugin')

      fixup_parser.set_defaults(func=fixup)

-     fixup_parser.add_argument('-b', '--basedn', required=True, help="base DN that contains entries to fix up")

-     fixup_parser.add_argument('-f', '--filter', help="Filter for entries to fix up.\n"

-         "If omitted, all entries with objectclass inetuser/inetadmin/nsmemberof under the\n"

-         "specified base will have their memberOf attribute regenerated."

-     )

+     fixup_parser.add_argument('DN', help="base DN that contains entries to fix up")

+     fixup_parser.add_argument('-f', '--filter',

+                               help='Filter for entries to fix up.\n If omitted, all entries with objectclass '

+                                    'inetuser/inetadmin/nsmemberof under the specified base will have '

+                                    'their memberOf attribute regenerated.')

@@ -68,7 +68,7 @@ 

  

      sd = SetupDs(args.verbose, args.dryrun, log, args.containerised)

      if sd.create_from_inf(args.file):

-         # print("Sucessfully created instance")

+         # print("Successfully created instance")

          return True

      else:

          # print("Failed to create instance")
@@ -200,6 +200,3 @@ 

      remove_parser.set_defaults(func=instance_remove)

      remove_parser.add_argument('--do-it', dest="ack", help="By default we do a dry run. This actually initiates the removal of the instance.",

                                 action='store_true', default=False)

- 

- 

- 

@@ -107,13 +107,13 @@ 

      mc = manager_class(inst, basedn)

      o = mc.create(properties=kwargs)

      o_str = o.__unicode__()

-     log.info('Sucessfully created %s' % o_str)

+     log.info('Successfully created %s' % o_str)

  

  

  def _generic_delete(inst, basedn, log, object_class, dn, args=None):

      # Load the oc direct

      o = object_class(inst, dn)

      o.delete()

-     log.info('Sucessfully deleted %s' % dn)

+     log.info('Successfully deleted %s' % dn)

  

  # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4

@@ -66,7 +66,7 @@ 

      print('Launching ...')

      sd = SetupDs(args.verbose, args.dryrun)

      if sd.create_from_inf(args.file):

-         print("Sucessfully created instance")

+         print("Successfully created instance")

      else:

          print("Failed to create instance")

          sys.exit(1)
@@ -75,4 +75,3 @@ 

  

  if __name__ == '__main__':

      ds_setup_main()

- 

file modified
+10 -10
@@ -1073,15 +1073,15 @@ 

                  raise ValueError('Failed to update replica: ' + str(e))

          elif replicarole == ReplicaRole.CONSUMER and newrole == ReplicaRole.MASTER:

              try:

-                 self.apply_mods([(REPL_TYPE, str(REPLICA_RDWR_TYPE)),

-                                  (REPL_FLAGS, str(REPLICA_FLAGS_WRITE)),

-                                  (REPL_ID, str(rid))])

+                 self.replace_many([(REPL_TYPE, str(REPLICA_RDWR_TYPE)),

+                                    (REPL_FLAGS, str(REPLICA_FLAGS_WRITE)),

+                                    (REPL_ID, str(rid))])

              except ldap.LDAPError as e:

                  raise ValueError('Failed to update replica: ' + str(e))

          elif replicarole == ReplicaRole.HUB and newrole == ReplicaRole.MASTER:

              try:

-                 self.apply_mods([(REPL_TYPE, str(REPLICA_RDWR_TYPE)),

-                                  (REPL_ID, str(rid))])

+                 self.replace_many([(REPL_TYPE, str(REPLICA_RDWR_TYPE)),

+                                    (REPL_ID, str(rid))])

              except ldap.LDAPError as e:

                  raise ValueError('Failed to update replica: ' + str(e))

  
@@ -1103,15 +1103,15 @@ 

          # Demote it - set the replica type, flags and rid

          if replicarole == ReplicaRole.MASTER and newrole == ReplicaRole.HUB:

              try:

-                 self.apply_mods([(REPL_TYPE, str(REPLICA_RDONLY_TYPE)),

-                                  (REPL_ID, str(CONSUMER_REPLICAID))])

+                 self.replace_many([(REPL_TYPE, str(REPLICA_RDONLY_TYPE)),

+                                    (REPL_ID, str(CONSUMER_REPLICAID))])

              except ldap.LDAPError as e:

                  raise ValueError('Failed to update replica: ' + str(e))

          elif replicarole == ReplicaRole.MASTER and newrole == ReplicaRole.CONSUMER:

              try:

-                 self.apply_mods([(REPL_TYPE, str(REPLICA_RDONLY_TYPE)),

-                                  (REPL_FLAGS, str(REPLICA_FLAGS_RDONLY)),

-                                  (REPL_ID, str(CONSUMER_REPLICAID))])

+                 self.replace_many([(REPL_TYPE, str(REPLICA_RDONLY_TYPE)),

+                                    (REPL_FLAGS, str(REPLICA_FLAGS_RDONLY)),

+                                    (REPL_ID, str(CONSUMER_REPLICAID))])

              except ldap.LDAPError as e:

                  raise ValueError('Failed to update replica: ' + str(e))

          elif replicarole == ReplicaRole.HUB and newrole == ReplicaRole.CONSUMER:

@@ -53,7 +53,7 @@ 

  

      args.suffix = SUFFIX

      backend_create(topology_st.standalone, None, None, args)

-     check_output("The database was sucessfully created")

+     check_output("The database was successfully created")

  

      def fin():

          sys.stdout = io.StringIO()
@@ -66,7 +66,7 @@ 

  

          # Delete backend

          backend_delete(topology_st.standalone, None, None, args, warn=False)

-         check_output("sucessfully deleted")

+         check_output("successfully deleted")

  

          # Verify it's removed

          args.suffix = False
@@ -137,7 +137,7 @@ 

      args.suffix = SUB_SUFFIX

      args.be_name = SUB_BE_NAME

      backend_create(topology_st.standalone, None, topology_st.logcap.log, args)

-     check_output("The database was sucessfully created")

+     check_output("The database was successfully created")

  

      # Verify subsuffix

      args.suffix = False
@@ -155,7 +155,7 @@ 

      args.enable_readonly = True  # Setting nsslapd-readonly to "on"

      args.disable_readonly = False

      backend_set(topology_st.standalone, None, topology_st.logcap.log, args)

-     check_output("sucessfully updated")

+     check_output("successfully updated")

  

      # Verify modified worked

      args.selector = SUB_BE_NAME
@@ -165,7 +165,7 @@ 

      # Delete subsuffix

      args.suffix = SUB_SUFFIX

      backend_delete(topology_st.standalone, None, topology_st.logcap.log, args, warn=False)

-     check_output("sucessfully deleted")

+     check_output("successfully deleted")

  

      # Verify it is deleted

      args.suffix = False
@@ -175,7 +175,7 @@ 

      # Modify backend (use same args from subsuffix modify)

      args.be_name = BE_NAME

      backend_set(topology_st.standalone, None, topology_st.logcap.log, args)

-     check_output("sucessfully updated")

+     check_output("successfully updated")

  

      # Verify modified worked

      args.selector = BE_NAME

@@ -46,7 +46,7 @@ 

      args.create_entries = True

      args.suffix = SUFFIX

      backend_create(topology_st.standalone, None, None, args)

-     check_output("The database was sucessfully created")

+     check_output("The database was successfully created")

  

      def fin():

          sys.stdout = io.StringIO()
@@ -60,7 +60,7 @@ 

  

          # Delete backend

          backend_delete(topology_st.standalone, None, None, args, warn=False)

-         check_output("sucessfully deleted")

+         check_output("successfully deleted")

  

          # Verify it's removed

          args.suffix = False

@@ -35,7 +35,7 @@ 

      topology.logcap.flush()

      g_args.cn = 'testgroup'

      create(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, g_args)

-     assert(topology.logcap.contains("Sucessfully created testgroup"))

+     assert(topology.logcap.contains("Successfully created testgroup"))

  

      # Assert it exists

      topology.logcap.flush()
@@ -54,7 +54,7 @@ 

      u_args.uidNumber = '5000'

      u_args.gidNumber = '5000'

      create_user(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, u_args)

-     assert(topology.logcap.contains("Sucessfully created testuser"))

+     assert(topology.logcap.contains("Successfully created testuser"))

  

      # Add them to the group as a member

      topology.logcap.flush()
@@ -86,5 +86,4 @@ 

      topology.logcap.flush()

      g_args.dn = "cn=testgroup,ou=groups,dc=example,dc=com"

      delete(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, g_args, warn=False)

-     assert(topology.logcap.contains("Sucessfully deleted cn=testgroup,ou=groups,dc=example,dc=com"))

- 

+     assert(topology.logcap.contains("Successfully deleted cn=testgroup,ou=groups,dc=example,dc=com"))

@@ -52,7 +52,7 @@ 

      u_args.gidNumber = '5000'

      create(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, u_args)

  

-     assert(topology.logcap.contains("Sucessfully created testuser"))

+     assert(topology.logcap.contains("Successfully created testuser"))

      # Assert they exist

      topology.logcap.flush()

      get(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, u_args)
@@ -82,9 +82,8 @@ 

  

      # Enroll a dummy sshkey

  

-     # Delete it 

+     # Delete it

      topology.logcap.flush()

      u_args.dn = 'uid=testuser,ou=people,dc=example,dc=com'

      delete(topology.standalone, DEFAULT_SUFFIX, topology.logcap.log, u_args, warn=False)

-     assert(topology.logcap.contains('Sucessfully deleted uid=testuser,ou=people,dc=example,dc=com'))

- 

+     assert(topology.logcap.contains('Successfully deleted uid=testuser,ou=people,dc=example,dc=com'))

Description: Add mainfunctionality to memberOf plugin tab.
Increase the eslint max line length from 80 to 100.
Rework plugin properties to be more compact.
Eslint webpack config. Add react-bootstrap-typeahead for
multivalued attributes. Fix the word 'successfully' typos.

https://pagure.io/389-ds-base/issue/50041

Reviewed by: ?

The current state of the PR is intermediate. I'll add 'fixup' task and 'config entry - manage' part i a couple of following days.
But please, review the rest. It is pretty big already...

Also, you need to reinstall the node_modules for 'react-bootstrap-typeahead' package.

rebased onto 5e6c009575cbac9bd1bbf5960645443497249bd2

5 years ago

This shouldn't be choices, because it could be any oc that takes MO.

What's an example of this command in use? dsconf localhost plugin memberof edit --attr ?

Okay, I'm not sure about generic edit. I'm currently working on "modify" in another ticket, that will perform the same. Part of the design, of having the ability to "wrap and set" attributes (which you have removed in this patch), is that it's really clear what can and can't be altered in a really natural way. Another aspect is the cli should do "lots of common tasks" but "not everything". We should be offering only common useful settings, rather than everything. The cli offers a safe modification interface.

A benefit of your change is "batch" changes, where you change many attributes at once. However, I would prefer that there is a "delete" keyword rather than "no string" meaning delete. I think that deletes should always be a requested action, not an implied actionn. The design of this --edit will mean any mistake on the persons part where they forgot to write an argument will cause harm. Any situation where a person could "hold it wrong" means we have failed as tool designers.

So I would say these are the options I request: leave the CLI as is and use the "modify" interface that I'm adding, which does what you want for "advanced" users, and has a better, clearer delete syntax.

Or, implement this change, but have a clear "delete" keyword when you want to remove an attribute, rather than the current method of empty string."--attr <add this>" // "--attr delete". Enforce that each --attr takes 1 or more arguments (if more is valid).

This shouldn't be choices, because it could be any oc that takes MO.

Okay, it had these choices before - so I left it as is. But I think you are right.
A user can add some custom schema. I'll change it to the text field in the UI.

What's an example of this command in use? dsconf localhost plugin memberof edit --attr ?

Yes

Okay, I'm not sure about generic edit. I'm currently working on "modify" in another ticket, that will perform the same. Part of the design, of having the ability to "wrap and set" attributes (which you have removed in this patch), is that it's really clear what can and can't be altered in a really natural way. Another aspect is the cli should do "lots of common tasks" but "not everything". We should be offering only common useful settings, rather than everything. The cli offers a safe modification interface.

I think the way I have it (and other CLI tools that Mark was implementing too - we use the same way everywhere) is also able to limit a user. Like, you can edit only the attributes that were explicitly specified in the '--' flags. If the administrator would like to set up something, he will open the help and he will see what can be set.
I agree with you. We shouldn't include everything in the explicit option list. There are few attributes that we can have in plugin entry that I didn't add directly to CLI (pluginarg0, etc.) So I always try to filter to the way you said.

A benefit of your change is "batch" changes, where you change many attributes at once. However, I would prefer that there is a "delete" keyword rather than "no string" meaning delete. I think that deletes should always be a requested action, not an implied actionn. The design of this --edit will mean any mistake on the persons part where they forgot to write an argument will cause harm. Any situation where a person could "hold it wrong" means we have failed as tool designers.
So I would say these are the options I request: leave the CLI as is and use the "modify" interface that I'm adding, which does what you want for "advanced" users, and has a better, clearer delete syntax.
Or, implement this change, but have a clear "delete" keyword when you want to remove an attribute, rather than the current method of empty string."--attr <add this="">" // "--attr delete". Enforce that each --attr takes 1 or more arguments (if more is valid).

Okay, I was thinking about that too. I will change it.

So in general, the way with batch changes is the way we have it now for some time. Mark and I was doing it for other parts of the CLI and I think we should keep at least because of the two reasons:
1) Consistensy
2) CLI -> UI interactions. Currently, we call the CLI command in the Cockpit. And we better have it as a batch change because a calling the OS command (cockpit.spawn) is a pretty expensive operation and we try to do it as less times as possible.

Using "print" was on purpose. I think you can not redirect the output using log.info(). There was some problem with it that was only fixed by using print for CLI output.

This shouldn't be choices, because it could be any oc that takes MO.

Okay, it had these choices before - so I left it as is. But I think you are right.
A user can add some custom schema. I'll change it to the text field in the UI.

Just don't try and enforce anything, just allow any objectClass here, but recommend that nsMemberOf is probably what they want in most cases.

Or, implement this change, but have a clear "delete" keyword when you want to remove an attribute, rather than the current method of empty string."--attr <add this="">" // "--attr delete". Enforce that each --attr takes 1 or more arguments (if more is valid).

Okay, I was thinking about that too. I will change it.
So in general, the way with batch changes is the way we have it now for some time. Mark and I was doing it for other parts of the CLI and I think we should keep at least because of the two reasons:
1) Consistensy
2) CLI -> UI interactions. Currently, we call the CLI command in the Cockpit. And we better have it as a batch change because a calling the OS command (cockpit.spawn) is a pretty expensive operation and we try to do it as less times as possible.

Yeah. Batching makes sense here. Check my patch in https://pagure.io/389-ds-base/pull-request/50158 because I think you can use this instead of generic edit here. Perhaps what is needed is a way to display from the "objectclass" the valid attributes from the schema (Which is why it's important we stop using extensible object ...).

Saying this, the two could be complimentary, rather than exclusive as well. We could keep edit with the list of valid options, and detailed help, and have modify as a raw interface to the objects? That could also get confusing in some cases too ....

PS: My patch in 50158 fixes some behaviour in mod_delete actions, so you probably will need to rebase and test this. :)

rebased onto 129f9b6eab3781aacb17cf650c69113499e9fa6e

5 years ago

Yeah. Batching makes sense here. Check my patch in https://pagure.io/389-ds-base/pull-request/50158 because I think you can use this instead of generic edit here. Perhaps what is needed is a way to display from the "objectclass" the valid attributes from the schema (Which is why it's important we stop using extensible object ...).
Saying this, the two could be complimentary, rather than exclusive as well. We could keep edit with the list of valid options, and detailed help, and have modify as a raw interface to the objects? That could also get confusing in some cases too ....
PS: My patch in 50158 fixes some behaviour in mod_delete actions, so you probably will need to rebase and test this. :)

Okay, I'll check it next thing. Thanks!
Currently, I've just added new functionality for the 'fixup' and 'configArea' stuff.
So it's ready for the final review (after I'll look into the issues mentioned above)

Great, thanks! Let me know what you think of that PR as well. I can't say much about the JS here so I'll leave @mreynolds for that part :)

PS: We still should think about modify vs edit interface for these calls by the way. I think that perhaps on reflection, leave the current code (so roll back your py change), then have the js use the modify interface in 50158? Would that be more complex? Or better? It allows you to perform arbitrary modifications you want ....

PS: We still should think about modify vs edit interface for these calls by the way. I think that perhaps on reflection, leave the current code (so roll back your py change), then have the js use the modify interface in 50158? Would that be more complex? Or better? It allows you to perform arbitrary modifications you want ....

I think it is better with options (as we have consistently over most CLI areas that Mark and me have added). It gives nice discoverability with --help. And I think it is more natural for the user to specify flags instead of a new non-standard syntax. At least, for the most cases.
And I think it's too late to change the approach and rewrite the rest of the CLI (if we want to have it consistent)

2 new commits added

  • Add fixup task and config entry management
  • Issue 50041 - CLI and WebUI - Add memberOf plugin functionality
5 years ago

Using "print" was on purpose. I think you can not redirect the output using log.info(). There was some problem with it that was only fixed by using print for CLI output.

Right... I didn't use the redirection with these tools but someone could need to.
Fixed (back to print)

Please, review!

PS: We still should think about modify vs edit interface for these calls by the way. I think that perhaps on reflection, leave the current code (so roll back your py change), then have the js use the modify interface in 50158? Would that be more complex? Or better? It allows you to perform arbitrary modifications you want ....

I think it is better with options (as we have consistently over most CLI areas that Mark and me have added). It gives nice discoverability with --help. And I think it is more natural for the user to specify flags instead of a new non-standard syntax. At least, for the most cases.
And I think it's too late to change the approach and rewrite the rest of the CLI (if we want to have it consistent)

This is a reasonable concern. I think it's hard because I have a different approach, but to make everything consistent would be breaking.

So how about we do the "edit" as you have here, and the "modify" from my other patch?

Otherwise, with this in mind, I'm happy with the python. Again, please rebase to my change in 50158 due to delete semantics, I'll let @mreynolds review the js. Thank you for being patient with me :)

Hi Simon, so there are some issues around "saving" the configuration because you are not maintaining a before and after snapshot of the settings. So if I do nothing and click save it actually rewrites the plugin entry in DS.

What I do here is I maintain two sets of the state attributes. Current and original, then on save I check if current is different than the original. If they are different I update the cmd list and perform the update. Something like this:

this.state({
    memberOfScope,
    _memberOfScope
})

if (this.state.memberOfScope != this.state._memberOfScope) {
    cmd.push{"--moscope=" + this.state.memberOfScope);
}

So when I "load" the config I set both state attributes, then "handleChange" updates the non "_" variable. Finally we compare them at "save" time, and only update what changed (if anything changed at all)

Also fixup task modal does not reset when it is reopened. If I put a value in the modal and cancel, then reopen it, the value is still present in the input field.

Also the "Config Entry" field, should be "Shared Config Entry".

The rest looks great!

Also, after making a config change (success or failure) I reload the config (which re-renders everything with the correct values and resets previous values ( --> the "_" state variables).

Also, by reloading the config after a successful or failed update means you don't have to maintain the html after a update because its always reset(re-rendered) from scratch. Well that's how I'm doing it in the database tab, and it works nicely, and actually creates less work.

Nice catch!

Yeah, actually I've noticed the issue before I switched to the docs and presentations work.
But I really like your solution a lot! It is very much reactive :) I'll use it. Thank you!

rebased onto eaf4bae4cbdc0c5ee184dd415ee931b52de81d8a

5 years ago

Okay, I fixed it a bit differently.
I think it is better to check just before the update on the server (so I fixed it in the lib389-CLI code). Too many things can happen between UI loading and the change. So I think the 'state' approach is not so reliable...
And we need the functionality in the CLI too anyway.

The rest is fixed too.

Well I disagree with your stance on using state to know if a value changed. Typically configuration is very static, and is only changed by one person. The odds of the memberOf plugin being changed between you opening the plugin (which should get fresh values), and clicking save is very slim.

Now, you are performing many operations even if the config did not change. My other concern here is attributes without values (or missing attributes), you will try to save empty-values which will fail for some attribute syntax checks (DirectoryString verses IA5String). In this case it does it correctly, but could this approach could fail for other configurations?

That being said, both approaches have pro's and con's - just proceed with caution if you continue to use this approach for other areas of the server.

Ack!

rebased onto 614ab2a

5 years ago

Well I disagree with your stance on using state to know if a value changed. Typically configuration is very static, and is only changed by one person. The odds of the memberOf plugin being changed between you opening the plugin (which should get fresh values), and clicking save is very slim.

I agree. I just wanted to have more precautions... Anyway, in the future, I'd like to have the UI to be in sync with the instance.

Now, you are performing many operations even if the config did not change. My other concern here is attributes without values (or missing attributes), you will try to save empty-values which will fail for some attribute syntax checks (DirectoryString verses IA5String). In this case it does it correctly, but could this approach could fail for other configurations?

I also check that we don't save any empty values. If the user set the field to an empty value, I assume the user would like to remove/not have the attribute in the entry.

That being said, both approaches have pro's and con's - just proceed with caution if you continue to use this approach for other areas of the server.
Ack!

Thanks!

Pull-Request has been merged by spichugi

5 years ago

I also check that we don't save any empty values. If the user set the field to an empty value, I assume the user would like to remove/not have the attribute in the entry.

What happens when it tries to delete an attribute that is not present in the entry? Is the error ignored, or is the operation rejected?

I also check that we don't save any empty values. If the user set the field to an empty value, I assume the user would like to remove/not have the attribute in the entry.

What happens when it tries to delete an attribute that is not present in the entry? Is the error ignored, or is the operation rejected?

CLI will filter the attribute from the modlist:
- delete operation
https://pagure.io/389-ds-base/blob/master/f/src/lib389/lib389/cli_conf/__init__.py#_56
- replace operation
https://pagure.io/389-ds-base/blob/master/f/src/lib389/lib389/cli_conf/__init__.py#_61

What happens when it tries to delete an attribute that is not present in the entry? Is the error ignored, or is the operation rejected?

CLI will filter the attribute from the modlist:

Great!

389-ds-base is moving from Pagure to Github. This means that new issues and pull requests
will be accepted only in 389-ds-base's github repository.

This pull request has been cloned to Github as issue and is available here:
- https://github.com/389ds/389-ds-base/issues/3234

If you want to continue to work on the PR, please navigate to the github issue,
download the patch from the attachments and file a new pull request.

Thank you for understanding. We apologize for all inconvenience.

Pull-Request has been closed by spichugi

3 years ago
Metadata
Changes Summary 24
+2 -1
file changed
src/cockpit/389-console/.eslintrc.json
+5 -2
file changed
src/cockpit/389-console/package.json
+15 -1
file changed
src/cockpit/389-console/src/css/ds.css
+1 -0
file changed
src/cockpit/389-console/src/index.html
+0 -1
file changed
src/cockpit/389-console/src/lib/customCollapse.jsx
+953 -2
file changed
src/cockpit/389-console/src/lib/plugins/memberOf.jsx
+116 -111
file changed
src/cockpit/389-console/src/lib/plugins/pluginBasicConfig.jsx
+100 -65
file changed
src/cockpit/389-console/src/lib/plugins/pluginModal.jsx
+109 -44
file changed
src/cockpit/389-console/src/plugins.jsx
+256
file added
src/cockpit/389-console/src/static/Typeahead.css
+34 -38
file changed
src/cockpit/389-console/webpack.config.js
+19 -12
file changed
src/lib389/lib389/_mapped_object.py
+67 -6
file changed
src/lib389/lib389/cli_conf/__init__.py
+3 -3
file changed
src/lib389/lib389/cli_conf/backend.py
+23 -21
file changed
src/lib389/lib389/cli_conf/plugin.py
+106 -250
file changed
src/lib389/lib389/cli_conf/plugins/memberof.py
+1 -4
file changed
src/lib389/lib389/cli_ctl/instance.py
+2 -2
file changed
src/lib389/lib389/cli_idm/__init__.py
+1 -2
file changed
src/lib389/lib389/clitools/ds_setup
+10 -10
file changed
src/lib389/lib389/replica.py
+6 -6
file changed
src/lib389/lib389/tests/cli/conf_backend_test.py
+2 -2
file changed
src/lib389/lib389/tests/cli/conf_chaining_test.py
+3 -4
file changed
src/lib389/lib389/tests/cli/idm_group_test.py
+3 -4
file changed
src/lib389/lib389/tests/cli/idm_user_test.py