#50481 Issue 50325 - Add Security tab to UI
Closed 2 years ago by spichugi. Opened 2 years ago by mreynolds.
mreynolds/389-ds-base ticket50325  into  master

@@ -1,4 +1,7 @@ 

  {

      "presets": ["@babel/env",

-                 "@babel/preset-react"]

+                 "@babel/preset-react"],

+     "plugins": [

+         "@babel/plugin-proposal-class-properties"

+     ]

  }

@@ -104,6 +104,105 @@ 

          "@babel/types": "^7.0.0"

        }

      },

+     "@babel/helper-create-class-features-plugin": {

+       "version": "7.4.4",

+       "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.4.4.tgz",

+       "integrity": "sha512-UbBHIa2qeAGgyiNR9RszVF7bUHEdgS4JAUNT8SiqrAN6YJVxlOxeLr5pBzb5kan302dejJ9nla4RyKcR1XT6XA==",

+       "dev": true,

+       "requires": {

+         "@babel/helper-function-name": "^7.1.0",

+         "@babel/helper-member-expression-to-functions": "^7.0.0",

+         "@babel/helper-optimise-call-expression": "^7.0.0",

+         "@babel/helper-plugin-utils": "^7.0.0",

+         "@babel/helper-replace-supers": "^7.4.4",

+         "@babel/helper-split-export-declaration": "^7.4.4"

+       },

+       "dependencies": {

+         "@babel/generator": {

+           "version": "7.4.4",

+           "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz",

+           "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==",

+           "dev": true,

+           "requires": {

+             "@babel/types": "^7.4.4",

+             "jsesc": "^2.5.1",

+             "lodash": "^4.17.11",

+             "source-map": "^0.5.0",

+             "trim-right": "^1.0.1"

+           }

+         },

+         "@babel/helper-replace-supers": {

+           "version": "7.4.4",

+           "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz",

+           "integrity": "sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg==",

+           "dev": true,

+           "requires": {

+             "@babel/helper-member-expression-to-functions": "^7.0.0",

+             "@babel/helper-optimise-call-expression": "^7.0.0",

+             "@babel/traverse": "^7.4.4",

+             "@babel/types": "^7.4.4"

+           }

+         },

+         "@babel/helper-split-export-declaration": {

+           "version": "7.4.4",

+           "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz",

+           "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==",

+           "dev": true,

+           "requires": {

+             "@babel/types": "^7.4.4"

+           }

+         },

+         "@babel/parser": {

+           "version": "7.4.5",

+           "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.5.tgz",

+           "integrity": "sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==",

+           "dev": true

+         },

+         "@babel/traverse": {

+           "version": "7.4.5",

+           "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.5.tgz",

+           "integrity": "sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==",

+           "dev": true,

+           "requires": {

+             "@babel/code-frame": "^7.0.0",

+             "@babel/generator": "^7.4.4",

+             "@babel/helper-function-name": "^7.1.0",

+             "@babel/helper-split-export-declaration": "^7.4.4",

+             "@babel/parser": "^7.4.5",

+             "@babel/types": "^7.4.4",

+             "debug": "^4.1.0",

+             "globals": "^11.1.0",

+             "lodash": "^4.17.11"

+           }

+         },

+         "@babel/types": {

+           "version": "7.4.4",

+           "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz",

+           "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==",

+           "dev": true,

+           "requires": {

+             "esutils": "^2.0.2",

+             "lodash": "^4.17.11",

+             "to-fast-properties": "^2.0.0"

+           }

+         },

+         "debug": {

+           "version": "4.1.1",

+           "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",

+           "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",

+           "dev": true,

+           "requires": {

+             "ms": "^2.1.1"

+           }

+         },

+         "source-map": {

+           "version": "0.5.7",

+           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",

+           "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",

+           "dev": true

+         }

+       }

+     },

      "@babel/helper-define-map": {

        "version": "7.1.0",

        "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz",
@@ -336,6 +435,16 @@ 

          "@babel/plugin-syntax-async-generators": "^7.0.0"

        }

      },

+     "@babel/plugin-proposal-class-properties": {

+       "version": "7.4.4",

+       "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.4.4.tgz",

+       "integrity": "sha512-WjKTI8g8d5w1Bc9zgwSz2nfrsNQsXcCf9J9cdCvrJV6RF56yztwm4TmJC0MgJ9tvwO9gUA/mcYe89bLdGfiXFg==",

+       "dev": true,

+       "requires": {

+         "@babel/helper-create-class-features-plugin": "^7.4.4",

+         "@babel/helper-plugin-utils": "^7.0.0"

+       }

+     },

      "@babel/plugin-proposal-json-strings": {

        "version": "7.0.0",

        "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.0.0.tgz",
@@ -7202,6 +7311,14 @@ 

          "warning": "^3.0.0"

        }

      },

