This is an automated email from the git hooks/post-receive script.
mreynolds pushed a commit to branch master in repository 389-ds-base.
The following commit(s) were added to refs/heads/master by this push: new a77abdb Issue 50325 - Add Security tab to UI a77abdb is described below
commit a77abdbc4fc9fbc846d6827e28e0d3fb4eb81fe0 Author: Mark Reynolds mreynolds@redhat.com AuthorDate: Wed Jul 3 16:36:53 2019 -0400
Issue 50325 - Add Security tab to UI
Description: This updates the CLI and UI to handle a majority of the security configuration. It also adds support for PF dual list selection even though I ended up not using it.
Relates: https://pagure.io/389-ds-base/issue/50325
Reviewed by: spichugi, and mhonek (Thanks!!)
Fixed Simon's issues
Fix issue with listing certs with spaces in the name
Fix npm vulnerabilities
Fix selinux port labeling, and add 'saving' spinners
Use a regex for parsing certutil output --- src/cockpit/389-console/.babelrc | 5 +- src/cockpit/389-console/package-lock.json | 117 +++ src/cockpit/389-console/package.json | 2 + src/cockpit/389-console/src/css/ds.css | 186 ++++- src/cockpit/389-console/src/ds.js | 6 +- src/cockpit/389-console/src/index.es6 | 7 + src/cockpit/389-console/src/index.html | 19 +- .../src/lib/database/databaseTables.jsx | 7 +- .../src/lib/security/certificateManagement.jsx | 617 +++++++++++++++ .../389-console/src/lib/security/ciphers.jsx | 274 +++++++ .../src/lib/security/securityModals.jsx | 689 +++++++++++++++++ .../src/lib/security/securityTables.jsx | 454 +++++++++++ src/cockpit/389-console/src/lib/tools.jsx | 18 +- src/cockpit/389-console/src/security.html | 502 ------------ src/cockpit/389-console/src/security.js | 137 ---- src/cockpit/389-console/src/security.jsx | 853 +++++++++++++++++++++ src/cockpit/389-console/webpack.config.js | 15 +- src/lib389/cli/dsconf | 3 - src/lib389/lib389/__init__.py | 27 +- src/lib389/lib389/cli_conf/security.py | 298 ++++++- src/lib389/lib389/config.py | 28 +- src/lib389/lib389/nss_ssl.py | 156 +++- src/lib389/lib389/utils.py | 8 +- 23 files changed, 3674 insertions(+), 754 deletions(-)
diff --git a/src/cockpit/389-console/.babelrc b/src/cockpit/389-console/.babelrc index d0ef093..23c75c5 100644 --- a/src/cockpit/389-console/.babelrc +++ b/src/cockpit/389-console/.babelrc @@ -1,4 +1,7 @@ { "presets": ["@babel/env", - "@babel/preset-react"] + "@babel/preset-react"], + "plugins": [ + "@babel/plugin-proposal-class-properties" + ] } diff --git a/src/cockpit/389-console/package-lock.json b/src/cockpit/389-console/package-lock.json index 67f170c..9fec71c 100644 --- a/src/cockpit/389-console/package-lock.json +++ b/src/cockpit/389-console/package-lock.json @@ -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/-/help...", + "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-sup...", + "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-s...", + "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....", @@ -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-...", + "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-prop...", @@ -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...", diff --git a/src/cockpit/389-console/package.json b/src/cockpit/389-console/package.json index 7d6fb4e..ddf44b5 100644 --- a/src/cockpit/389-console/package.json +++ b/src/cockpit/389-console/package.json @@ -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" } diff --git a/src/cockpit/389-console/src/css/ds.css b/src/cockpit/389-console/src/css/ds.css index 1ad8d5c..f8945bf 100644 --- a/src/cockpit/389-console/src/css/ds.css +++ b/src/cockpit/389-console/src/css/ds.css @@ -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 @@ td { padding-bottom: 10px; }
+.ds-cipher-width { + max-width: 350px !important; + min-width: 350px !important; +} + /* * Popup modal stuff */ @@ -1637,8 +1642,8 @@ option { font-size: 16px; }
-.ds-no-padding () { - padding: 0 !imporant; +.ds-no-padding { + padding-right: 0 !important; }
.alert { @@ -1650,6 +1655,10 @@ option { margin-top: 5%; }
+.ds-select { + width: 120px; +} + .treeview .list-group-item { /* remove focus border */ outline: none; @@ -1680,3 +1689,174 @@ input { .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 */ diff --git a/src/cockpit/389-console/src/ds.js b/src/cockpit/389-console/src/ds.js index ec57488..e023fdc 100644 --- a/src/cockpit/389-console/src/ds.js +++ b/src/cockpit/389-console/src/ds.js @@ -8,7 +8,7 @@ var dn_regex = new RegExp( "^([A-Za-z]+=.*)" ); * 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 @@ $(window.document).ready(function() { $(".all-pages").hide(); $("#monitor-content").show(); }); + $("#security-tab").on("click", function() { + $(".all-pages").hide(); + $("#security-content").show(); + }); }); diff --git a/src/cockpit/389-console/src/index.es6 b/src/cockpit/389-console/src/index.es6 index 0483752..71e9c5e 100644 --- a/src/cockpit/389-console/src/index.es6 +++ b/src/cockpit/389-console/src/index.es6 @@ -3,6 +3,7 @@ import ReactDOM from "react-dom"; 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 @@ function renderReactDOM(clear) { <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 diff --git a/src/cockpit/389-console/src/index.html b/src/cockpit/389-console/src/index.html index 7c5dbf8..c93f754 100644 --- a/src/cockpit/389-console/src/index.html +++ b/src/cockpit/389-console/src/index.html @@ -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> diff --git a/src/cockpit/389-console/src/lib/database/databaseTables.jsx b/src/cockpit/389-console/src/lib/database/databaseTables.jsx index adf535e..5d90f02 100644 --- a/src/cockpit/389-console/src/lib/database/databaseTables.jsx +++ b/src/cockpit/389-console/src/lib/database/databaseTables.jsx @@ -53,7 +53,7 @@ class ReferralTable extends React.Component { }, cell: { props: { - index: 2 + index: 1 }, formatters: [ (value, { rowData }) => { @@ -75,6 +75,7 @@ class ReferralTable extends React.Component { ] }; this.getColumns = this.getColumns.bind(this); + this.getSingleColumn = this.getSingleColumn.bind(this); }
getSingleColumn () { @@ -345,6 +346,7 @@ class EncryptedAttrTable extends React.Component { ] }; this.getColumns = this.getColumns.bind(this); + this.getSingleColumn = this.getSingleColumn.bind(this); }
getSingleColumn () { @@ -508,6 +510,7 @@ class LDIFTable extends React.Component { };
this.getColumns = this.getColumns.bind(this); + this.getSingleColumn = this.getSingleColumn.bind(this); }
getSingleColumn () { @@ -703,6 +706,7 @@ class LDIFManageTable extends React.Component { };
this.getColumns = this.getColumns.bind(this); + this.getSingleColumn = this.getSingleColumn.bind(this); } // Constructor
getColumns() { @@ -877,6 +881,7 @@ class BackupTable extends React.Component { };
this.getColumns = this.getColumns.bind(this); + this.getSingleColumn = this.getSingleColumn.bind(this); } // Constructor
getColumns() { diff --git a/src/cockpit/389-console/src/lib/security/certificateManagement.jsx b/src/cockpit/389-console/src/lib/security/certificateManagement.jsx new file mode 100644 index 0000000..d5cd927 --- /dev/null +++ b/src/cockpit/389-console/src/lib/security/certificateManagement.jsx @@ -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; diff --git a/src/cockpit/389-console/src/lib/security/ciphers.jsx b/src/cockpit/389-console/src/lib/security/ciphers.jsx new file mode 100644 index 0000000..4714fcb --- /dev/null +++ b/src/cockpit/389-console/src/lib/security/ciphers.jsx @@ -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; diff --git a/src/cockpit/389-console/src/lib/security/securityModals.jsx b/src/cockpit/389-console/src/lib/security/securityModals.jsx new file mode 100644 index 0000000..f8ad49c --- /dev/null +++ b/src/cockpit/389-console/src/lib/security/securityModals.jsx @@ -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> + <Button + bsStyle="default" + className="btn-cancel" + onClick={closeHandler} + > + Cancel + </Button> + <Button + bsStyle="primary" + onClick={saveHandler} + > + Add Certificate + </Button> + </Modal.Footer> + </div> + </Modal> + ); + } +} + +export class SecurityAddCertModal 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 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 + </Modal.Title> + </Modal.Header> + <Modal.Body> + <Form horizontal autoComplete="off"> + <h4> + Add 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> + <Button + bsStyle="default" + className="btn-cancel" + onClick={closeHandler} + > + Cancel + </Button> + <Button + bsStyle="primary" + onClick={saveHandler} + > + Add Certificate + </Button> + </Modal.Footer> + </div> + </Modal> + ); + } +} + +export class SecurityEnableModal extends React.Component { + render() { + const { + showModal, + closeHandler, + handleChange, + saveHandler, + primaryName, + certs, + spinning + } = this.props; + + // Build list of cert names for the select list + let certNames = []; + for (let cert of certs) { + certNames.push(cert.attrs['nickname']); + } + let certNameOptions = certNames.map((name) => + <option key={name} value={name}>{name}</option> + ); + let spinner = ""; + if (spinning) { + spinner = + <Row> + <div className="ds-modal-spinner"> + <Spinner loading inline size="lg" />Enabling security... + </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> + Enable Security + </Modal.Title> + </Modal.Header> + <Modal.Body> + <Form horizontal autoComplete="off"> + <h4> + You are choosing to enable security for the Directory Server which + allows the server to accept incoming client TLS connections. Please + select which certificate the server should use. + </h4> + <hr /> + <Row className="ds-margin-top" title="The server certificate the Directory Server will use"> + <Col sm={4}> + <ControlLabel>Available Certificates</ControlLabel> + </Col> + <Col sm={8}> + <select id="certNameSelect" onChange={handleChange} defaultValue={primaryName}> + {certNameOptions} + </select> + </Col> + </Row> + <p /> + {spinner} + </Form> + </Modal.Body> + <Modal.Footer> + <Button + bsStyle="default" + className="btn-cancel" + onClick={closeHandler} + > + Cancel + </Button> + <Button + bsStyle="primary" + onClick={saveHandler} + > + Enable Security + </Button> + </Modal.Footer> + </div> + </Modal> + ); + } +} + +export class EditCertModal extends React.Component { + render() { + const { + showModal, + closeHandler, + handleChange, + saveHandler, + flags, + spinning + } = this.props; + + let spinner = ""; + if (spinning) { + spinner = + <Row> + <div className="ds-modal-spinner"> + <Spinner loading inline size="lg" />Saving certificate... + </div> + </Row>; + } + + // Process the cert flags + let CSSLChecked = false; + let CEmailChecked = false; + let COSChecked = false; + let TSSLChecked = false; + let TEmailChecked = false; + let TOSChecked = false; + let cSSLChecked = false; + let cEmailChecked = false; + let cOSChecked = false; + let PSSLChecked = false; + let PEmailChecked = false; + let POSChecked = false; + let pSSLChecked = false; + let pEmailChecked = false; + let pOSChecked = false; + let uSSLChecked = false; + let uEmailChecked = false; + let uOSChecked = false; + let SSLFlags = ''; + let EmailFlags = ''; + let OSFlags = ''; + if (flags != "") { + [SSLFlags, EmailFlags, OSFlags] = flags.split(','); + if (SSLFlags.includes('T')) { + TSSLChecked = true; + } + if (EmailFlags.includes('T')) { + TEmailChecked = true; + } + if (OSFlags.includes('T')) { + TOSChecked = true; + } + if (SSLFlags.includes('C')) { + CSSLChecked = true; + } + if (EmailFlags.includes('C')) { + CEmailChecked = true; + } + if (OSFlags.includes('C')) { + COSChecked = true; + } + if (SSLFlags.includes('c')) { + cSSLChecked = true; + } + if (EmailFlags.includes('c')) { + cEmailChecked = true; + } + if (OSFlags.includes('c')) { + cOSChecked = true; + } + if (SSLFlags.includes('P')) { + PSSLChecked = true; + } + if (EmailFlags.includes('P')) { + PEmailChecked = true; + } + if (OSFlags.includes('P')) { + POSChecked = true; + } + if (SSLFlags.includes('p')) { + pSSLChecked = true; + } + if (EmailFlags.includes('p')) { + pEmailChecked = true; + } + if (OSFlags.includes('p')) { + pOSChecked = true; + } + if (SSLFlags.includes('u')) { + uSSLChecked = true; + } + if (EmailFlags.includes('u')) { + uEmailChecked = true; + } + if (OSFlags.includes('u')) { + uOSChecked = true; + } + } + + 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> + Edit Certificate Trust Flags + </Modal.Title> + </Modal.Header> + <Modal.Body> + <Form horizontal autoComplete="off"> + <Row className="ds-margin-top"> + <Col sm={4}> + <ControlLabel>Flags</ControlLabel> + </Col> + <Col sm={2}> + <ControlLabel>SSL</ControlLabel> + </Col> + <Col sm={2}> + <ControlLabel>Email</ControlLabel> + </Col> + <Col sm={3}> + <ControlLabel>Object Signing</ControlLabel> + </Col> + </Row> + <hr /> + <Row> + <Col sm={4} title="Trusted CA (flag 'C', also implies 'c' flag)"> + (C) - Trusted CA + </Col> + <Col sm={2}> + <Checkbox + id="CflagSSL" + checked={CSSLChecked} + onChange={handleChange} + /> + </Col> + <Col sm={2}> + <Checkbox + id="CflagEmail" + checked={CEmailChecked} + onChange={handleChange} + /> + </Col> + <Col sm={2}> + <Checkbox + id="CflagOS" + checked={COSChecked} + onChange={handleChange} + /> + </Col> + </Row> + <p /> + <Row> + <Col sm={4} title="Trusted CA for client authentication (flag 'T')"> + (T) - Trusted CA Client Auth + </Col> + <Col sm={2}> + <Checkbox + id="TflagSSL" + checked={TSSLChecked} + onChange={handleChange} + /> + </Col> + <Col sm={2}> + <Checkbox + id="TflagEmail" + checked={TEmailChecked} + onChange={handleChange} + /> + </Col> + <Col sm={2}> + <Checkbox + id="TflagOS" + checked={TOSChecked} + onChange={handleChange} + /> + </Col> + </Row> + <p /> + <Row> + <Col sm={4} title="Valid CA (flag 'c')"> + (c) - Valid CA + </Col> + <Col sm={2}> + <Checkbox + id="cflagSSL" + checked={cSSLChecked} + onChange={handleChange} + /> + </Col> + <Col sm={2}> + <Checkbox + id="cflagEmail" + checked={cEmailChecked} + onChange={handleChange} + /> + </Col> + <Col sm={2}> + <Checkbox + id="cflagOS" + checked={cOSChecked} + onChange={handleChange} + /> + </Col> + </Row> + <p /> + <Row> + <Col sm={4} title="Trusted Peer (flag 'P', implies flag 'p')"> + (P) - Trusted Peer + </Col> + <Col sm={2}> + <Checkbox + id="PflagSSL" + checked={PSSLChecked} + onChange={handleChange} + /> + </Col> + <Col sm={2}> + <Checkbox + id="PflagEmail" + checked={PEmailChecked} + onChange={handleChange} + /> + </Col> + <Col sm={2}> + <Checkbox + id="PflagOS" + checked={POSChecked} + onChange={handleChange} + /> + </Col> + </Row> + <p /> + <Row> + <Col sm={4} title="Valid Peer (flag 'p')"> + (p) - Valid Peer + </Col> + <Col sm={2}> + <Checkbox + id="pflagSSL" + checked={pSSLChecked} + onChange={handleChange} + /> + </Col> + <Col sm={2}> + <Checkbox + id="pflagEmail" + checked={pEmailChecked} + onChange={handleChange} + /> + </Col> + <Col sm={2}> + <Checkbox + id="pflagOS" + checked={pOSChecked} + onChange={handleChange} + /> + </Col> + </Row> + <hr /> + <Row> + <Col sm={4} title="A private key is associated with the certificate. This is a dynamic flag and you cannot adjust it."> + (u) - Private Key + </Col> + <Col sm={2}> + <Checkbox + id="uflagSSL" + checked={uSSLChecked} + disabled + /> + </Col> + <Col sm={2}> + <Checkbox + id="uflagEmail" + checked={uEmailChecked} + disabled + /> + </Col> + <Col sm={2}> + <Checkbox + id="uflagOS" + checked={uOSChecked} + disabled + /> + </Col> + </Row> + <p /> + {spinner} + </Form> + </Modal.Body> + <Modal.Footer> + <Button + bsStyle="default" + className="btn-cancel" + onClick={closeHandler} + > + Cancel + </Button> + <Button + bsStyle="primary" + onClick={saveHandler} + > + Save + </Button> + </Modal.Footer> + </div> + </Modal> + ); + } +} + +SecurityEnableModal.propTypes = { + showModal: PropTypes.bool, + closeHandler: PropTypes.func, + handleChange: PropTypes.func, + saveHandler: PropTypes.func, + primaryName: PropTypes.string, + certs: PropTypes.array, + spinning: PropTypes.bool, +}; + +SecurityEnableModal.defaultProps = { + showModal: false, + closeHandler: noop, + handleChange: noop, + saveHandler: noop, + primaryName: "", + certs: [], + spinning: false, +}; + +EditCertModal.propTypes = { + showModal: PropTypes.bool, + closeHandler: PropTypes.func, + handleChange: PropTypes.func, + saveHandler: PropTypes.func, + flags: PropTypes.string, + spinning: PropTypes.bool, +}; + +EditCertModal.defaultProps = { + showModal: false, + closeHandler: noop, + handleChange: noop, + saveHandler: noop, + flags: "", + spinning: false, +}; + +SecurityAddCertModal.propTypes = { + showModal: PropTypes.bool, + closeHandler: PropTypes.func, + handleChange: PropTypes.func, + saveHandler: PropTypes.func, + spinning: PropTypes.bool, + error: PropTypes.object, +}; + +SecurityAddCertModal.defaultProps = { + showModal: false, + closeHandler: noop, + handleChange: noop, + saveHandler: noop, + spinning: false, + error: {}, +}; + +SecurityAddCACertModal.propTypes = { + showModal: PropTypes.bool, + closeHandler: PropTypes.func, + handleChange: PropTypes.func, + saveHandler: PropTypes.func, + spinning: PropTypes.bool, + error: PropTypes.object, +}; + +SecurityAddCACertModal.defaultProps = { + showModal: false, + closeHandler: noop, + handleChange: noop, + saveHandler: noop, + spinning: false, + error: {}, +}; diff --git a/src/cockpit/389-console/src/lib/security/securityTables.jsx b/src/cockpit/389-console/src/lib/security/securityTables.jsx new file mode 100644 index 0000000..6b74a01 --- /dev/null +++ b/src/cockpit/389-console/src/lib/security/securityTables.jsx @@ -0,0 +1,454 @@ +import React from "react"; +import { + // Button, + DropdownButton, + MenuItem, + actionHeaderCellFormatter, + sortableHeaderCellFormatter, + tableCellFormatter, + noop +} from "patternfly-react"; +import { DSTable } from "../dsTable.jsx"; +import PropTypes from "prop-types"; +import "../../css/ds.css"; + +class CertTable extends React.Component { + constructor(props) { + super(props); + + this.state = { + rowKey: "nickname", + columns: [ + { + property: "nickname", + header: { + label: "Certificate Name", + props: { + index: 0, + rowSpan: 1, + colSpan: 1, + sort: true + }, + transforms: [], + formatters: [], + customFormatters: [sortableHeaderCellFormatter] + }, + cell: { + props: { + index: 0 + }, + formatters: [tableCellFormatter] + } + }, + { + property: "subject", + header: { + label: "Subject DN", + props: { + index: 1, + rowSpan: 1, + colSpan: 1, + sort: true + }, + transforms: [], + formatters: [], + customFormatters: [sortableHeaderCellFormatter] + }, + cell: { + props: { + index: 1 + }, + formatters: [tableCellFormatter] + } + }, + { + property: "issuer", + header: { + label: "Issued By", + props: { + index: 2, + rowSpan: 1, + colSpan: 1, + sort: true + }, + transforms: [], + formatters: [], + customFormatters: [sortableHeaderCellFormatter] + }, + cell: { + props: { + index: 2 + }, + formatters: [tableCellFormatter] + } + }, + { + property: "flags", + header: { + label: "Trust Flags", + props: { + index: 3, + rowSpan: 1, + colSpan: 1, + sort: true + }, + transforms: [], + formatters: [], + customFormatters: [sortableHeaderCellFormatter] + }, + cell: { + props: { + index: 3 + }, + formatters: [tableCellFormatter] + } + }, + { + property: "expires", + header: { + label: "Expiration Date", + props: { + index: 4, + rowSpan: 1, + colSpan: 1, + sort: true + }, + transforms: [], + formatters: [], + customFormatters: [sortableHeaderCellFormatter] + }, + cell: { + props: { + index: 4 + }, + formatters: [tableCellFormatter] + } + }, + { + property: "action", + header: { + label: "", + props: { + index: 5, + rowSpan: 1, + colSpan: 1 + }, + formatters: [actionHeaderCellFormatter] + }, + cell: { + props: { + index: 5 + }, + formatters: [ + (value, { rowData }) => { + return [ + <td key={rowData.nickname[0]}> + <DropdownButton id={rowData.nickname[0]} + bsStyle="default" title="Actions"> + <MenuItem eventKey="1" onClick={() => { + this.props.editCert(rowData); + }} + > + Edit Trust Flags + </MenuItem> + <MenuItem divider /> + <MenuItem eventKey="2" onClick={() => { + this.props.delCert(rowData); + }} + > + Delete Certificate + </MenuItem> + </DropdownButton> + </td> + ]; + } + ] + } + } + ] + }; + this.getColumns = this.getColumns.bind(this); + this.getSingleColumn = this.getSingleColumn.bind(this); + } + + getSingleColumn () { + return [ + { + property: "msg", + header: { + label: "Certificates", + props: { + index: 0, + rowSpan: 1, + colSpan: 1, + sort: true + }, + transforms: [], + formatters: [], + customFormatters: [sortableHeaderCellFormatter] + }, + cell: { + props: { + index: 0 + }, + formatters: [tableCellFormatter] + } + }, + ]; + } + + getColumns() { + return this.state.columns; + } + + render() { + let certRows = []; + let serverTable; + for (let cert of this.props.certs) { + let obj = { + 'nickname': [cert.attrs['nickname']], + 'subject': [cert.attrs['subject']], + 'issuer': [cert.attrs['issuer']], + 'expires': [cert.attrs['expires']], + 'flags': [cert.attrs['flags']], + }; + certRows.push(obj); + } + + if (certRows.length == 0) { + serverTable = <DSTable + getColumns={this.getSingleColumn} + rowKey={"msg"} + rows={[{msg: "No Certificates"}]} + key={"nocerts"} + />; + } else { + serverTable = <DSTable + getColumns={this.getColumns} + rowKey={this.state.rowKey} + rows={certRows} + key={certRows} + disableLoadingSpinner + />; + } + + return ( + <div> + {serverTable} + </div> + ); + } +} + +// Future - https://pagure.io/389-ds-base/issue/50491 +class CRLTable extends React.Component { + constructor(props) { + super(props); + + this.state = { + rowKey: "name", + columns: [ + { + property: "name", + header: { + label: "Issued By", + props: { + index: 0, + rowSpan: 1, + colSpan: 1, + sort: true + }, + transforms: [], + formatters: [], + customFormatters: [sortableHeaderCellFormatter] + }, + cell: { + props: { + index: 0 + }, + formatters: [tableCellFormatter] + } + }, + { + property: "effective", + header: { + label: "Effective Date", + props: { + index: 1, + rowSpan: 1, + colSpan: 1, + sort: true + }, + transforms: [], + formatters: [], + customFormatters: [sortableHeaderCellFormatter] + }, + cell: { + props: { + index: 1 + }, + formatters: [tableCellFormatter] + } + }, + { + property: "nextUpdate", + header: { + label: "Next Updateo", + props: { + index: 2, + rowSpan: 1, + colSpan: 1, + sort: true + }, + transforms: [], + formatters: [], + customFormatters: [sortableHeaderCellFormatter] + }, + cell: { + props: { + index: 2 + }, + formatters: [tableCellFormatter] + } + }, + + { + property: "type", + header: { + label: "Type", + props: { + index: 3, + rowSpan: 1, + colSpan: 1, + sort: true + }, + transforms: [], + formatters: [], + customFormatters: [sortableHeaderCellFormatter] + }, + cell: { + props: { + index: 3 + }, + formatters: [tableCellFormatter] + } + }, + { + property: "action", + header: { + label: "", + props: { + index: 4, + rowSpan: 1, + colSpan: 1 + }, + formatters: [actionHeaderCellFormatter] + }, + cell: { + props: { + index: 4 + }, + formatters: [ + (value, { rowData }) => { + return [ + <td key={rowData.name[0]}> + <DropdownButton id={rowData.name[0]} + bsStyle="default" title="Actions"> + <MenuItem eventKey="1" onClick={() => { + this.props.editIndex(rowData); + }} + > + View CRL + </MenuItem> + <MenuItem eventKey="2" onClick={() => { + this.props.reindexIndex(rowData); + }} + > + Delete CRL + </MenuItem> + </DropdownButton> + </td> + ]; + } + ] + } + } + ] + }; + this.getColumns = this.getColumns.bind(this); + } + + getSingleColumn () { + return [ + { + property: "msg", + header: { + label: "Certificate Revocation Lists", + props: { + index: 0, + rowSpan: 1, + colSpan: 1, + sort: true + }, + transforms: [], + formatters: [], + customFormatters: [sortableHeaderCellFormatter] + }, + cell: { + props: { + index: 0 + }, + formatters: [tableCellFormatter] + } + }, + ]; + } + + getColumns() { + return this.state.columns; + } + + render() { + let crlTable; + if (this.props.rows.length == 0) { + crlTable = <DSTable + getColumns={this.getSingleColumn} + rowKey={"msg"} + rows={[{msg: "None"}]} + />; + } else { + crlTable = <DSTable + getColumns={this.getColumns} + rowKey={this.state.rowKey} + rows={this.props.rows} + disableLoadingSpinner + />; + } + return ( + <div> + {crlTable} + </div> + ); + } +} + +// Props and defaults + +CertTable.propTypes = { + // serverId: PropTypes.string, + certs: PropTypes.array, + editCert: PropTypes.func, + delCert: PropTypes.func, +}; + +CertTable.defaultProps = { + // serverId: "", + certs: [], + editCert: noop, + delCert: noop, +}; + +export { + CertTable, + CRLTable +}; diff --git a/src/cockpit/389-console/src/lib/tools.jsx b/src/cockpit/389-console/src/lib/tools.jsx index 5d482ba..b3e7573 100644 --- a/src/cockpit/389-console/src/lib/tools.jsx +++ b/src/cockpit/389-console/src/lib/tools.jsx @@ -1,8 +1,8 @@ export function searchFilter(searchFilterValue, columnsToSearch, rows) { if (searchFilterValue && rows && rows.length) { - const filteredRows = []; + let filteredRows = []; rows.forEach(row => { - var rowToSearch = []; + let rowToSearch = []; if (columnsToSearch && columnsToSearch.length) { columnsToSearch.forEach(column => rowToSearch.push(row[column]) @@ -27,18 +27,18 @@ export function searchFilter(searchFilterValue, columnsToSearch, rows) {
export function log_cmd(js_func, desc, cmd_array) { if (console) { - var pw_args = ["--passwd", "--bind-pw"]; - var cmd_list = []; - var converted_pw = false; + let pw_args = ["--passwd", "--bind-pw"]; + let cmd_list = []; + let converted_pw = false;
- for (var idx in cmd_array) { - var cmd = cmd_array[idx]; + for (let idx in cmd_array) { + let cmd = cmd_array[idx]; converted_pw = false; for (var arg_idx in pw_args) { if (cmd.startsWith(pw_args[arg_idx])) { // We are setting a password, if it has a value we need to hide it - var arg_len = cmd.indexOf("="); - var arg = cmd.substring(0, arg_len); + let arg_len = cmd.indexOf("="); + let arg = cmd.substring(0, arg_len); if (cmd.length != arg_len + 1) { // We are setting a password value... cmd_list.push(arg + "=**********"); diff --git a/src/cockpit/389-console/src/security.html b/src/cockpit/389-console/src/security.html deleted file mode 100644 index 1444418..0000000 --- a/src/cockpit/389-console/src/security.html +++ /dev/null @@ -1,502 +0,0 @@ - - <div id="sec-config" class="security-ctrl all-pages" hidden> - <h3 class="ds-config-header">Security Configuration</h3> - - <input type="checkbox" class="ds-config-checkbox" id="nsslapd-security"><label - for="nsslapd-security" class="ds-label" title="Enable security in the server (nsslapd-security)."> Enable Security</label> - <hr class=""> - <div class="ds-expired-div" id="cert-attrs"> - - <div class="ds-container"> - <div class="ds-inline"> - <div> - <label for="nsslapd-secureport" class="ds-config-label" title="The server's secure port number (nsslapd-secureport).">Server Secure Port</label><input - class="ds-input" type="text" id="nsslapd-secureport" size="20"/> - </div> - <div> - <label for="nsslapd-securelistenhost" class="ds-config-label" title="This parameter can be used to restrict the Directory Server instance to a single IP interface (hostname, or IP address). This parameter specifically sets what interface to use for TLS traffic. Requires restart. (nsslapd-securelistenhost)."> - Secure Listen Host Address</label><input class="ds-input" type="text" id="nsslapd-securelistenhost" size="20"/> - </div> - <div> - <label for="sec-sslmin" class="ds-config-label" title="The minimum SSL/TLS version the server will accept (sslversionmin).">Minimum SSL/TLS Version </label><select - class="btn btn-default dropdown ds-sec-dropdown" id="sec-sslmin"> - <option>TLS1.3</option> - <option>TLS1.2</option> - <option>TLS1.1</option> - <option>TLS1.0</option> - <option>SSL3</option> - </select> - </div> - <div> - <label for="sec-sslmax" class="ds-config-label" title="The maximum SSL/TLS version the server will accept (sslversionmax)."> Maximum SSL/TLS Version</label><select - class="btn btn-default dropdown ds-sec-dropdown" id="sec-sslmax"> - <option>TLS1.3</option> - <option>TLS1.2</option> - <option>TLS1.1</option> - <option>TLS1.0</option> - <option>SSL3</option> - </select> - </div> - <div> - <label for="sec-clientauth" class="ds-config-label" title="shows how the Directory Server enforces client authentication (nsSSLClientAuth)."> Client Authentication</label><select - class="btn btn-default dropdown ds-sec-dropdown" id="sec-clientauth"> - <option>Off</option> - <option>Allowed</option> - <option>Required</option> - </select> - </div> - <div> - <label for="sec-validate" class="ds-config-label" title="Validate server's certificate expiration date (nsslapd-validate-cert)."> Validate Certificate Expiration</label><select - class="btn btn-default dropdown ds-sec-dropdown" id="sec-validate"> - <option>Warn</option> - <option>On</option> - <option>Off</option> - </select> - </div> - </div> - <div class="ds-divider"></div> - <div class="ds-divider"></div> - <div class="ds-divider"></div> - <div class="ds-line"> - <div> - <input type="checkbox" class="ds-config-checkbox" id="nsslapd-require-secure-binds"><label - for="nsslapd-require-secure-binds" class="ds-label" title="Require all connections use TLS (nsslapd-require-secure-binds)."> Require Secure Connections</label> - </div> - <div> - <input type="checkbox" class="ds-config-checkbox" id="nsslapd-ssl-check-hostname"><label - for="nsslapd-ssl-check-hostname" class="ds-label" title="Verify authenticity of a request by matching the host name against the value assigned to the common name (cn) attribute of the subject name (subjectDN field) in the certificate being presented. (nsslapd-ssl-check-hostname)."> Verify Certificate Subject Hostname</label> - </div> - <div> - <input type="checkbox" class="ds-config-checkbox" id="allowWeakCipher"><label - for="allowWeakCipher" class="ds-label" title="Allow weak ciphers (allowWeakCipher)."> Allow Weak Ciphers</label> - </div> - <div class="ds-first"> - <button class="btn btn-default" id="set-sec-passwd-btn" title="Change the Security Database password">Set Security Password</button> - </div> - </div> - </div> - <hr> - - <h4>Allowed Ciphers</h4> - <div class="ds-indent"> - <input type="checkbox" class="ds-config-checkbox" id="cipher-default-state"><label - for="cipher-default-state" class="ds-label" title="Use the preferred default ciphers, as opposed to allowing all the ciphers">Use Default Ciphers</label> - </div> - <div id="cipher-table"> - <table class="table table-striped table-bordered table-hover ds-loglevel-table" id="allowed-cipher-table"> - <thead> - <tr> - <th class="ds-table-btn">State</th> - <th>Cipher</th> - </tr> - </thead> - <tbody> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown" id="cipher-all-state"> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td id="cipher-all">All</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown"> - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_DHE_DSS_WITH_AES_256_GCM_SHA384</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown"> - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_DHE_DSS_WITH_AES_128_GCM_SHA256</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_DHE_DSS_WITH_AES_256_GCM_SHA384</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_DHE_RSA_WITH_AES_128_GCM_SHA256</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_RSA_WITH_AES_256_GCM_SHA384</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_RSA_WITH_AES_128_GCM_SHA256</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_RSA_WITH_SEED_CBC_SHA</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_RSA_WITH_CAMELLIA_256_CBC_SHA</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_DHE_RSA_WITH_AES_256_CBC_SHA256</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_DHE_DSS_WITH_AES_256_CBC_SHA256</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_DHE_RSA_WITH_AES_128_CBC_SHA256</td> - </tr> - <tr class="cipher-row"> - <td class="ds-table-btn"> <select class="btn btn-default dropdown ds-cipher-dropdown" > - <option></option> - <option>Enabled</option> - <option>Disabled</option> - </select> - </td> - <td>TLS_DHE_DSS_WITH_RC4_128_SHA</td> - </tr> - </tbody> - </table> - </div> - </div> - <div class="ds-footer"> - <button class="btn btn-primary save-button">Save</button> - </div> - </div> - - - <div class="security-ctrl all-pages" id="sec-ciphers-page" hidden> - <h3 class="ds-config-header">Supported Ciphers</h3> - <table id="nssslsupportedciphers" class="display ds-table" cellspacing="0" width="100%"> - <thead> - <tr class="ds-table-header"> - <th>Cipher Name</th> - <th>Symmetric Cipher Name</th> - <th>Mac Algorithm Name</th> - <th>Symmetric Key Bits</th> - </tr> - </thead> - <tbody id="cipher-body"> - <tr> - <td>TLS_RSA_WITH_AES_256_CBC_SHA256</td> - <td>AES</td> - <td>SHA256</td> - <td>256</td> - </tr> - <tr> - <td>TLS_DHE_DSS_WITH_DES_CBC_SHA</td> - <td>AES</td> - <td>SHA256</td> - <td>256</td> - </tr> - </tbody> - </table> - </div> - - <div id="sec-cacert-page" class="all-pages" hidden> - <h3 class="ds-config-header">CA Certificates</h3> - <table id="ca-cert-table" class="display ds-repl-table" cellspacing="0" width="100%"> - <thead> - <tr class="ds-table-header"> - <th>CA Certificate Name</th> - <th>Trust Attributes</th> - <th>Expiration Date</th> - <th>Actions</th> - </tr> - </thead> - <tbody id="cipher-body"> - <tr> - <td>CA Certificate</td> - <td>CTu,u,u</td> - <td>2020/12/31</td> - <td> - <div class="dropdown"> - <button class="btn btn-default dropdown-toggle ds-agmt-dropdown-button" type="button" id="menu1" data-toggle="dropdown">Choose Action... - <span class="caret"></span></button> - <ul id="cert-dropdown" class="dropdown-menu ds-agmt-dropdown" role="menu" aria-labelledby="menu1"> - <li role="certificate"><a role="menuitem" class="ca-cert-dropdown" href="#">View Certificate</a></li> - <li role="certificate"><a role="menuitem" class="ca-cert-dropdown" href="#">Edit Certificate</a></li> - <li role="certificate"><a role="menuitem" class="ca-cert-dropdown" href="#">Verify Certificate</a></li> - <li role="certificate"><a role="menuitem" class="ca-cert-dropdown" href="#">Export Certificate</a></li> - <li role="certificate"><a role="menuitem" class="ca-cert-dropdown" href="#">Delete Certificate</a></li> - </ul> - </div> - </td> - </tr> - </tbody> - </table> - <button class="btn btn-primary" id="import-ca-cert" data-toggle="modal" data-target="#import-cacert-form">Import CA Certificate</button> - </div> - - <div id="sec-svrcert-page" class="all-pages" hidden> - <h3 class="ds-config-header">Server Certificates</h3> - <table id="server-cert-table" class="display ds-repl-table" cellspacing="0" width="100%"> - <thead> - <tr class="ds-table-header"> - <th>Server Certificate Name</th> - <th>Trust Attributes</th> - <th>Issued To</th> - <th>Issued By</th> - <th>Expiration Date</th> - <th>Actions</th> - </tr> - </thead> - <tbody id="cipher-body"> - <tr> - <td>Server-Cert</td> - <td>u,u,Pu</td> - <td>localhost.localdomain</td> - <td>Mark's CA Cert</td> - <td>2020/11/22</td> - <td> - <div class="dropdown"> - <button class="btn btn-default dropdown-toggle" type="button" id="menu1" data-toggle="dropdown">Choose Action... - <span class="caret"></span></button> - <ul id="cert-dropdown" class="dropdown-menu" role="menu" aria-labelledby="menu1"> - <li role="certificate"><a role="menuitem" class="server-cert-dropdown" href="#">View Certificate</a></li> - <li role="certificate"><a role="menuitem" class="server-cert-dropdown" href="#">Verify Certificate</a></li> - <li role="certificate"><a role="menuitem" class="server-cert-dropdown" href="#">Renew Certificate</a></li> - <li role="certificate"><a role="menuitem" class="server-cert-dropdown" href="#">Export Certificate</a></li> - <li role="certificate"><a role="menuitem" class="server-cert-dropdown" href="#">Delete Certificate</a></li> - </ul> - </div> - </td> - </tr> - </tbody> - </table> - <button class="btn btn-default ds-spacing-sm" id="import-server-cert" data-toggle="modal" data-target="#import-cert-form">Import Certificate</button> - <button class="btn btn-default" id="import-server-cert">Request Certificate</button> - </div> - - <div id="sec-revoked-page" class="all-pages" hidden> - <h3 class="ds-config-header">Revoked Certificates</h3> - <table id="revoked-cert-table" class="display ds-repl-table" cellspacing="0" width="100%"> - <thead> - <tr class="ds-table-header"> - <th>Issued By</th> - <th>Effective Date</th> - <th>Next Update</th> - <th>Type</th> - <th>Actions</th> - </tr> - </thead> - <tbody id="cipher-body"> - <tr> - <td>Mark's CA Cert2</td> - <td>2018/11/22</td> - <td>2019/11/22</td> - <td>CRL</td> - <td> - <div class="dropdown"> - <button class="btn btn-default dropdown-toggle ds-agmt-dropdown-button" type="button" id="menu1" data-toggle="dropdown">Choose Action... - <span class="caret"></span></button> - <ul id="cert-dropdown" class="dropdown-menu ds-agmt-dropdown" role="menu" aria-labelledby="menu1"> - <li role="certificate"><a role="menuitem" class="revoked-cert-dropdown" href="#">View</a></li> - <li role="certificate"><a role="menuitem" class="revoked-cert-dropdown" href="#">Delete</a></li> - </ul> - </div> - </td> - </tr> - </tbody> - </table> - <button class="btn btn-default ds-spacing-sm" id="add-revoked-btn" data-toggle="modal" data-target="#revoked-form">Add CRL/CKL</button> - </div> - - <!-- Modals/Popups/Wizards --> - - - - <div class="modal fade" id="import-cert-form" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="import-cert-label" aria-hidden="true"> - <div class="modal-dialog ds-modal-wide"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true" aria-label="Close"> - <span class="pficon pficon-close"></span> - </button> - <h4 class="modal-title" id="import-cert-label">Import Server Certificate</h4> - </div> - <div class="modal-body"> - <form class="form-horizontal"> - <div class="ds-inline"> - <label for="import-cert-file" class="" title="The name of the database link.">Certificate File</label><input - class="ds-input ds-left-margin" type="text" id="import-cert-file" name="name" size="40"> - </div> - </form> - </div> - <div class="modal-footer ds-modal-footer"> - <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> - <button type="button" class="btn btn-primary" id="import-cert-btn" data-dismiss="modal">Import Certificate</button> - </div> - </div> - </div> - </div> - - <div class="modal fade" data-backdrop="static" id="import-cacert-form" tabindex="-1" role="dialog" aria-labelledby="import-cacert-label" aria-hidden="true"> - <div class="modal-dialog ds-modal-wide"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true" aria-label="Close"> - <span class="pficon pficon-close"></span> - </button> - <h4 class="modal-title" id="import-cacert-label">Import CA Certificate</h4> - </div> - <div class="modal-body"> - <form class="form-horizontal"> - <div class="ds-inline"> - <label for="import-cert-file" class="" title="The name of the database link.">Certificate File</label><input - class="ds-input ds-left-margin" type="text" id="import-cacert-file" name="name" size="40"> - </div> - </form> - </div> - <div class="modal-footer ds-modal-footer"> - <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> - <button type="button" class="btn btn-primary" id="import-cacert-btn" data-dismiss="modal">Import Certificate</button> - </div> - </div> - </div> - </div> - - - <div class="modal fade" id="revoked-form" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="revoked-label" aria-hidden="true"> - <div class="modal-dialog ds-modal-wide"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true" aria-label="Close"> - <span class="pficon pficon-close"></span> - </button> - <h4 class="modal-title" id="revoked-label">Add Certificate Revocation List/Compromised Key List</h4> - </div> - <div class="modal-body"> - <form class="form-horizontal"> - <div class="ds-inline"> - <label for="revoked-file">CRL/CKL PEM File</label><input - class="ds-input ds-left-margin" type="text" id="revoked-file" name="name" size="40"> - </div> - </form> - </div> - <div class="modal-footer ds-modal-footer"> - <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> - <button type="button" class="btn btn-primary" id="add-crl-btn" data-dismiss="modal">Add</button> - </div> - </div> - </div> - </div> - - - - - <div id="export-cert" class="modal"> - <form class="modal-content animate"> - <div class="container"> - <h3 id=""><b>Export Certificate</b> <span class="close" id="export-cert-close">×</span></h3> - - <div class="clearfix ds-container"> - <div class="ds-panel-left"> - <button type="button" id="export-cert-cancel" class="ds-button-left">Cancel</button> - </div> - <div class="ds-panel-right"> - <button type="submit" id="export-cert-save" class="ds-button-right">Export Certificate</button> - </div> - </div> - </div> - </form> - </div> - - diff --git a/src/cockpit/389-console/src/security.js b/src/cockpit/389-console/src/security.js deleted file mode 100644 index fd76dbb..0000000 --- a/src/cockpit/389-console/src/security.js +++ /dev/null @@ -1,137 +0,0 @@ - - -// TODO clear form functions - - -$(document).ready( function() { - $("#security-content").load("security.html", function () { - // default setting - $('#cert-attrs *').attr('disabled', true); - - $(".dropdown").on("change", function() { - // Refreshes dropdown on Chrome - $(this).blur(); - }); - - $("#sec-config-btn").on("click", function() { - $(".all-pages").hide(); - $("#security-content").show(); - $("#sec-config").show(); - }); - - $("#sec-cacert-btn").on("click", function() { - $(".all-pages").hide(); - $("#security-content").show(); - $("#sec-cacert-page").show(); - }); - - $("#sec-srvcert-btn").on("click", function() { - $(".all-pages").hide(); - $("#security-content").show(); - $("#sec-svrcert-page").show(); - }); - $("#sec-revoked-btn").on("click", function() { - $(".all-pages").hide(); - $("#security-content").show(); - $("#sec-revoked-page").show(); - }); - $("#sec-ciphers-btn").on("click", function() { - $(".all-pages").hide(); - $("#security-content").show(); - $("#sec-ciphers-page").show(); - }); - - $("#sec-config").show(); - - // Clear forms as theyare clicked - - $("#add-revoked-btn").on('click', function () { - // TODO Clear form - - }); - - $("#add-crl-btn").on('click', function () { - // Add CRL/CKL - - // Close form - $("#revoked-form").modal("toggle"); - }); - - $('#nssslsupportedciphers').DataTable( { - "paging": true, - "bAutoWidth": false, - "dom": '<"pull-left"f><"pull-right"l>tip', - "lengthMenu": [ 10, 25, 50, 100], - "language": { - "emptyTable": "No ciphers!" - } - }); - - $("#nsslapd-security").change(function() { - if(this.checked) { - $('#cert-attrs *').attr('disabled', false); - } else { - $('#cert-attrs *').attr('disabled', true); - } - }); - - $("#cipher-default-state").change(function() { - if(this.checked) { - $("#cipher-table").hide(); - } else { - $("#cipher-table").show(); - } - }); - - // Set up ca cert table - $('#ca-cert-table').DataTable( { - "paging": false, - "bAutoWidth": false, - "searching": false, - "dom": '<"pull-left"f><"pull-right"l>tip', - "lengthMenu": [ 10, 25, 50, 100], - "language": { - "emptyTable": "No Certificates In Database" - }, - "columnDefs": [ { - "targets": 3, - "orderable": false - } ] - }); - - // Set up server cert table - $('#server-cert-table').DataTable( { - "paging": false, - "bAutoWidth": false, - "searching": false, - "dom": '<"pull-left"f><"pull-right"l>tip', - "lengthMenu": [ 10, 25, 50, 100], - "language": { - "emptyTable": "No Certificates In Database" - }, - "columnDefs": [ { - "targets": 5, - "orderable": false - } ] - }); - - // Set up revoked cert table - $('#revoked-cert-table').DataTable( { - "paging": false, - "bAutoWidth": false, - "searching": false, - "dom": '<"pull-left"f><"pull-right"l>tip', - "lengthMenu": [ 10, 25, 50, 100], - "language": { - "emptyTable": "No Certificates In Database" - }, - "columnDefs": [ { - "targets": 4, - "orderable": false - } ] - }); - // Page is loaded, mark it as so... - security_page_loaded = 1; - }); -}); - diff --git a/src/cockpit/389-console/src/security.jsx b/src/cockpit/389-console/src/security.jsx new file mode 100644 index 0000000..43edf49 --- /dev/null +++ b/src/cockpit/389-console/src/security.jsx @@ -0,0 +1,853 @@ +import cockpit from "cockpit"; +import React from "react"; +import Switch from "react-switch"; +import { NotificationController, ConfirmPopup } from "./lib/notifications.jsx"; +import { log_cmd } from "./lib/tools.jsx"; +import { Typeahead } from "react-bootstrap-typeahead"; +import { CertificateManagement } from "./lib/security/certificateManagement.jsx"; +import { SecurityEnableModal } from "./lib/security/securityModals.jsx"; +import { Ciphers } from "./lib/security/ciphers.jsx"; +import { + Nav, + NavItem, + TabContainer, + TabContent, + TabPane, + Col, + Row, + ControlLabel, + Button, + Checkbox, + Spinner +} from "patternfly-react"; +import PropTypes from "prop-types"; +import "./css/ds.css"; + +export class Security extends React.Component { + constructor (props) { + super(props); + this.state = { + loaded: false, + saving: false, + notifications: [], + activeKey: 1, + + errObj: {}, + showConfirmDisable: false, + showSecurityEnableModal: false, + primaryCertName: '', + serverCertNames: [], + serverCerts: [], + // Ciphers + supportedCiphers: [], + enabledCiphers: [], + // Config settings + securityEnabled: false, + requireSecureBinds: false, + secureListenhost: false, + securePort: '636', + clientAuth: false, + checkHostname: false, + validateCert: '', + sslVersionMin: '', + sslVersionMax: '', + allowWeakCipher: false, + nssslpersonalityssl: '', + // Original config Settings + _securityEnabled: false, + _requireSecureBinds: false, + _secureListenhost: false, + _securePort: '636', + _clientAuth: false, + _checkHostname: false, + _validateCert: '', + _sslVersionMin: '', + _sslVersionMax: '', + _allowWeakCipher: false, + _nssslpersonalityssl: '', + }; + + this.handleChange = this.handleChange.bind(this); + this.addNotification = this.addNotification.bind(this); + this.removeNotification = this.removeNotification.bind(this); + this.handleNavSelect = this.handleNavSelect.bind(this); + this.handleSwitchChange = this.handleSwitchChange.bind(this); + this.handleTypeaheadChange = this.handleTypeaheadChange.bind(this); + this.loadSecurityConfig = this.loadSecurityConfig.bind(this); + this.loadEnabledCiphers = this.loadEnabledCiphers.bind(this); + this.loadSupportedCiphers = this.loadSupportedCiphers.bind(this); + this.loadCerts = this.loadCerts.bind(this); + this.loadCACerts = this.loadCACerts.bind(this); + this.closeConfirmDisable = this.closeConfirmDisable.bind(this); + this.enableSecurity = this.enableSecurity.bind(this); + this.disableSecurity = this.disableSecurity.bind(this); + this.saveSecurityConfig = this.saveSecurityConfig.bind(this); + this.closeSecurityEnableModal = this.closeSecurityEnableModal.bind(this); + } + + addNotification(type, message, timerdelay, persistent) { + this.setState(prevState => ({ + notifications: [ + ...prevState.notifications, + { + key: prevState.notifications.length + 1, + type: type, + persistent: persistent, + timerdelay: timerdelay, + message: message, + } + ] + })); + } + + removeNotification(notificationToRemove) { + this.setState({ + notifications: this.state.notifications.filter( + notification => notificationToRemove.key !== notification.key + ) + }); + } + + componentWillMount () { + if (!this.state.loaded) { + this.setState({securityEnabled: true}, this.setState({securityEnabled: false})); + this.loadSecurityConfig(); + } + } + + loadSupportedCiphers () { + const cmd = [ + "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "security", "ciphers", "list", "--supported" + ]; + log_cmd("loadSupportedCiphers", "Load the security configuration", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(content => { + const config = JSON.parse(content); + this.setState({ + supportedCiphers: config.items + }, this.loadEnabledCiphers); + }) + .fail(err => { + let errMsg = JSON.parse(err); + let msg = errMsg.desc; + if ('info' in errMsg) { + msg = errMsg.desc + " - " + errMsg.info; + } + this.addNotification( + "error", + `Error loading security configuration - ${msg}` + ); + }); + } + + loadEnabledCiphers () { + const cmd = [ + "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "security", "ciphers", "list", "--enabled" + ]; + log_cmd("loadEnabledCiphers", "Load the security configuration", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(content => { + const config = JSON.parse(content); + this.setState({ + enabledCiphers: config.items, + }, this.loadCerts); + }) + .fail(err => { + let errMsg = JSON.parse(err); + let msg = errMsg.desc; + if ('info' in errMsg) { + msg = errMsg.desc + " - " + errMsg.info; + } + this.addNotification( + "error", + `Error loading security configuration - ${msg}` + ); + }); + } + + loadCACerts () { + const cmd = [ + "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "security", "ca-certificate", "list", + ]; + log_cmd("loadCACerts", "Load certificates", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(content => { + let certs = JSON.parse(content); + this.setState(() => ( + { + CACerts: certs, + loaded: true + }) + ); + }) + .fail(err => { + let errMsg = JSON.parse(err); + let msg = errMsg.desc; + if ('info' in errMsg) { + msg = errMsg.desc + " - " + errMsg.info; + } + this.addNotification( + "error", + `Error loading CA certificates - ${msg}` + ); + }); + } + + loadCerts () { + // Set loaded: true + const cmd = [ + "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "security", "certificate", "list", + ]; + log_cmd("loadCerts", "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, + serverCertNames: certNames, + }), this.loadCACerts + ); + }) + .fail(err => { + let errMsg = JSON.parse(err); + let msg = errMsg.desc; + if ('info' in errMsg) { + msg = errMsg.desc + " - " + errMsg.info; + } + this.addNotification( + "error", + `Error loading server certificates - ${msg}` + ); + }); + } + + loadRSAConfig() { + const cmd = [ + "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "security", "rsa", "get" + ]; + log_cmd("loadRSAConfig", "Load the RSA configuration", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(content => { + const config = JSON.parse(content); + const nickname = config.items['nssslpersonalityssl']; + this.setState(() => ( + { + nssslpersonalityssl: nickname, + _nssslpersonalityssl: nickname, + } + ), this.loadSupportedCiphers); + }) + .fail(err => { + let errMsg = JSON.parse(err); + let msg = errMsg.desc; + if ('info' in errMsg) { + msg = errMsg.desc + " - " + errMsg.info; + } + this.addNotification( + "error", + `Error loading security RSA configuration - ${msg}` + ); + }); + } + + loadSecurityConfig(saving) { + const cmd = [ + "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "security", "get" + ]; + log_cmd("loadSecurityConfig", "Load the security configuration", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(content => { + const config = JSON.parse(content); + const attrs = config.items; + let secEnabled = false; + let secReqSecBinds = false; + let clientAuth = "allowed"; + let validateCert = "warn"; + let cipherPref = "default"; + let allowWeak = false; + + if ('nsslapd-security' in attrs) { + if (attrs['nsslapd-security'].toLowerCase() == "on") { + secEnabled = true; + } + } + if ('nsslapd-require-secure-binds' in attrs) { + if (attrs['nsslapd-require-secure-binds'].toLowerCase() == "on") { + secReqSecBinds = true; + } + } + if ('nssslclientauth' in attrs) { + if (attrs['nssslclientauth'] != "") { + clientAuth = attrs['nssslclientauth']; + } + } + if ('nsslapd-validate-cert' in attrs) { + if (attrs['nsslapd-validate-cert'] != "") { + validateCert = attrs['nsslapd-validate-cert'].toLowerCase(); + } + } + if ('allowweakcipher' in attrs) { + if (attrs['allowweakcipher'].toLowerCase() == "on") { + allowWeak = true; + } + } + if ('nsssl3ciphers' in attrs) { + if (attrs['nsssl3ciphers'] != "") { + cipherPref = attrs['nsssl3ciphers']; + } + } + + this.setState(() => ( + { + securityEnabled: secEnabled, + requireSecureBinds: secReqSecBinds, + secureListenhost: attrs['nsslapd-securelistenhost'], + securePort: attrs['nsslapd-secureport'], + clientAuth: clientAuth, + checkHostname: attrs['nsslapd-ssl-check-hostname'], + validateCert: validateCert, + sslVersionMin: attrs['sslversionmin'], + sslVersionMax: attrs['sslversionmax'], + allowWeakCipher: allowWeak, + cipherPref: cipherPref, + _securityEnabled: secEnabled, + _requireSecureBinds: secReqSecBinds, + _secureListenhost: attrs['nsslapd-securelistenhost'], + _securePort: attrs['nsslapd-secureport'], + _clientAuth: clientAuth, + _checkHostname: attrs['nsslapd-ssl-check-hostname'], + _validateCert: validateCert, + _sslVersionMin: attrs['sslversionmin'], + _sslVersionMax: attrs['sslversionmax'], + _allowWeakCipher: allowWeak, + } + ), function() { + if (!saving) { + this.loadRSAConfig(); + } + }); + }) + .fail(err => { + let errMsg = JSON.parse(err); + let msg = errMsg.desc; + if ('info' in errMsg) { + msg = errMsg.desc + " - " + errMsg.info; + } + this.addNotification( + "error", + `Error loading security configuration - ${msg}` + ); + }); + } + + handleNavSelect(key) { + this.setState({ + activeKey: key + }); + } + + handleSwitchChange(value) { + if (!value) { + // We are disabling security, ask for confirmation + this.setState({showConfirmDisable: true}); + } else { + // Check if we have certs, if we do make the user choose one from dropdown list, otherwise reject the + // enablement + if (this.state.serverCerts.length > 0) { + this.setState({ + primaryCertName: this.state.nssslpersonalityssl, + showSecurityEnableModal: true, + }); + } else { + this.addNotification( + "error", + `There must be at least one server certificate present in the security database to enable security` + ); + } + } + } + + closeSecurityEnableModal () { + this.setState({ + showSecurityEnableModal: false, + }); + } + + handleSecEnableChange (e) { + const value = e.target.value.trim(); + this.setState({ + primaryCertName: value, + }); + } + + closeConfirmDisable () { + this.setState({ + showConfirmDisable: false + }); + } + + enableSecurity () { + /* start the spinner */ + this.setState({ + secEnableSpinner: true + }); + + const cmd = [ + "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "security", "enable", + ]; + log_cmd("enableSecurity", "Enable security", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(() => { + this.addNotification( + "success", + `Successfully enabled security. You must restart the server for this to take effect.` + ); + this.setState({ + securityEnabled: true, + secEnableSpinner: false, + showSecurityEnableModal: false, + }); + }) + .fail(err => { + let errMsg = JSON.parse(err); + let msg = errMsg.desc; + if ('info' in errMsg) { + msg = errMsg.desc + " - " + errMsg.info; + } + this.addNotification( + "error", + `Error enabling security - ${msg}` + ); + this.setState({ + secEnableSpinner: false, + showSecurityEnableModal: false, + }); + }); + } + + disableSecurity () { + const cmd = [ + "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "security", "disable", + ]; + log_cmd("disableSecurity", "Disable security", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(() => { + this.addNotification( + "success", + `Successfully disabled security. You must restart the server for this to take effect.` + ); + this.setState({ + securityEnabled: false, + }); + }) + .fail(err => { + let errMsg = JSON.parse(err); + let msg = errMsg.desc; + if ('info' in errMsg) { + msg = errMsg.desc + " - " + errMsg.info; + } + this.addNotification( + "error", + `Error disabling security - ${msg}` + ); + }); + } + + saveSecurityConfig () { + let cmd = [ + 'dsconf', '-j', 'ldapi://%2fvar%2frun%2fslapd-' + this.props.serverId + '.socket', + 'security', 'set' + ]; + + if (this.state._validateCert != this.state.validateCert) { + cmd.push("--verify-cert-chain-on-startup=" + this.state.validateCert); + } + if (this.state._sslVersionMin != this.state.sslVersionMin) { + cmd.push("--tls-protocol-min=" + this.state.sslVersionMin); + } + if (this.state._sslVersionMax != this.state.sslVersionMax) { + cmd.push("--tls-protocol-max=" + this.state.sslVersionMax); + } + if (this.state._clientAuth != this.state.clientAuth) { + cmd.push("--tls-client-auth=" + this.state.clientAuth); + } + if (this.state._securePort != this.state.securePort) { + cmd.push("--secure-port=" + this.state.securePort); + } + if (this.state._secureListenhost != this.state.secureListenhost) { + cmd.push("--listen-host=" + this.state.secureListenhost); + } + if (this.state._allowWeakCipher != this.state.allowWeakCipher) { + let val = "off"; + if (this.state.allowWeakCipher) { + val = "on"; + } + cmd.push("--allow-insecure-ciphers=" + val); + } + if (this.state._checkHostname != this.state.checkHostname) { + let val = "off"; + if (this.state.checkHostname) { + val = "on"; + } + cmd.push("--check-hostname=" + val); + } + if (this.state._requireSecureBinds != this.state.requireSecureBinds) { + let val = "off"; + if (this.state.requireSecureBinds) { + val = "on"; + } + cmd.push("--require-secure-authentication=" + val); + } + + if (cmd.length > 5) { + log_cmd("saveSecurityConfig", "Applying security config change", cmd); + let msg = "Successfully updated security configuration. You must restart the server for these changes to take effect."; + + this.setState({ + // Start the spinner + saving: true + }); + + cockpit + .spawn(cmd, {superuser: true, "err": "message"}) + .done(content => { + this.loadSecurityConfig(1); + this.addNotification( + "success", + msg + ); + this.setState({ + saving: false + }); + }) + .fail(err => { + let errMsg = JSON.parse(err); + this.loadSecurityConfig(); + this.setState({ + saving: false + }); + let msg = errMsg.desc; + if ('info' in errMsg) { + msg = errMsg.desc + " - " + errMsg.info; + } + this.addNotification( + "error", + `Error updating security configuration - ${msg}` + ); + }); + } + } + + handleTypeaheadChange(value) { + if (value.length == 0) { + return; + } + this.setState({ + nssslpersonalityssl: value[0], + }); + } + + handleChange(e) { + const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; + this.setState({ + [e.target.id]: value, + }); + } + + handleLoginModal(e) { + const value = e.target.value.trim(); + 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 + }); + } + + render() { + let securityPage = ""; + let serverCert = [this.state.nssslpersonalityssl]; + + if (this.state.loaded && !this.state.saving) { + let configPage = ""; + if (this.state.securityEnabled) { + configPage = + <div> + <Row className="ds-margin-top" title="The server's secure port number (nsslapd-secureport)."> + <Col componentClass={ControlLabel} sm={2}> + Server Secure Port + </Col> + <Col sm={4}> + <input id="securePort" className="ds-input-auto" onChange={this.handleChange} type="text" defaultValue={this.state.securePort} /> + </Col> + </Row> + <Row className="ds-margin-top" title="This parameter can be used to restrict the Directory Server instance to a single IP interface (hostname, or IP address). This parameter specifically sets what interface to use for TLS traffic. Requires restart. (nsslapd-securelistenhost)."> + <Col componentClass={ControlLabel} sm={2}> + Secure Listen Host + </Col> + <Col sm={4}> + <input id="secureListenhost" className="ds-input-auto" type="text" onChange={this.handleChange} defaultValue={this.state.secureListenhost} /> + </Col> + </Row> + <Row className="ds-margin-top" title="The name, or nickname, of the server certificate inthe NSS datgabase the server should use (nsSSLPersonalitySSL)."> + <Col className="ds-no-padding" sm={2}> + <ControlLabel>Server Certificate Name</ControlLabel> + </Col> + <Col sm={4}> + <Typeahead + id="serverCertNameTypeahead" + onChange={this.handleTypeaheadChange} + selected={serverCert} + emptyLabel="No matching certificates found" + options={this.state.serverCertNames} + newSelectionPrefix="Select a server certificate" + placeholder="Type a sever certificate nickname..." + /> + </Col> + </Row> + <Row className="ds-margin-top" title="The minimum SSL/TLS version the server will accept (sslversionmin)."> + <Col componentClass={ControlLabel} sm={2}> + Minimum TLS Version + </Col> + <Col sm={4}> + <select id="sslVersionMin" className="btn btn-default dropdown ds-select" onChange={this.handleChange} defaultValue={this.state.sslVersionMin}> + <option /> + <option>TLS1.3</option> + <option>TLS1.2</option> + <option>TLS1.1</option> + <option>TLS1.0</option> + <option>SSL3</option> + </select> + </Col> + </Row> + <Row className="ds-margin-top" title="The maximum SSL/TLS version the server will accept (sslversionmax)."> + <Col componentClass={ControlLabel} sm={2}> + Maximum TLS Version + </Col> + <Col sm={4}> + <select id="sslVersionMax" className="btn btn-default dropdown ds-select" onChange={this.handleChange} defaultValue={this.state.sslVersionMax}> + <option /> + <option>TLS1.3</option> + <option>TLS1.2</option> + <option>TLS1.1</option> + <option>TLS1.0</option> + <option>SSL3</option> + </select> + </Col> + </Row> + <Row className="ds-margin-top" title="Sets how the Directory Server enforces TLS client authentication (nsSSLClientAuth)."> + <Col componentClass={ControlLabel} sm={2}> + Client Authentication + </Col> + <Col sm={4}> + <select id="clientAuth" className="btn btn-default dropdown ds-select" onChange={this.handleChange} defaultValue={this.state.clientAuth}> + <option>off</option> + <option>allowed</option> + <option>required</option> + </select> + </Col> + </Row> + <Row className="ds-margin-top" title="Validate server's certificate expiration date (nsslapd-validate-cert)."> + <Col componentClass={ControlLabel} sm={2}> + Validate Certificate + </Col> + <Col sm={4}> + <select id="validateCert" className="btn btn-default dropdown ds-select" onChange={this.handleChange} defaultValue={this.state.validateCert}> + <option>warn</option> + <option>on</option> + <option>off</option> + </select> + </Col> + </Row> + <p /> + <Row> + <Col sm={5}> + <Checkbox + id="requireSecureBinds" + defaultChecked={this.state.requireSecureBinds} + onChange={this.handleChange} + title="Require all connections use TLS (nsslapd-require-secure-binds)." + > + Require Secure Connections + </Checkbox> + </Col> + </Row> + <Row> + <Col sm={5}> + <Checkbox + id="checkHostname" + defaultChecked={this.state.checkHostname} + onChange={this.handleChange} + title="Verify authenticity of a request by matching the host name against the value assigned to the common name (cn) attribute of the subject name (subjectDN field) in the certificate being presented. (nsslapd-ssl-check-hostname)." + > + Verify Certificate Subject Hostname + </Checkbox> + </Col> + </Row> + <Row> + <Col sm={5}> + <Checkbox + id="allowWeakCipher" + defaultChecked={this.state.allowWeakCipher} + onChange={this.handleChange} + title="Allow weak ciphers (allowWeakCipher)." + > + Allow Weak Ciphers + </Checkbox> + </Col> + </Row> + <p /> + <Button + bsStyle="primary" + className="ds-margin-top-med" + onClick={() => { + this.saveSecurityConfig(); + }} + > + Save Configuration + </Button> + </div>; + } + + securityPage = + <div className="container-fluid"> + <NotificationController + notifications={this.state.notifications} + removeNotificationAction={this.removeNotification} + /> + <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: 'Security Configuration'}} /> + </NavItem> + <NavItem eventKey={2}> + <div dangerouslySetInnerHTML={{__html: 'Certificate Management'}} /> + </NavItem> + <NavItem eventKey={3}> + <div dangerouslySetInnerHTML={{__html: 'Cipher Preferences'}} /> + </NavItem> + </Nav> + <TabContent> + <TabPane eventKey={1}> + <div className="ds-margin-top-xlg ds-indent"> + <Row> + <Col componentClass={ControlLabel} sm={2}> + Security Enabled + </Col> + <Col sm={2}> + <Switch + onChange={this.handleSwitchChange} + checked={this.state.securityEnabled} + height={20} + /> + </Col> + </Row> + <hr /> + {configPage} + </div> + </TabPane> + + <TabPane eventKey={2}> + <div className="ds-margin-top-lg"> + <CertificateManagement + serverId={this.props.serverId} + CACerts={this.state.CACerts} + ServerCerts={this.state.serverCerts} + addNotification={this.addNotification} + /> + </div> + </TabPane> + + <TabPane eventKey={3}> + <div className="ds-indent ds-tab-table"> + <Ciphers + serverId={this.props.serverId} + supportedCiphers={this.state.supportedCiphers} + cipherPref={this.state.cipherPref} + enabledCiphers={this.state.enabledCiphers} + addNotification={this.addNotification} + /> + </div> + </TabPane> + </TabContent> + </div> + </TabContainer> + </div> + </div>; + } else if (this.state.saving) { + securityPage = + <div className="ds-loading-spinner ds-center"> + <p /> + <h4>Saving security information ...</h4> + <Spinner loading size="md" /> + </div>; + } else { + securityPage = + <div className="ds-loading-spinner ds-center"> + <p /> + <h4>Loading security information ...</h4> + <Spinner loading size="md" /> + </div>; + } + return ( + <div> + {securityPage} + <ConfirmPopup + showModal={this.state.showConfirmDisable} + closeHandler={this.closeConfirmDisable} + actionFunc={this.disableSecurity} + msg="Are you sure you want to disable security?" + msgContent="Attention: this requires the server to be restarted to take effect." + /> + <SecurityEnableModal + showModal={this.state.showSecurityEnableModal} + closeHandler={this.closeSecurityEnableModal} + handleChange={this.handleSecEnableChange} + saveHandler={this.enableSecurity} + primaryName={this.state.primaryCertName} + certs={this.state.serverCerts} + spinning={this.state.secEnableSpinner} + /> + </div> + ); + } +} + +// Props and defaultProps + +Security.propTypes = { + serverId: PropTypes.string, +}; + +Security.defaultProps = { + serverId: "", +}; + +export default Security; diff --git a/src/cockpit/389-console/webpack.config.js b/src/cockpit/389-console/webpack.config.js index 8c0b433..941a6e8 100644 --- a/src/cockpit/389-console/webpack.config.js +++ b/src/cockpit/389-console/webpack.config.js @@ -34,8 +34,6 @@ var info = { "replication.js", "schema.html", "schema.js", - "security.html", - "security.js", "servers.html", "servers.js", "static", @@ -131,7 +129,18 @@ module.exports = { { exclude: /node_modules/, loader: "babel-loader", - test: /.jsx$/ + test: /.jsx$/, + options: { + presets: [ + '@babel/preset-env', + '@babel/preset-react', + { + plugins: [ + '@babel/plugin-proposal-class-properties' + ] + } + ] + }, }, { exclude: /node_modules/, diff --git a/src/lib389/cli/dsconf b/src/lib389/cli/dsconf index c0c0b4d..22635ca 100755 --- a/src/lib389/cli/dsconf +++ b/src/lib389/cli/dsconf @@ -11,14 +11,11 @@ # PYTHON_ARGCOMPLETE_OK
import argparse, argcomplete -import logging import ldap import sys import signal import json import ast -from lib389 import DirSrv -from lib389._constants import DN_CONFIG, DN_DM from lib389.cli_conf import config as cli_config from lib389.cli_conf import backend as cli_backend from lib389.cli_conf import directory_manager as cli_directory_manager diff --git a/src/lib389/lib389/__init__.py b/src/lib389/lib389/__init__.py index c7324f1..6e2e54a 100644 --- a/src/lib389/lib389/__init__.py +++ b/src/lib389/lib389/__init__.py @@ -1,5 +1,5 @@ # --- BEGIN COPYRIGHT BLOCK --- -# Copyright (C) 2015 Red Hat, Inc. +# Copyright (C) 2019 Red Hat, Inc. # Copyright (C) 2019 William Brown william@blackhats.net.au # All rights reserved. # @@ -19,40 +19,28 @@ TODO: reorganize method parameters according to SimpleLDAPObject naming: filterstr, attrlist """ -try: - from subprocess import Popen, PIPE, STDOUT - HASPOPEN = True -except ImportError: - import popen2 - HASPOPEN = False - -import io + import sys import os import stat import pwd import grp import os.path -import base64 import socket import ldif import re import ldap import ldapurl import time -import operator import shutil from datetime import datetime import logging -import decimal import glob import tarfile import subprocess from collections.abc import Callable import signal import errno -import pwd -import grp import uuid import json from shutil import copy2 @@ -63,25 +51,18 @@ import warnings import inspect
from ldap.ldapobject import SimpleLDAPObject -from ldap.cidict import cidict -from ldap import LDAPError # file in this package
from lib389._constants import * from lib389.properties import * from lib389._entry import Entry -from lib389._replication import CSN, RUV from lib389._ldifconn import LDIFConn from lib389.tools import DirSrvTools -from lib389.mit_krb5 import MitKrb5 from lib389.utils import ( ds_is_older, isLocalHost, - is_a_dn, normalizeDN, - suffixfilt, escapeDNValue, - update_newhost_with_fqdn, formatInfData, ensure_bytes, ensure_str, @@ -765,7 +746,7 @@ class DirSrv(SimpleLDAPObject, object): for pi in potential_inst: pi_dse_ldif = os.path.join(pi, 'dse.ldif') # Takes /etc/dirsrv/slapd-instance -> slapd-instance -> instance - pi_name = pi.split('/')[-1].split('-')[-1] + pi_name = pi.split('/')[-1].split('slapd-')[-1] # parse + append if os.path.exists(pi_dse_ldif): instances.append(_parse_configfile(pi_dse_ldif, pi_name)) @@ -3094,7 +3075,7 @@ class DirSrv(SimpleLDAPObject, object): ]
try: - result = subprocess.check_output(cmd, encoding='utf-8') + subprocess.check_output(cmd, encoding='utf-8') except subprocess.CalledProcessError as e: self.log.debug("Command: %s failed with the return code %s and the error %s", format_cmd_list(cmd), e.returncode, e.output) diff --git a/src/lib389/lib389/cli_conf/security.py b/src/lib389/lib389/cli_conf/security.py index 6d8c1ae..20f2574 100644 --- a/src/lib389/lib389/cli_conf/security.py +++ b/src/lib389/lib389/cli_conf/security.py @@ -8,7 +8,7 @@
from collections import OrderedDict, namedtuple import json - +import os from lib389.config import Config, Encryption, RSA from lib389.nss_ssl import NssSsl
@@ -27,45 +27,48 @@ SECURITY_ATTRS_MAP = OrderedDict([ ('secure-port', Props(Config, 'nsslapd-securePort', 'Port for LDAPS to listen on', range(1, 65536))), - ('tls-client-auth', Props(Config, 'nsSSLClientAuth', - 'Client authentication requirement', - ('off', 'allowed', 'required'))), + ('tls-client-auth', Props(Encryption, 'nsSSLClientAuth', + 'Client authentication requirement', + ('off', 'allowed', 'required'))), ('require-secure-authentication', Props(Config, 'nsslapd-require-secure-binds', - 'Require binds over LDAPS, StartTLS, or SASL', - onoff)), + 'Require binds over LDAPS, StartTLS, or SASL', + onoff)), ('check-hostname', Props(Config, 'nsslapd-ssl-check-hostname', 'Check Subject of remote certificate against the hostname', onoff)), ('verify-cert-chain-on-startup', Props(Config, 'nsslapd-validate-cert', - 'Validate server certificate during startup', - ('warn', *onoff))), + 'Validate server certificate during startup', + ('warn', *onoff))), ('session-timeout', Props(Encryption, 'nsSSLSessionTimeout', 'Secure session timeout', int)), ('tls-protocol-min', Props(Encryption, 'sslVersionMin', - 'Secure protocol minimal allowed version', - protocol_versions)), + 'Secure protocol minimal allowed version', + protocol_versions)), ('tls-protocol-max', Props(Encryption, 'sslVersionMax', - 'Secure protocol maximal allowed version', - protocol_versions)), + 'Secure protocol maximal allowed version', + protocol_versions)), ('allow-insecure-ciphers', Props(Encryption, 'allowWeakCipher', - 'Allow weak ciphers for legacy use', - onoff)), + 'Allow weak ciphers for legacy use', + onoff)), ('allow-weak-dh-param', Props(Encryption, 'allowWeakDHParam', 'Allow short DH params for legacy use', onoff)), + ('cipher-pref', Props(Encryption, 'nsSSL3Ciphers', + 'Use this command to directly set nsSSL3Ciphers attribute. It is a comma separated list ' + 'of cipher names (prefixed with + or -), optionally including +all or -all. The attribute ' + 'may optionally be prefixed by keyword default. Please refer to documentation of ' + 'the attribute for a more detailed description.', + onoff)), ])
RSA_ATTRS_MAP = OrderedDict([ ('tls-allow-rsa-certificates', Props(RSA, 'nsSSLActivation', - 'Activate use of RSA certificates', - onoff)), + 'Activate use of RSA certificates', onoff)), ('nss-cert-name', Props(RSA, 'nsSSLPersonalitySSL', - 'Server certificate name in NSS DB', - str)), + 'Server certificate name in NSS DB', str)), ('nss-token', Props(RSA, 'nsSSLToken', - 'Security token name (module of NSS DB)', - str)) + 'Security token name (module of NSS DB)', str)) ])
@@ -73,7 +76,9 @@ def _security_generic_get(inst, basedn, logs, args, attrs_map): result = {} for attr, props in attrs_map.items(): val = props.cls(inst).get_attr_val_utf8(props.attr) - result[props.attr] = val + if val is None: + val = "" + result[props.attr.lower()] = val if args.json: print(json.dumps({'type': 'list', 'items': result})) else: @@ -126,14 +131,22 @@ def _security_generic_toggle_parsers(parent, cls, attr, help_pattern):
return list(map(add_parser, ('Enable', 'Disable'), ('on', 'off')))
- def security_enable(inst, basedn, log, args): dbpath = inst.get_cert_dir() tlsdb = NssSsl(dbpath=dbpath) - if not tlsdb._db_exists(even_partial=True): # we want to be very careful - log.info(f'Secure database does not exist. Creating a new one in {dbpath}.') - tlsdb.reinit() - + certs = tlsdb.list_certs() + if len(certs) == 0: + raise ValueError('There are no server certificates in the security ' + + 'database, security can not be enabled.') + + if len(certs) == 1: + # If there is only cert make sure it is set as the server certificate + RSA(inst).set('nsSSLPersonalitySSL', certs[0][0]) + elif args.cert_name is not None: + # A certificate nickname was provided, set it as the server certificate + RSA(inst).set('nsSSLPersonalitySSL', args.cert_name) + + # it should now be safe to enable security Config(inst).set('nsslapd-security', 'on')
@@ -184,29 +197,246 @@ def security_ciphers_list(inst, basedn, log, args): print(*lst, sep='\n')
+def cert_add(inst, basedn, log, args): + """Add server certificate + """ + # Verify file and certificate name + os.path.isfile(args.file) + tlsdb = NssSsl(dirsrv=inst) + if not tlsdb._db_exists(even_partial=True): # we want to be very careful + log.info('Security database does not exist. Creating a new one in {}.'.format(inst.get_cert_dir())) + tlsdb.reinit() + + try: + tlsdb.get_cert_details(args.name) + raise ValueError("Certificate already exists with the same name") + except ValueError: + pass + + if args.primary_cert: + # This is the server's primary certificate, update RSA entry + RSA(inst).set('nsSSLPersonalitySSL', args.name) + + # Add the cert + tlsdb.add_cert(args.name, args.file) + + +def cacert_add(inst, basedn, log, args): + """Add CA certificate + """ + # Verify file and certificate name + os.path.isfile(args.file) + tlsdb = NssSsl(dirsrv=inst) + if not tlsdb._db_exists(even_partial=True): # we want to be very careful + log.info('Security database does not exist. Creating a new one in {}.'.format(inst.get_cert_dir())) + tlsdb.reinit() + + try: + tlsdb.get_cert_details(args.name) + raise ValueError("Certificate already exists with the same name") + except ValueError: + pass + + # Add the cert + tlsdb.add_cert(args.name, args.file, ca=True) + + +def cert_list(inst, basedn, log, args): + """List all the server certificates + """ + cert_list = [] + tlsdb = NssSsl(dirsrv=inst) + certs = tlsdb.list_certs() + for cert in certs: + if args.json: + cert_list.append( + { + "type": "certificate", + "attrs": { + 'nickname': cert[0], + 'subject': cert[1], + 'issuer': cert[2], + 'expires': cert[3], + 'flags': cert[4], + } + } + ) + else: + log.info('Certificate Name: {}'.format(cert[0])) + log.info('Subject DN: {}'.format(cert[1])) + log.info('Issuer DN: {}'.format(cert[2])) + log.info('Expires: {}'.format(cert[3])) + log.info('Trust Flags: {}\n'.format(cert[4])) + if args.json: + log.info(json.dumps(cert_list)) + + +def cacert_list(inst, basedn, log, args): + """List all CA certs + """ + cert_list = [] + tlsdb = NssSsl(dirsrv=inst) + certs = tlsdb.list_certs(ca=True) + for cert in certs: + if args.json: + cert_list.append( + { + "type": "certificate", + "attrs": { + 'nickname': cert[0], + 'subject': cert[1], + 'issuer': cert[2], + 'expires': cert[3], + 'flags': cert[4], + } + } + ) + else: + log.info('Certificate Name: {}'.format(cert[0])) + log.info('Subject DN: {}'.format(cert[1])) + log.info('Issuer DN: {}'.format(cert[2])) + log.info('Expires: {}'.format(cert[3])) + log.info('Trust Flags: {}\n'.format(cert[4])) + if args.json: + log.info(json.dumps(cert_list)) + + +def cert_get(inst, basedn, log, args): + """Get the details about a server certificate + """ + tlsdb = NssSsl(dirsrv=inst) + details = tlsdb.get_cert_details(args.name) + if args.json: + log.info(json.dumps( + { + "type": "certificate", + "attrs": { + 'nickname': details[0], + 'subject': details[1], + 'issuer': details[2], + 'expires': details[3], + 'flags': details[4], + } + } + ) + ) + else: + log.info('Certificate Name: {}'.format(details[0])) + log.info('Subject DN: {}'.format(details[1])) + log.info('Issuer DN: {}'.format(details[2])) + log.info('Expires: {}'.format(details[3])) + log.info('Trust Flags: {}'.format(details[4])) + + +def cert_edit(inst, basedn, log, args): + """Edit cert + """ + tlsdb = NssSsl(dirsrv=inst) + tlsdb.edit_cert_trust(args.name, args.flags) + + +def cert_del(inst, basedn, log, args): + """Delete cert + """ + tlsdb = NssSsl(dirsrv=inst) + tlsdb.del_cert(args.name) + + def create_parser(subparsers): security = subparsers.add_parser('security', help='Query and manipulate security options') security_sub = security.add_subparsers(help='security') - security_set = _security_generic_set_parser(security_sub, SECURITY_ATTRS_MAP, 'Set general security options', + + # Core security management + _security_generic_set_parser(security_sub, SECURITY_ATTRS_MAP, 'Set general security options', ('Use this command for setting security related options located in cn=config and cn=encryption,cn=config.' '\n\nTo enable/disable security you can use enable and disable commands instead.')) - security_get = _security_generic_get_parser(security_sub, SECURITY_ATTRS_MAP, 'Get general security options') + _security_generic_get_parser(security_sub, SECURITY_ATTRS_MAP, 'Get general security options') security_enable_p = security_sub.add_parser('enable', help='Enable security', description=( 'If missing, create security database, then turn on security functionality. Please note this is usually not' - ' enought for TLS connections to work - proper setup of CA and server certificate is necessary.')) + ' enough for TLS connections to work - proper setup of CA and server certificate is necessary.')) + security_enable_p.add_argument('--cert-name', default=None, + help='The name of the certificate the server should use') security_enable_p.set_defaults(func=security_enable) security_disable_p = security_sub.add_parser('disable', help='Disable security', description=( 'Turn off security functionality. The rest of the configuration will be left untouched.')) security_disable_p.set_defaults(func=security_disable)
- rsa = security_sub.add_parser('rsa', help='Query and mainpulate RSA security options') + # Server certificate management + certs = security_sub.add_parser('certificate', help='Manage TLS certificates') + certs_sub = certs.add_subparsers(help='certificate') + cert_add_parser = certs_sub.add_parser('add', help='Add a server certificate', description=( + 'Add a server certificate to the NSS database')) + cert_add_parser.add_argument('--file', required=True, + help='The file name of the certificate') + cert_add_parser.add_argument('--name', required=True, + help='The name/nickname of the certificate') + cert_add_parser.add_argument('--primary-cert', action='store_true', + help="Set this certificate as the server's certificate") + cert_add_parser.set_defaults(func=cert_add) + + cert_edit_parser = certs_sub.add_parser('set-trust-flags', help='Set the Trust flags', + description=('Change the trust flags of a server certificate')) + cert_edit_parser.add_argument('name', help='The name/nickname of the certificate') + cert_edit_parser.add_argument('--flags', required=True, + help='The trust flags for the server certificate') + cert_edit_parser.set_defaults(func=cert_edit) + + cert_del_parser = certs_sub.add_parser('del', help='Delete a certificate', + description=('Delete a certificate from the NSS database')) + cert_del_parser.add_argument('name', help='The name/nickname of the certificate') + cert_del_parser.set_defaults(func=cert_del) + + cert_get_parser = certs_sub.add_parser('get', help="Get a server certificate's information", + description=('Get detailed information about a certificate, like trust attributes, expiration dates, Subject and Issuer DNs ')) + cert_get_parser.add_argument('name', help='The name/nickname of the certificate') + cert_get_parser.set_defaults(func=cert_get) + + cert_list_parser = certs_sub.add_parser('list', help='List the server certificates', + description=('List the server certificates in the NSS database')) + cert_list_parser.set_defaults(func=cert_list) + + # CA certificate management + cacerts = security_sub.add_parser('ca-certificate', help='Manage TLS Certificate Authorities') + cacerts_sub = cacerts.add_subparsers(help='ca-certificate') + cacert_add_parser = cacerts_sub.add_parser('add', help='Add a Certificate Authority', description=( + 'Add a Certificate Authority to the NSS database')) + cacert_add_parser.add_argument('--file', required=True, + help='The file name of the CA certificate') + cacert_add_parser.add_argument('--name', required=True, + help='The name/nickname of the CA certificate') + cacert_add_parser.set_defaults(func=cacert_add) + + cacert_edit_parser = cacerts_sub.add_parser('set-trust-flags', help='Set the Trust flags', + description=('Change the trust attributes of a CA certificate. Certificate Authorities typically use "CT,,"')) + cacert_edit_parser.add_argument('name', help='The name/nickname of the CA certificate') + cacert_edit_parser.add_argument('--flags', required=True, + help='The trust flags for the CA certificate') + cacert_edit_parser.set_defaults(func=cert_edit) + + cacert_del_parser = cacerts_sub.add_parser('del', help='Delete a certificate', + description=('Delete a CA certificate from the NSS database')) + cacert_del_parser.add_argument('name', help='The name/nickname of the CA certificate') + cacert_del_parser.set_defaults(func=cert_del) + + cacert_get_parser = cacerts_sub.add_parser('get', help="Get a Certificate Authority's information", + description=('Get detailed information about a CA certificate, like trust attributes, expiration dates, Subject and Issuer DN')) + cacert_get_parser.add_argument('name', help='The name/nickname of the CA certificate') + cacert_get_parser.set_defaults(func=cert_get) + + cacert_list_parser = cacerts_sub.add_parser('list', help='List the Certificate Authorities', + description=('List the CA certificates in the NSS database')) + cacert_list_parser.set_defaults(func=cacert_list) + + # RSA management + rsa = security_sub.add_parser('rsa', help='Query and manipulate RSA security options') rsa_sub = rsa.add_subparsers(help='rsa') - rsa_set = _security_generic_set_parser(rsa_sub, RSA_ATTRS_MAP, 'Set RSA security options', + _security_generic_set_parser(rsa_sub, RSA_ATTRS_MAP, 'Set RSA security options', ('Use this command for setting RSA (private key) related options located in cn=RSA,cn=encryption,cn=config.' '\n\nTo enable/disable RSA you can use enable and disable commands instead.')) - rsa_get = _security_generic_get_parser(rsa_sub, RSA_ATTRS_MAP, 'Get RSA security options') - rsa_toggles = _security_generic_toggle_parsers(rsa_sub, RSA, 'nsSSLActivation', '{} RSA') + _security_generic_get_parser(rsa_sub, RSA_ATTRS_MAP, 'Get RSA security options') + _security_generic_toggle_parsers(rsa_sub, RSA, 'nsSSLActivation', '{} RSA')
+ # Cipher management ciphers = security_sub.add_parser('ciphers', help='Manage secure ciphers') ciphers_sub = ciphers.add_subparsers(help='ciphers')
@@ -226,7 +456,7 @@ def create_parser(subparsers):
ciphers_set = ciphers_sub.add_parser('set', help='Set ciphers attribute', description=( 'Use this command to directly set nsSSL3Ciphers attribute. It is a comma separated list ' - 'of cipher names (prefixed with + or -), optionaly including +all or -all. The attribute ' + 'of cipher names (prefixed with + or -), optionally including +all or -all. The attribute ' 'may optionally be prefixed by keyword default. Please refer to documentation of ' 'the attribute for a more detailed description.')) ciphers_set.set_defaults(func=security_ciphers_set) diff --git a/src/lib389/lib389/config.py b/src/lib389/lib389/config.py index c2a34fa..23ab9f2 100644 --- a/src/lib389/lib389/config.py +++ b/src/lib389/lib389/config.py @@ -20,9 +20,7 @@ import ldap from lib389._constants import * from lib389 import Entry from lib389._mapped_object import DSLdapObject -from lib389.dseldif import DSEldif -from lib389.utils import ensure_bytes, ensure_str - +from lib389.utils import ensure_bytes, selinux_label_port, selinux_present from lib389.lint import DSCLE0001, DSCLE0002, DSELE0001
class Config(DSLdapObject): @@ -37,7 +35,7 @@ class Config(DSLdapObject): super(Config, self).__init__(instance=conn) self._dn = DN_CONFIG # self._instance = conn - # self.log = conn.log + self.log = conn.log config_compare_exclude = [ 'nsslapd-ldapifilepath', 'nsslapd-accesslog', @@ -65,6 +63,16 @@ class Config(DSLdapObject): def rdn(self): return DN_CONFIG
+ def replace(self, key, value): + if key.lower() == 'nsslapd-secureport' and selinux_present(): + # Get old port and remove label + old_port = self.get_attr_val_utf8(key) + self.log.debug("Removing old port's selinux label...") + selinux_label_port(old_port, remove_label=True) + self.log.debug("Setting new port's selinux label...") + selinux_label_port(value) + super(Config, self).replace(key, value) + def _alter_log_enabled(self, service, state): if service not in ('access', 'error', 'audit'): self._log.error('Attempted to enable invalid log service "%s"' % service) @@ -245,7 +253,10 @@ class Encryption(DSLdapObject): :returns: list of str """ val = self.get_attr_val_utf8('nsSSL3Ciphers') - return val.split(',') if val else [] + if val: + return val.split(',') + else: + return ['default']
@ciphers.setter def ciphers(self, ciphers): @@ -370,7 +381,7 @@ class CertmapLegacy(object):
def _parse_maps(self, maps): certmaps = {} - cur_map = None + for l in maps: if l.startswith('certmap'): # Line matches format of: certmap name issuer @@ -457,10 +468,7 @@ class LDBMConfig(DSLdapObject): def __init__(self, conn): super(LDBMConfig, self).__init__(instance=conn) self._dn = DN_CONFIG_LDBM - config_compare_exclude = [] + # config_compare_exclude = [] self._rdn_attribute = 'cn' self._lint_functions = [] self._protected = True - - - diff --git a/src/lib389/lib389/nss_ssl.py b/src/lib389/lib389/nss_ssl.py index a54095c..8af7132 100644 --- a/src/lib389/lib389/nss_ssl.py +++ b/src/lib389/lib389/nss_ssl.py @@ -10,9 +10,6 @@ """
import os -import sys -import random -import string import re import socket import time @@ -24,7 +21,7 @@ from datetime import datetime, timedelta from subprocess import check_output from lib389.passwd import password_generate
-from lib389.utils import ensure_str, ensure_bytes, format_cmd_list +from lib389.utils import ensure_str, format_cmd_list import uuid
KEYBITS = 4096 @@ -362,8 +359,9 @@ only. # Now make the lines usable cert_values = [] for line in lines: - data = line.split() - cert_values.append((data[0], data[1])) + if line == '': + continue + cert_values.append(re.match(r'^(.+[^\s])[\s]+([^\s]+)$', line.rstrip()).groups()) return cert_values
def _rsa_cert_key_exists(self, cert_tuple): @@ -380,7 +378,6 @@ only. result = ensure_str(check_output(cmd, stderr=subprocess.STDOUT))
lines = result.split('\n')[1:-1] - key_list = [] for line in lines: m = re.match('<(?P<id>.*)> (?P<type>\w+)\s+(?P<hash>\w+).*:(?P<name>.+)', line) if name == m.group('name'): @@ -712,3 +709,148 @@ only. crt_der_path = '%s/%s%s.der' % (self._certdb, USER_PREFIX, name) return {'ca': ca_path, 'key': key_path, 'crt': crt_path, 'crt_der_path': crt_der_path}
+ # Certificate helper functions + def del_cert(self, nickname): + """Delete this certificate + """ + cmd = [ + '/usr/bin/certutil', + '-D', + '-d', self._certdb, + '-n', nickname, + '-f', + '%s/%s' % (self._certdb, PWD_TXT), + ] + self.log.debug("del_cert cmd: %s", format_cmd_list(cmd)) + check_output(cmd, stderr=subprocess.STDOUT) + + def edit_cert_trust(self, nickname, trust_flags): + """Edit trust flags + """ + + # validate trust flags + flag_sections = trust_flags.split(',') + if len(flag_sections) != 3: + raise ValueError("Invalid trust flag format") + + for section in flag_sections: + if len(section) > 6: + raise ValueError("Invalid trust flag format, too many flags in a section") + + for c in trust_flags: + if c not in ['p', 'P', 'c', 'C', 'T', 'u', ',']: + raise ValueError("Invalid trust flag {}".format(c)) + + # Modify certificate flags + cmd = [ + '/usr/bin/certutil', + '-M', + '-d', self._certdb, + '-n', nickname, + '-t', trust_flags, + '-f', + '%s/%s' % (self._certdb, PWD_TXT), + ] + self.log.debug("edit_cert_trust cmd: %s", format_cmd_list(cmd)) + check_output(cmd, stderr=subprocess.STDOUT) + + + def get_cert_details(self, nickname): + """Get the trust flags, subject DN, issuer, and expiration date + + return a list: + 0 - nickname + 1 - subject + 2 - issuer + 3 - expire date + 4 - trust_flags + """ + all_certs = self._rsa_cert_list() + for cert in all_certs: + if cert[0] == nickname: + trust_flags = cert[1] + cmd = [ + '/usr/bin/certutil', + '-d', self._certdb, + '-n', nickname, + '-L', + '-f', + '%s/%s' % (self._certdb, PWD_TXT), + ] + self.log.debug("get_cert_details cmd: %s", format_cmd_list(cmd)) + + # Expiration date + certdetails = check_output(cmd, stderr=subprocess.STDOUT, encoding='utf-8') + end_date_str = certdetails.split("Not After : ")[1].split("\n")[0] + date_format = '%a %b %d %H:%M:%S %Y' + end_date = datetime.strptime(end_date_str, date_format) + + # Subject DN + subject = "" + for line in certdetails.splitlines(): + line = line.lstrip() + if line.startswith("Subject: "): + subject = line.split("Subject: ")[1].split("\n")[0] + elif subject != "": + if not line.startswith("Subject Public Key Info:"): + subject += line + else: + # Done, strip off quotes + subject = subject[1:-1] + break + + # Issuer + issuer = "" + for line in certdetails.splitlines(): + line = line.lstrip() + if line.startswith("Issuer: "): + issuer = line.split("Issuer: ")[1].split("\n")[0] + elif issuer != "": + if not line.startswith("Validity:"): + issuer += line + else: + issuer = issuer[1:-1] + break + + return ([nickname, subject, issuer, str(end_date), trust_flags]) + + # Did not find cert with that name + raise ValueError("Certificate '{}' not found in NSS database".format(nickname)) + + + def list_certs(self, ca=False): + all_certs = self._rsa_cert_list() + certs = [] + for cert in all_certs: + trust_flags = cert[1] + if (ca and "CT" in trust_flags) or (not ca and "CT" not in trust_flags): + certs.append(self.get_cert_details(cert[0])) + return certs + + + def add_cert(self, nickname, input_file, ca=False): + """Add server or CA cert + """ + + # Verify input_file exists + if not os.path.exists(input_file): + raise ValueError("The certificate file ({}) does not exist".format(input_file)) + + if ca: + trust_flags = "CT,," + else: + trust_flags = ",," + + cmd = [ + '/usr/bin/certutil', + '-A', + '-d', self._certdb, + '-n', nickname, + '-t', trust_flags, + '-i', input_file, + '-a', + '-f', + '%s/%s' % (self._certdb, PWD_TXT), + ] + self.log.debug("add_cert cmd: %s", format_cmd_list(cmd)) + check_output(cmd, stderr=subprocess.STDOUT) diff --git a/src/lib389/lib389/utils.py b/src/lib389/lib389/utils.py index 1472309..10c8cae 100644 --- a/src/lib389/lib389/utils.py +++ b/src/lib389/lib389/utils.py @@ -234,7 +234,7 @@ def selinux_label_port(port, remove_label=False): """ Either set or remove an SELinux label(ldap_port_t) for a TCP port
- :param port: The TCP port to be labelled + :param port: The TCP port to be labeled :type port: str :param remove_label: Set True if the port label should be removed :type remove_label: boolean @@ -258,9 +258,10 @@ def selinux_label_port(port, remove_label=False):
# We only label ports that ARE NOT in the default policy that comes with # a RH based system. + port = int(port) selinux_default_ports = [389, 636, 3268, 3269, 7389] if port in selinux_default_ports: - log.debug('port %s already in %s, skipping port relabel' % (port, selinux_default_ports)) + log.debug('port {} already in {}, skipping port relabel'.format(port, selinux_default_ports)) return
label_set = False @@ -283,11 +284,10 @@ def selinux_label_port(port, remove_label=False): elif not remove_label: # Port belongs to someone else (bad) # This is only an issue during setting a label, not removing a label - raise ValueError("Port {} was already labelled with: ({}) Please choose a different port number".format(port, policy['type'])) + raise ValueError("Port {} was already labeled with: ({}) Please choose a different port number".format(port, policy['type']))
if (remove_label and label_set) or (not remove_label and not label_set): for i in range(5): - try: subprocess.check_call(["semanage", "port", "-d" if remove_label else "-a",
389-commits@lists.fedoraproject.org