+     "react-switch": {

+       "version": "5.0.0",

+       "resolved": "https://registry.npmjs.org/react-switch/-/react-switch-5.0.0.tgz",

+       "integrity": "sha512-+zxY9xj9dMc8Y4gv/kkqQrirfEiIQ+SlQfJDW1Wi81L3xoh1fcbBYyJyh0TnhM/U/b6HxuBmkmU4Ooxgtuoavw==",

+       "requires": {

+         "prop-types": "^15.6.2"

+       }

+     },

      "react-transition-group": {

        "version": "2.5.1",

        "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.1.tgz",

@@ -19,6 +19,7 @@ 

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

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

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

+     "@babel/plugin-proposal-class-properties": "^7.0.0",

      "ajv": "^6.0.0",

      "audit-ci": "^1.7.0",

      "babel-eslint": "^9.0.0",
@@ -57,6 +58,7 @@ 

      "react-bootstrap": "0.32.4",

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

      "react-dom": "16.6.1",

+     "react-switch": "^5.0.0",

      "recompose": "0.30.0",

      "table-resolver": "4.1.1"

    }

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

  /* Main nav page index.html */

  .ds-content {

      padding: 0;

-     padding-top: 115px;

+     padding-top: 115px;  /* this pushes the content below fixed nav bar */

      padding-bottom: 50px;

      margin-top: 0;

      margin-left: 25px;
@@ -672,6 +672,11 @@ 

      padding-bottom: 10px;

  }

  

+ .ds-cipher-width {

+     max-width: 350px !important;

+     min-width: 350px !important;

+ }

+ 

  /*

   * Popup modal stuff

  */
@@ -1637,8 +1642,8 @@ 

      font-size: 16px;

  }

  

- .ds-no-padding () {

-     padding: 0 !imporant;

+ .ds-no-padding {

+     padding-right: 0 !important;

  }

  

  .alert {
@@ -1650,6 +1655,10 @@ 

      margin-top: 5%;

  }

  

+ .ds-select {

+     width: 120px;

+ }

+ 

  .treeview .list-group-item {

      /* remove focus border */

      outline: none;
@@ -1680,3 +1689,174 @@ 

  .ds-width-auto {

      width: 100%;

  }

+ 

+ /* Dual List CSS */

+ .dual-list-pf-arrows {

+     display: inline-block;

+     margin: auto;

+     position: relative;

+     bottom: 170px;

+     font-size: 23px;

+     color: #bbb;

+ }

+ @media only screen and (max-width: 600px) {

+     .dual-list-pf-arrows {

+         display: block;

+         position: inherit;

+         margin: 5px 0;

+         padding-left: 79px;

+     }

+ }

+ 

+ .dual-list-pf-arrows span {

+     display: block;

+     margin: 25px;

+     cursor: pointer;

+     transition: color 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);

+     transform: rotate(-90deg);

+ }

+ @media only screen and (max-width: 600px) {

+     .dual-list-pf-arrows span {

+         display: inline;

+         margin: 0 20px 0 0;

+     }

+ }

+ 

+ .dual-list-pf-arrows span:hover {

+       color: #8b8d8f;

+ }

+ 

+ .dual-list-pf-body {

+     height: 375px;

+     width: 320px;

+     overflow-y: scroll;

+     overflow-x: auto;

+     display: inline-grid;

+     align-content: flex-start;

+ }

+ 

+ .dual-list-pf-body::-webkit-scrollbar {

+     width: 12px;

+     height: 12px;

+     background: #fafafa;

+ }

+ 

+ .dual-list-pf-body::-webkit-scrollbar-thumb {

+     background: #d1d1d1;

+     border-radius: 6px;

+     border: 3px solid transparent;

+     background-clip: content-box;

+ }

+ 

+ .dual-list-pf-body::-webkit-scrollbar-thumb:hover {

+       background: #bbb;

+       border-radius: 6px;

+       border: 3px solid transparent;

+       background-clip: content-box;

+ }

+ 

+ .dual-list-pf-filter {

+     margin-left: 20px;

+ }

+ 

+ .dual-list-pf-filter input {

+     background-color: #f5f5f5;

+     border: 1px solid #ededed;

+     width: 145px;

+     padding: 0 22px 0 5px;

+     margin-top: 3px;

+     margin-bottom: 3px;

+ }

+ 

+ .dual-list-pf-filter .search-icon {

+     position: relative;

+     right: 20px;

+     bottom: 1px;

+     color: #bbb;

+ }

+ 

+ .dual-list-pf-filter ::-webkit-input-placeholder {

+     font-style: italic;

+ }

+ 

+ .dual-list-pf-footer {

+     padding: 10px;

+     border-top: 1px solid #d1d1d1;

+ }

+ 

+ .dual-list-pf-heading {

+     border-bottom: 1px solid #d1d1d1;

+ }

+ 

+ .dual-list-pf-item {

+     padding: 5px 0;

+     margin-bottom: 0;

+     font-weight: 400;

+     transition: background 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), color 0.3s ease-out;

+     cursor: pointer;

+     white-space: nowrap;

+ }

+ 

+ .dual-list-pf-item input[type='checkbox'] {

+     position: relative;

+     left: 10px;

+     vertical-align: top;

+     cursor: pointer;

+ }

+ 

+ .dual-list-pf-item.selected {

+     background-color: #0088ce;

+     color: white;

+ }

+ 

+ .dual-list-pf-item.disabled {

+     cursor: not-allowed;

+     background: #f5f5f5;

+     color: #8b8d8f;

+ }

+ 

+ .dual-list-pf-item.disabled input[type='checkbox'] {

+     cursor: not-allowed;

+ }

+ 

+ .dual-list-pf-item.child {

+     padding-left: 22px;

+ }

+ 

+ .dual-list-pf-item:hover:not(.selected):not(.disabled) {

+     background-color: #bee1f4;

+     color: inherit;

+ }

+ 

+ .dual-list-pf-item-label {

+     margin-left: 20px;

+ }

+ 

+ .dual-list-pf-main-checkbox {

+     position: relative;

+     left: 10px;

+     vertical-align: text-top;

+     cursor: pointer;

+ }

+ 

+ .dual-list-pf-no-items {

+     margin-top: 30px;

+     text-align: center;

+ }

+ 

+ .dual-list-pf-selector {

+     display: inline-block;

+     border: 1px solid #d1d1d1;

+     user-select: none;

+ }

+ 

+ .dual-list-pf-sort-icon {

+   cursor: pointer;

+ }

+ 

+ .dropdown-kebab-pf.btn-group {

+     margin-left: 10px;

+     float: right;

+     margin-right: 10px;

+ }

+ /* End of dual list */

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

   * to track the loading, and once all the pages are loaded, then we can load the config

   */

  var server_page_loaded = 0;

- var security_page_loaded = 0;

+ var security_page_loaded = 1;

  var db_page_loaded = 1;

  var repl_page_loaded = 0;

  var plugin_page_loaded = 1;
@@ -482,4 +482,8 @@ 

      $(".all-pages").hide();

      $("#monitor-content").show();

    });

+   $("#security-tab").on("click", function() {

+     $(".all-pages").hide();

+     $("#security-content").show();

+   });

  });

@@ -3,6 +3,7 @@ 

  import { Plugins } from "./plugins.jsx";

  import { Database } from "./database.jsx";

  import { Monitor } from "./monitor.jsx";

+ import { Security } from "./security.jsx";

  

  var serverIdElem;

  
@@ -35,6 +36,12 @@ 

          <Monitor serverId={serverIdElem} key={tabKey} />,

          document.getElementById("monitor")

      );

+ 

+     // Security tab

+     ReactDOM.render(

+         <Security serverId={serverIdElem} key={tabKey} />,

+         document.getElementById("security")

+     );

  }

  

  // We have to create the wrappers because this is no simple way

@@ -21,7 +21,6 @@ 

    <script src="ds.js"></script>

    <script src="schema.js"></script>

    <script src="servers.js"></script>

-   <script src="security.js"></script>

    <script src="replication.js"></script>

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

    <link href="static/jquery.dataTables.min.css" type="text/css" rel="stylesheet">
@@ -80,23 +79,10 @@ 

              </li>

  

              <!-- Security navtab -->

-             <li class="dropdown ds-nav-tab">

-               <a href="#0" class="dropdown-toggle ds-tab-list" data-toggle="dropdown" id="security-tab">

+             <li class="ds-nav-tab">

+               <a href="#0" class="ds-tab-list ds-tab-standalone" id="security-tab">

                  Security

-                 <b class="caret"></b>

                </a>

-               <ul class="dropdown-menu ds-nav-item">

-                 <li><a href="#0" class="ds-nav-choice" id="sec-config-btn" parent-id="security-tab">Security Settings</a></li>

-                 <li class="dropdown-submenu">

-                   <a tabindex="-1" href="#0">Certificate Management</a>

-                   <ul class="dropdown-menu">

-                     <li><a href="#0" class="ds-nav-choice" id="sec-cacert-btn"  parent-id="security-tab">CA Certificates</a></li>

-                     <li><a href="#0" class="ds-nav-choice" id="sec-srvcert-btn" parent-id="security-tab">Server Certificates</a></li>

-                     <li><a href="#0" class="ds-nav-choice" id="sec-revoked-btn" parent-id="security-tab">Revoked Certificates</a></li>

-                   </ul>

-                 </li>

-                 <li><a href="#0" class="ds-nav-choice" id="sec-ciphers-btn" parent-id="security-tab">Supported Ciphers</a></li>

-               </ul>

              </li>

  

              <!-- Database navtab -->
@@ -501,6 +487,7 @@ 

        </div>

  

        <div id="security-content" class="all-pages" hidden>

+           <div id="security"></div>

        </div>

  

        <div id="database-content" class="all-pages" hidden>

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

                      },

                      cell: {

                          props: {

-                             index: 2

+                             index: 1

                          },

                          formatters: [

                              (value, { rowData }) => {
@@ -75,6 +75,7 @@ 

              ]

          };

          this.getColumns = this.getColumns.bind(this);

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

      }

  

      getSingleColumn () {
@@ -345,6 +346,7 @@ 

              ]

          };

          this.getColumns = this.getColumns.bind(this);

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

      }

  

      getSingleColumn () {
@@ -508,6 +510,7 @@ 

          };

  

          this.getColumns = this.getColumns.bind(this);

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

      }

  

      getSingleColumn () {
@@ -703,6 +706,7 @@ 

          };

  

          this.getColumns = this.getColumns.bind(this);

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

      } // Constructor

  

      getColumns() {
@@ -877,6 +881,7 @@ 

          };

  

          this.getColumns = this.getColumns.bind(this);

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

      } // Constructor

  

      getColumns() {

@@ -0,0 +1,617 @@ 

+ import cockpit from "cockpit";

+ import React from "react";

+ import {

+     Nav,

+     NavItem,

+     TabContainer,

+     TabContent,

+     TabPane,

+     Button,

+     Spinner,

+     noop

+ } from "patternfly-react";

+ import { ConfirmPopup } from "../../lib/notifications.jsx";

+ import {

+     CertTable

+ } from "./securityTables.jsx";

+ import {

+     EditCertModal,

+     SecurityAddCertModal,

+     SecurityAddCACertModal,

+ } from "./securityModals.jsx";

+ import PropTypes from "prop-types";

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

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

+ 

+ export class CertificateManagement extends React.Component {

+     constructor(props) {

+         super(props);

+         this.state = {

+             activeKey: 1,

+             ServerCerts: this.props.ServerCerts,

+             CACerts: this.props.CACerts,

+             showEditModal: false,

+             showAddModal: false,

+             modalSpinner: false,

+             showConfirmDelete: false,

+             certName: "",

+             certFile: "",

+             flags: "",

+             errObj: {},

+             isCACert: false,

+             showConfirmCAChange: false,

+             loading: false,

+         };

+ 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+     }

+ 

+     handleNavSelect(key) {

+         this.setState({

+             activeKey: key

+         });

+     }

+ 

+     showAddModal () {

+         this.setState({

+             showAddModal: true,

+             errObj: {certName: true, certFile: true}

+         });

+     }

+ 

+     closeAddModal () {

+         this.setState({

+             showAddModal: false,

+             certName: "",

+             certFile: "",

+         });

+     }

+ 

+     showAddCAModal () {

+         this.setState({

+             showAddCAModal: true,

+             errObj: {certName: true, certFile: true}

+         });

+     }

+ 

+     closeAddCAModal () {

+         this.setState({

+             showAddCAModal: false,

+             certName: "",

+             certFile: "",

+         });

+     }

+ 

+     addCert () {

+         if (this.state.certName == "") {

+             this.props.addNotification(

+                 "warning",

+                 `Missing certificate nickname`

+             );

+             return;

+         } else if (this.state.certFile == "") {

+             this.props.addNotification(

+                 "warning",

+                 `Missing certificate file name`

+             );

+             return;

+         }

+ 

+         this.setState({

+             modalSpinner: true,

+             loading: true,

+         });

+         const cmd = [

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

+             "security", "certificate", "add", "--name=" + this.state.certName, "--file=" + this.state.certFile

+         ];

+         log_cmd("addCert", "Adding server cert", cmd);

+         cockpit

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

+                 .done(() => {

+                     this.reloadCACerts();

+                     this.setState({

+                         showAddModal: false,

+                         certFile: '',

+                         certName: '',

+                         modalSpinner: false

+                     });

+                     this.props.addNotification(

+                         "success",

+                         `Successfully added certificate`

+                     );

+                 })

+                 .fail(err => {

+                     let errMsg = JSON.parse(err);

+                     let msg = errMsg.desc;

+                     if ('info' in errMsg) {

+                         msg = errMsg.desc + " - " + errMsg.info;

+                     }

+                     this.setState({

+                         modalSpinner: false,

+                         loading: false,

+                     });

+                     this.props.addNotification(

+                         "error",

+                         `Error adding certificate - ${msg}`

+                     );

+                 });

+     }

+ 

+     addCACert () {

+         if (this.state.certName == "") {

+             this.props.addNotification(

+                 "warning",

+                 `Missing certificate nickname`

+             );

+             return;

+         } else if (this.state.certFile == "") {

+             this.props.addNotification(

+                 "warning",

+                 `Missing certificate file name`

+             );

+             return;

+         }

+ 

+         this.setState({

+             modalSpinner: true,

+             loading: true,

+         });

+         const cmd = [

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

+             "security", "ca-certificate", "add", "--name=" + this.state.certName, "--file=" + this.state.certFile

+         ];

+         log_cmd("addCACert", "Adding CA certificate", cmd);

+         cockpit

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

+                 .done(() => {

+                     this.reloadCACerts();

+                     this.setState({

+                         showAddCAModal: false,

+                         certFile: '',

+                         certName: '',

+                         modalSpinner: false,

+                     });

+                     this.props.addNotification(

+                         "success",

+                         `Successfully added certificate`

+                     );

+                 })

+                 .fail(err => {

+                     let errMsg = JSON.parse(err);

+                     let msg = errMsg.desc;

+                     if ('info' in errMsg) {

+                         msg = errMsg.desc + " - " + errMsg.info;

+                     }

+                     this.setState({

+                         modalSpinner: false,

+                         loading: false,

+                     });

+                     this.props.addNotification(

+                         "error",

+                         `Error adding certificate - ${msg}`

+                     );

+                 });

+     }

+ 

+     showDeleteConfirm(dataRow) {

+         this.setState({

+             showConfirmDelete: true,

+             certName: dataRow.nickname[0],

+         });

+     }

+ 

+     delCert () {

+         this.setState({

+             modalSpinner: true,

+             loading: true

+         });

+         const cmd = [

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

+             "security", "certificate", "del", this.state.certName

+         ];

+         log_cmd("delCert", "Deleting certificate", cmd);

+         cockpit

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

+                 .done(() => {

+                     this.reloadCACerts();

+                     this.setState({

+                         certName: '',

+                         modalSpinner: false,

+                         showConfirmDelete: false,

+                     });

+                     this.props.addNotification(

+                         "success",

+                         `Successfully deleted certificate`

+                     );

+                 })

+                 .fail(err => {

+                     let errMsg = JSON.parse(err);

+                     let msg = errMsg.desc;

+                     if ('info' in errMsg) {

+                         msg = errMsg.desc + " - " + errMsg.info;

+                     }

+                     this.setState({

+                         certName: '',

+                         modalSpinner: false,

+                         loading: false,

+                     });

+                     this.props.addNotification(

+                         "error",

+                         `Error deleting certificate - ${msg}`

+                     );

+                 });

+     }

+ 

+     showEditModal (rowData) {

+         this.setState({

+             showEditModal: true,

+             certName: rowData.nickname[0],

+             flags: rowData.flags[0],

+             isCACert: false,

+         });

+     }

+ 

+     closeEditModal () {

+         this.setState({

+             showEditModal: false,

+             flags: ''

+         });

+     }

+ 

+     showEditCAModal (rowData) {

+         this.setState({

+             showEditModal: true,

+             certName: rowData.nickname[0],

+             flags: rowData.flags[0],

+             isCACert: true,

+         });

+     }

+ 

+     editCert () {

+         // Check if CA cert flags were removed

+         if (this.state.isCACert) {

+             let SSLFlags = '';

+             SSLFlags = this.state.flags.split(',', 1);

+             if (!SSLFlags[0].includes('C') || !SSLFlags[0].includes('T')) {

+                 // This could remove the CA cert properties, better warn user

+                 this.setState({

+                     showConfirmCAChange: true

+                 });

+                 return;

+             }

+         }

+         this.doEditCert();

+     }

+ 

+     closeConfirmCAChange () {

+         this.setState({

+             showConfirmCAChange: false

+         });

+     }

+ 

+     doEditCert () {

+         this.setState({

+             modalSpinner: true,

+             loading: true,

+         });

+         const cmd = [

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

+             "security", "certificate", "set-trust-flags", this.state.certName, "--flags=" + this.state.flags

+         ];

+         log_cmd("doEditCert", "Editing trust flags", cmd);

+         cockpit

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

+                 .done(() => {

+                     this.reloadCACerts();

+                     this.setState({

+                         showEditModal: false,

+                         flags: '',

+                         certName: '',

+                         modalSpinner: false,

+                     });

+                     this.props.addNotification(

+                         "success",

+                         `Successfully changed certificate's trust flags`

+                     );

+                 })

+                 .fail(err => {

+                     let errMsg = JSON.parse(err);

+                     let msg = errMsg.desc;

+                     if ('info' in errMsg) {

+                         msg = errMsg.desc + " - " + errMsg.info;

+                     }

+                     this.setState({

+                         showEditModal: false,

+                         flags: '',

+                         certName: '',

+                         modalSpinner: false,

+                         loading: false,

+                     });

+                     this.props.addNotification(

+                         "error",

+                         `Error setting trust flags - ${msg}`

+                     );

+                 });

+     }

+ 

+     handleAddChange (e) {

+         const value = e.target.value;

+         let valueErr = false;

+         let errObj = this.state.errObj;

+ 

+         if (value == "") {

+             valueErr = true;

+         }

+         errObj[e.target.id] = valueErr;

+         this.setState({

+             [e.target.id]: value,

+             errObj: errObj

+         });

+     }

+ 

+     handleFlagChange (e) {

+         const checked = e.target.checked;

+         const id = e.target.id;

+         let flags = this.state.flags;

+         let SSLFlags = '';

+         let EmailFlags = '';

+         let OSFlags = '';

+         [SSLFlags, EmailFlags, OSFlags] = flags.split(',');

+ 

+         if (id.endsWith('SSL')) {

+             for (let trustFlag of ['C', 'T', 'c', 'P', 'p']) {

+                 if (id.startsWith(trustFlag)) {

+                     if (checked) {

+                         SSLFlags += trustFlag;

+                     } else {

+                         SSLFlags = SSLFlags.replace(trustFlag, '');

+                     }

+                 }

+             }

+         } else if (id.endsWith('Email')) {

+             for (let trustFlag of ['C', 'T', 'c', 'P', 'p']) {

+                 if (id.startsWith(trustFlag)) {

+                     if (checked) {

+                         EmailFlags += trustFlag;

+                     } else {

+                         EmailFlags = EmailFlags.replace(trustFlag, '');

+                     }

+                 }

+             }

+         } else {

+             // Object Signing (OS)

+             for (let trustFlag of ['C', 'T', 'c', 'P', 'p']) {

+                 if (id.startsWith(trustFlag)) {

+                     if (checked) {

+                         OSFlags += trustFlag;

+                     } else {

+                         OSFlags = OSFlags.replace(trustFlag, '');

+                     }

+                 }

+             }

+         }

+         this.setState({

+             flags: SSLFlags + "," + EmailFlags + "," + OSFlags

+         });

+     }

+ 

+     closeConfirmDelete () {

+         this.setState({

+             showConfirmDelete: false,

+         });

+     }

+ 

+     reloadCerts () {

+         const cmd = [

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

+             "security", "certificate", "list",

+         ];

+         log_cmd("reloadCerts", "Load certificates", cmd);

+         cockpit

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

+                 .done(content => {

+                     const certs = JSON.parse(content);

+                     let certNames = [];

+                     for (let cert of certs) {

+                         certNames.push(cert.attrs['nickname']);

+                     }

+                     this.setState({

+                         ServerCerts: certs,

+                         loading: false,

+                     });

+                 })

+                 .fail(err => {

+                     let errMsg = JSON.parse(err);

+                     let msg = errMsg.desc;

+                     if ('info' in errMsg) {

+                         msg = errMsg.desc + " - " + errMsg.info;

+                     }

+                     this.props.addNotification(

+                         "error",

+                         `Error loading server certificates - ${msg}`

+                     );

+                 });

+     }

+ 

+     reloadCACerts () {

+         const cmd = [

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

+             "security", "ca-certificate", "list",

+         ];

+         log_cmd("reloadCACerts", "Load certificates", cmd);

+         cockpit

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

+                 .done(content => {

+                     let certs = JSON.parse(content);

+                     this.setState({

+                         CACerts: certs,

+                         loading: false

+                     }, this.reloadCerts);

+                 })

+                 .fail(err => {

+                     let errMsg = JSON.parse(err);

+                     let msg = errMsg.desc;

+                     if ('info' in errMsg) {

+                         msg = errMsg.desc + " - " + errMsg.info;

+                     }

+                     this.props.addNotification(

+                         "error",

+                         `Error loading CA certificates - ${msg}`

+                     );

+                 });

+     }

+ 

+     render () {

+         let CATitle = 'Trusted Certificate Authorites <font size="1">(' + this.state.CACerts.length + ')</font>';

+         let ServerTitle = 'TLS Certificates <font size="1">(' + this.state.ServerCerts.length + ')</font>';

+ 

+         let certificatePage = '';

+ 

+         if (this.state.loading) {

+             certificatePage =

+                 <div className="ds-loading-spinner ds-center">

+                     <p />

+                     <h4>Loading certificates ...</h4>

+                     <Spinner loading size="md" />

+                 </div>;

+         } else {

+             certificatePage =

+                 <div className="container-fluid">

+                     <div className="ds-tab-table">

+                         <TabContainer id="basic-tabs-pf" onSelect={this.handleNavSelect} activeKey={this.state.activeKey}>

+                             <div>

+                                 <Nav bsClass="nav nav-tabs nav-tabs-pf">

+                                     <NavItem eventKey={1}>

+                                         <div dangerouslySetInnerHTML={{__html: CATitle}} />

+                                     </NavItem>

+                                     <NavItem eventKey={2}>

+                                         <div dangerouslySetInnerHTML={{__html: ServerTitle}} />

+                                     </NavItem>

+                                 </Nav>

+                                 <TabContent>

+                                     <TabPane eventKey={1}>

+                                         <div className="ds-margin-top-lg">

+                                             <CertTable

+                                                 certs={this.state.CACerts}

+                                                 key={this.state.CACerts}

+                                                 editCert={this.showEditCAModal}

+                                                 delCert={this.showDeleteConfirm}

+                                             />

+                                             <Button

+                                                 bsStyle="primary"

+                                                 className="ds-margin-top-med"

+                                                 onClick={() => {

+                                                     this.showAddCAModal();

+                                                 }}

+                                             >

+                                                 Add CA Certificate

+                                             </Button>

+                                         </div>

+                                     </TabPane>

+                                     <TabPane eventKey={2}>

+                                         <div className="ds-margin-top-lg">

+                                             <CertTable

+                                                 certs={this.state.ServerCerts}

+                                                 key={this.state.ServerCerts}

+                                                 editCert={this.showEditModal}

+                                                 delCert={this.showDeleteConfirm}

+                                             />

+                                             <Button

+                                                 bsStyle="primary"

+                                                 className="ds-margin-top-med"

+                                                 onClick={() => {

+                                                     this.showAddModal();

+                                                 }}

+                                             >

+                                                 Add Server Certificate

+                                             </Button>

+                                         </div>

+                                     </TabPane>

+                                 </TabContent>

+                             </div>

+                         </TabContainer>

+                     </div>

+                 </div>;

+         }

+         return (

+             <div>

+                 {certificatePage}

+                 <EditCertModal

+                     showModal={this.state.showEditModal}

+                     closeHandler={this.closeEditModal}

+                     handleChange={this.handleFlagChange}

+                     saveHandler={this.editCert}

+                     flags={this.state.flags}

+                     spinning={this.state.modalSpinner}

+                 />

+                 <SecurityAddCertModal

+                     showModal={this.state.showAddModal}

+                     closeHandler={this.closeAddModal}

+                     handleChange={this.handleAddChange}

+                     saveHandler={this.addCert}

+                     spinning={this.state.modalSpinner}

+                     error={this.state.errObj}

+                 />

+                 <SecurityAddCACertModal

+                     showModal={this.state.showAddCAModal}

+                     closeHandler={this.closeAddCAModal}

+                     handleChange={this.handleAddChange}

+                     saveHandler={this.addCACert}

+                     spinning={this.state.modalSpinner}

+                     error={this.state.errObj}

+                 />

+                 <ConfirmPopup

+                     showModal={this.state.showConfirmDelete}

+                     closeHandler={this.closeConfirmDelete}

+                     actionFunc={this.delCert}

+                     msg="Are you sure you want to delete this certificate?"

+                     msgContent={this.state.certName}

+                 />

+                 <ConfirmPopup

+                     showModal={this.state.showConfirmCAChange}

+                     closeHandler={this.closeConfirmCAChange}

+                     actionFunc={this.doEditCert}

+                     msg="Removing the 'C' or 'T' flags from the SSL trust catagory could break all TLS connectivity to and from the server, are you sure you want to proceed?"

+                 />

+             </div>

+         );

+     }

+ }

+ 

+ // Props and defaults

+ 

+ CertificateManagement.propTypes = {

+     serverId: PropTypes.string,

+     CACerts: PropTypes.array,

+     ServerCerts: PropTypes.array,

+     addNotification: PropTypes.func,

+ };

+ 

+ CertificateManagement.defaultProps = {

+     serverId: "",

+     CACerts: [],

+     ServerCerts: [],

+     addNotification: noop,

+ };

+ 

+ export default CertificateManagement;

@@ -0,0 +1,274 @@ 

+ import React from "react";

+ import cockpit from "cockpit";

+ import {

+     Button,

+     Row,

+     Col,

+     ControlLabel,

+     Spinner,

+     noop,

+ } from "patternfly-react";

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

+ import PropTypes from "prop-types";

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

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

+ 

+ export class Ciphers extends React.Component {

+     constructor(props) {

+         super(props);

+         this.state = {

+             allowCiphers: [],

+             denyCiphers: [],

+             cipherPref: "default",

+             prefs: this.props.cipherPref,

+             saving: false,

+         };

+ 

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

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

+     }

+ 

+     componentWillMount () {

+         let cipherPref = "default";

+         let allowedCiphers = [];

+         let deniedCiphers = [];

+ 

+         // Parse SSL cipher attributes (nsSSL3Ciphers)

+         if (this.props.cipherPref != "") {

+             let rawCiphers = this.props.cipherPref.split(",");

+ 

+             // First check the first element as it has special meaning

+             if (rawCiphers[0].toLowerCase() == "default") {

+                 rawCiphers.shift();

+             } else if (rawCiphers[0].toLowerCase() == "+all") {

+                 cipherPref = "+all";

+                 rawCiphers.shift();

+             } else if (rawCiphers[0].toLowerCase() == "-all") {

+                 cipherPref = "-all";

+                 rawCiphers.shift();

+             }

+ 

+             // Process the remaining ciphers

+             rawCiphers = rawCiphers.map(function(x) { return x.toUpperCase() });

+             for (let cipher of rawCiphers) {

+                 if (cipher.startsWith("+")) {

+                     allowedCiphers.push(cipher.substring(1));

+                 } else if (cipher.startsWith("-")) {

+                     deniedCiphers.push(cipher.substring(1));

+                 }

+             }

+         }

+ 

+         this.setState({

+             cipherPref: cipherPref,

+             allowCiphers: allowedCiphers,

+             denyCiphers: deniedCiphers,

+         });

+     }

+ 

+     saveCipherPref () {

+         /* start the spinner */

+         this.setState({

+             saving: true

+         });

+         let prefs = this.state.cipherPref;

+         for (let cipher of this.state.allowCiphers) {

+             prefs += ",+" + cipher;

+         }

+         for (let cipher of this.state.denyCiphers) {

+             prefs += ",-" + cipher;

+         }

+ 

+         const cmd = [

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

+             "security", "ciphers", "set", "--", prefs

+         ];

+         log_cmd("saveCipherPref", "Saving cipher preferences", cmd);

+         cockpit

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

+                 .done(() => {

+                     this.props.addNotification(

+                         "success",

+                         `Successfully set cipher preferences.  You must restart the server for these changes to take effect.`

+                     );

+                     this.setState({

+                         saving: false,

+                     });

+                 })

+                 .fail(err => {

+                     let errMsg = JSON.parse(err);

+                     let msg = errMsg.desc;

+                     if ('info' in errMsg) {

+                         msg = errMsg.desc + " - " + errMsg.info;

+                     }

+                     this.props.addNotification(

+                         "error",

+                         `Error setting cipher preferences - ${msg}`

+                     );

+                     this.setState({

+                         saving: false,

+                     });

+                 });

+     }

+ 

+     handlePrefChange (e) {

+         this.setState({

+             cipherPref: e.target.value,

+         });

+     }

+ 

+     render () {

+         let supportedCiphers = [];

+         let enabledCiphers = [];

+         let cipherPage;

+ 

+         for (let cipher of this.props.supportedCiphers) {

+             if (!this.props.enabledCiphers.includes(cipher)) {

+                 // This cipher is not currently enabled, so list it as available

+                 supportedCiphers.push(cipher);

+             }

+         }

+         for (let cipher of this.props.enabledCiphers) {

+             enabledCiphers.push(cipher);

+         }

+         let supportedList = supportedCiphers.map((name) =>

+             <option key={name}>{name}</option>

+         );

+         let enabledList = enabledCiphers.map((name) =>

+             <option key={name}>{name}</option>

+         );

+ 

+         if (this.state.saving) {

+             cipherPage =

+                 <div className="ds-loading-spinner ds-center">

+                     <p />

+                     <h4>Saving cipher preferences ...</h4>

+                     <Spinner loading size="md" />

+                 </div>;

+         } else {

+             cipherPage =

+                 <div className="container-fluid">

+                     <div className="ds-container">

+                         <div className='ds-inline'>

+                             <div>

+                                 <h4>Enabled Ciphers</h4>

+                             </div>

+                             <div>

+                                 <select

+                                     className="ds-cipher-width"

+                                     size="16"

+                                     title="The current ciphers the server is accepting.  This is only updated after a server restart"

+                                 >

+                                     {enabledList}

+                                 </select>

+                             </div>

+                         </div>

+                         <div className="ds-divider-lrg" />

+                         <div className='ds-inline'>

+                             <div>

+                                 <h4>Other Available Ciphers</h4>

+                             </div>

+                             <div>

+                                 <select className="ds-cipher-width" size="16">

+                                     {supportedList}

+                                 </select>

+                             </div>

+                         </div>

+                     </div>

+                     <hr />

+                     <Row>

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

+                              Cipher Suite

+                         </Col>

+                         <Col sm={9}>

+                             <select

+                                 id="cipherPref"

+                                 onChange={this.handlePrefChange}

+                                 defaultValue={this.state.cipherPref}

+                             >

+                                 <option title="default" value="default" key="default">Default Ciphers</option>

+                                 <option title="+all" value="+all" key="all">All Ciphers</option>

+                                 <option title="-all" value="-all" key="none">No Ciphers</option>

+                             </select>

+                         </Col>

+                     </Row>

+                     <Row className="ds-margin-top">

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

+                              Allow Specific Ciphers

+                         </Col>

+                         <Col sm={9}>

+                             <Typeahead

+                                 multiple

+                                 onChange={value => {

+                                     this.setState({

+                                         allowCiphers: value

+                                     });

+                                 }}

+                                 selected={this.state.allowCiphers}

+                                 options={this.props.supportedCiphers}

+                                 newSelectionPrefix="Add a cipher: "

+                                 placeholder="Type a cipher..."

+                                 id="allowCipher"

+                             />

+                         </Col>

+                     </Row>

+                     <Row className="ds-margin-top">

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

+                              Deny Specific Ciphers

+                         </Col>

+                         <Col sm={9}>

+                             <Typeahead

+                                 multiple

+                                 onChange={value => {

+                                     this.setState({

+                                         denyCiphers: value

+                                     });

+                                 }}

+                                 selected={this.state.denyCiphers}

+                                 options={this.props.supportedCiphers}

+                                 newSelectionPrefix="Add a cipher: "

+                                 placeholder="Type a cipher..."

+                                 id="denyCipher"

+                             />

+                         </Col>

+                     </Row>

+                     <p />

+                     <Button

+                         bsStyle="primary"

+                         className="ds-margin-top"

+                         onClick={() => {

+                             this.saveCipherPref();

+                         }}

+                     >

+                         Save Cipher Preferences

+                     </Button>

+                 </div>;

+         }

+ 

+         return (

+             <div>

+                 {cipherPage}

+             </div>

+         );

+     }

+ }

+ 

+ // Props and defaults

+ 

+ Ciphers.propTypes = {

+     serverId: PropTypes.string,

+     supportedCiphers: PropTypes.array,

+     enabledCiphers: PropTypes.array,

+     cipherPref: PropTypes.string,

+     addNotification: PropTypes.func,

+ };

+ 

+ Ciphers.defaultProps = {

+     serverId: "",

+     supportedCiphers: [],

+     enabledCiphers: [],

+     cipherPref: "",

+     addNotification: noop,

+ };

+ 

+ export default Ciphers;

@@ -0,0 +1,689 @@ 

+ import React from "react";

+ import {

+     Modal,

+     Row,

+     Col,

+     ControlLabel,

+     Checkbox,

+     FormControl,

+     Icon,

+     Button,

+     Form,

+     Spinner,

+     noop

+ } from "patternfly-react";

+ import PropTypes from "prop-types";

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

+ 

+ export class SecurityAddCACertModal extends React.Component {

+     render() {

+         const {

+             showModal,

+             closeHandler,

+             handleChange,

+             saveHandler,

+             spinning,

+             error

+         } = this.props;

+ 

+         let spinner = "";

+         if (spinning) {

+             spinner =

+                 <Row>

+                     <div className="ds-modal-spinner">

+                         <Spinner loading inline size="lg" />Adding CA certificate...

+                     </div>

+                 </Row>;

+         }

+ 

+         return (

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

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

+                     <Modal.Header>

+                         <button

+                             className="close"

+                             onClick={closeHandler}

+                             aria-hidden="true"

+                             aria-label="Close"

+                         >

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

+                         </button>

+                         <Modal.Title>

+                             Add Certificate Authority

+                         </Modal.Title>

+                     </Modal.Header>

+                     <Modal.Body>

+                         <Form horizontal autoComplete="off">

+                             <h4>

+                                 Add CA certificate to the security database.

+                             </h4>

+                             <hr />

+                             <Row title="Enter full path to and and including certificate file name">

+                                 <Col sm={4}>

+                                     <ControlLabel>Certificate File</ControlLabel>

+                                 </Col>

+                                 <Col sm={8}>

+                                     <FormControl

+                                         type="text"

+                                         id="certFile"

+                                         className={error.certFile ? "ds-input-bad" : ""}

+                                         onChange={handleChange}

+                                     />

+                                 </Col>

+                             </Row>

+                             <p />

+                             <Row title="Enter name/nickname of the certificate">

+                                 <Col sm={4}>

+                                     <ControlLabel>Certificate Nickname</ControlLabel>

+                                 </Col>

+                                 <Col sm={8}>

+                                     <FormControl

+                                         type="text"

+                                         id="certName"

+                                         className={error.certName ? "ds-input-bad" : ""}

+                                         onChange={handleChange}

+                                     />

+                                 </Col>

+                             </Row>

+                             <p />

+                             {spinner}

+                         </Form>

+                     </Modal.Body>

+                     <Modal.Footer>