diff options
| author | miramurali23 <miramurali23@gmail.com> | 2019-06-17 13:07:24 -0500 |
|---|---|---|
| committer | Gunnar Mills <gmills@us.ibm.com> | 2019-09-26 20:04:56 +0000 |
| commit | afc8a799627b71bba716e207cee8185852a6d390 (patch) | |
| tree | 69a9bf15b0603a51bf8194c218aba6a0d20e5409 /app/access-control | |
| parent | 5e258e43070b46b9d1ec5ec01e02b9f707cbf7b8 (diff) | |
| download | phosphor-webui-afc8a799627b71bba716e207cee8185852a6d390.tar.gz phosphor-webui-afc8a799627b71bba716e207cee8185852a6d390.zip | |
Update users navigation section
- Changed the section name to be access-control
- Moved LDAP Settings and Certificate Management to access-control navigation
- Changed Manage User Account subsection name to Local User Management
Resolves: openbmc/phosphor-webui#619
Signed-off-by: Mira Murali <miramurali23@gmail.com>
Signed-off-by: Derick Montague <derick.montague@ibm.com>
Change-Id: I0d94c80c295b997d94c04330fd87f4fc4d229bf8
Diffstat (limited to 'app/access-control')
17 files changed, 2449 insertions, 0 deletions
diff --git a/app/access-control/controllers/certificate-controller.html b/app/access-control/controllers/certificate-controller.html new file mode 100644 index 0000000..4226262 --- /dev/null +++ b/app/access-control/controllers/certificate-controller.html @@ -0,0 +1,294 @@ +<loader loading="loading"></loader> +<div id="configuration-cert"> + <div class="row column"> + <h1>SSL certificates</h1> + </div> + <div ng-repeat="certificate in certificates | filter:{isExpiring:true}" class="row column"> + <div class="small-12 alert alert-warning" role="alert"> + <icon file="icon-warning.svg" aria-hidden="true"></icon> + The uploaded {{certificate.name}} is expiring in {{getDays(certificate.ValidNotAfter) === 0 ? 'less than one day!' : getDays(certificate.ValidNotAfter) + + ' days!'}} Consider replacing it with a new certificate. + </div> + </div> + <div ng-repeat="certificate in certificates|filter:{isExpired:true}" class="row column"> + <div class="small-12 alert alert-danger" role="alert"> + <div class="icon__critical inline"></div> The uploaded {{certificate.name}} has expired! Consider replacing it with a new certificate. + </div> + </div> + <div class="row column"> + <button type="button" class="btn btn-tertiary" ng-disabled="availableCertificateTypes.length === 0" ng-click="addCertificateModal=true"> + <icon class="icon-add" file="icon-plus.svg"></icon> + Add new certificate + </button> + <button type="button" class="btn btn-tertiary" ng-click="addCSRModal=true"> + <icon class="icon-add" file="icon-plus.svg"></icon> + Generate CSR + </button> + </div> + <div class="row column"> + <div class="small-12 certificate__table"> + <div class="table__row-header"> + <div class="row column"> + <div class="certificate__type-header"> + Certificate + </div> + <div class="certificate__issue-header"> + Issued by + </div> + <div class="certificate__issue-header"> + Issued to + </div> + <div class="certificate__date-header"> + Valid from + </div> + <div class="certificate__status-header"> + </div> + <div class="certificate__date-header"> + Valid until + </div> + </div> + </div> + <div ng-if="certificates.length < 1" class="empty__logs">There have been no certificates added.</div> + <div ng-repeat="certificate in certificates"> + <certificate cert="certificate" reload="loadCertificates()" )></certificate> + </div> + </div> + </div> + <section class="modal add__certificate__modal" aria-hidden="true" role="dialog" ng-class="{'active': addCertificateModal}"> + <form name="add__cert__form" id="add__cert__form" ng-class="{'submitted': submitted}"> + <div class="modal__content"> + <button class="certificate__close-modal" ng-click="addCertificateModal = false; add__cert__form.$setUntouched()"> + <icon aria-hidden="true" file="icon-close.svg"> + </button> + <h2 class="page-header">Add new certificate</h2> + <div class="row column add-certificate__section "> + <div class="small-12"> + <label for="cert__type">Certificate type</label> + <select id="cert__type" name="cert__type" ng-model="newCertificate.selectedType" required> + <option class="courier-bold" ng-value="">Select an option</option> + <option class="courier-bold" ng-value="type" ng-repeat="type in availableCertificateTypes"> + {{type.name}}</option> + </select> + <div ng-messages="add__cert__form.cert__type.$error" class="form-error" ng-class="{'visible' : add__cert__form.cert__type.$touched || submitted }"> + <p ng-message="required">Field is required</p> + </div> + </div> + </div> + <div class="row column add-certificate__section"> + <div class="small-12"> + <label class="select__new-label" for="upload_cert_new">Certificate file</label> + </div> + <div class="row column file__upload add-certificate__section "> + <label for='upload_cert_new'> + <input name="upload_cert_new" id="upload_cert_new" type="file" file="newCertificate.file" class="hide" /> + <span class="btn btn-secondary select__new-button">Choose file</span> + </label> + </div> + <div class="row column add-certificate__section "> + <div ng-if="newCertificate.file" class="small-7 file__name"> + <span>{{newCertificate.file.name}}</span> + <icon file="icon-close.svg" ng-if="newCertificate.file.name" ng-click="newCertificate.file = '';" class="float-right"></icon> + </div> + </div> + </div> + </div> + <div class="modal__button-wrapper"> + <button class="btn btn-secondary" ng-click="addCertificateModal = false; newCertificate={};add__cert__form.$setUntouched();">Cancel</button> + <button class="btn btn-primary" ng-disabled="add__cert__form.$invalid || !newCertificate.file" ng-click="submitted = true; uploadCertificate();">Save</button> + </div> + </form> + </section> + + <section class="modal add-csr__modal" aria-hidden="true" role="dialog" ng-class="{'active': addCSRModal}"> + <!--Close button for displaying CSR Code, we need a close button within form to reset form validation correctly--> + <button class="certificate__close-modal" ng-click="resetCSRModal();" ng-if="displayCSRCode==true"> + <icon aria-hidden="true" file="icon-close.svg"> + </button> + + <!-- CSR Code display content--> + + <div ng-if="displayCSRCode==true"> + <h2 class="page-header">Certificate Signing Request (CSR)</h2> + <div class="modal__content add-csr__container"> + <span id="csrCode" class="add-csr__container-csr-code">{{csrCode}}</span> + </div> + <div class="modal__button-wrapper"> + <button class="btn btn-secondary" clipboard text="csrCode" on-copied="copySuccess(event)" on-error="copyfailed(err)"> + <span ng-if="!copied">Copy</span> + <span ng-if="copied"> + <icon aria-hidden="true" file="icon-check.svg"></icon> + <span>Copied</span> + </span> + </button> + <button class="btn btn-primary" ng-click="addCSRModal = false;"> + <a ng-href="data:text/json;charset=utf-8,{{csrCode}}" download="csrCode.txt" class="add-csr__text-download"> + Download</a> + </button> + </div> + + </div> + + + + <form name="add__csr__form" id="add__csr__form" novalidate ng-if="displayCSRCode==false"> + <div class="modal__content add-csr__container"> + <button class="certificate__close-modal" ng-click="resetCSRModal(); add__csr__form.$setUntouched()"> + <icon aria-hidden="true" file="icon-close.svg"> + </button> + <h2 class="page-header">Generate a Certificate Signing Request (CSR)</h2> + <div class="row"> + <fieldset class="column medium-8 add-csr__section"> + <legend class="add-csr__section-title">General</legend> + <div class="row"> + <div class="column medium-6"> + <label for="cert__type" class="add-csr__label">Certificate Type *</label> + <select class="add-csr__select" id="cert__type" name="cert__type" ng-model="newCSR.certificateCollection" required> + <option class="courier-bold" ng-value="default" ng-model="selectOption">Select an option</option> + <!-- Do not show CA certificate as an option. Only a certificate authority can generate a CA certificate (known as TrustStore Certificate in Redfish) --> + <option class="courier-bold" ng-value="type" ng-repeat="type in allCertificateTypes" ng-if="type.Description !== 'TrustStore Certificate'"> + {{type.name}}</option> + </select> + <div ng-messages="add__csr__form.cert__type.$error" class="form-error" ng-class="{'visible' : add__csr__form.cert__type.$touched}"> + <p ng-message="required">Field is required</p> + </div> + </div> + <div class="column medium-6"> + <label for="countryCode" class="add-csr__label">Country *</label> + <select class="add-csr__select" id="countryCode" name="countryCode" ng-model="newCSR.countryCode" required> + <option class="courier-bold" ng-value="" ng-model="selectOption">Select an option</option> + <option class="courier-bold" ng-value="country" ng-repeat="country in countryList">{{country.Name}} + </option> + </select> + <div ng-messages="add__csr__form.countryCode.$error" class="form-error" ng-class="{'visible' : add__csr__form.countryCode.$touched}"> + <p ng-message="required">Field is required</p> + </div> + </div> + + <div class="column medium-6"> + <label for="state" class="add-csr__label">State *</label> + <input class="add-csr__input" ng-model="newCSR.state" type="text" id="state" name="state" required></input> + <div ng-messages="add__csr__form.state.$error" class="form-error" ng-class="{'visible' : add__csr__form.state.$touched}"> + <p ng-message="required">Field is required</p> + </div> + </div> + + <div class="column medium-6"> + <label for="city" class="add-csr__label">City *</label> + <input class="add-csr__input" id="city" name="city" ng-model="newCSR.city" type="text" required></input> + <div ng-messages="add__csr__form.city.$error" class="form-error" ng-class="{'visible' : add__csr__form.city.$touched}"> + <p ng-message="required">Field is required</p> + </div> + </div> + + <div class="column medium-6"> + <label for="companyName" class="add-csr__label">Company Name *</label> + <input class="add-csr__input" type="text" ng-model="newCSR.organization" id="companyName" name="companyName" required></input> + <div ng-messages="add__csr__form.companyName.$error" class="form-error" ng-class="{'visible' : add__csr__form.companyName.$touched}"> + <p ng-message="required">Field is required</p> + </div> + </div> + + <div class="column medium-6"> + <label for="companyUnit" class="add-csr__label">Company Unit *</label> + <input class="add-csr__input" ng-model="newCSR.companyUnit" name="companyUnit" id="companyUnit" type="text" required></input> + <div ng-messages="add__csr__form.companyUnit.$error" class="form-error" ng-class="{'visible' : add__csr__form.companyUnit.$touched}"> + <p ng-message="required">Field is required</p> + </div> + </div> + + <div class="column medium-6"> + <label for="commonName" class="add-csr__label">Common Name *</label> + <input class="add-csr__input" ng-model="newCSR.commonName" name="commonName" type="text" id="commonName" required></input> + <div ng-messages="add__csr__form.commonName.$error" class="form-error" ng-class="{'visible' : add__csr__form.commonName.$touched}"> + <p ng-message="required">Field is required</p> + </div> + </div> + + <div class="column medium-6"> + <label for="challengePassword" class="add-csr__label">Challenge Password</label> + <input class="add-csr__input-no-validation" id="challengePassword" ng-model="newCSR.challengePassword" type="text"></input> + </div> + + <div class="column medium-6"> + <label for="contactPerson" class="add-csr__label">Contact Person</label> + <input class="add-csr__input-no-validation" id="contactPerson" ng-model="newCSR.contactPerson" type="text"></input> + </div> + + <div class="column medium-6"> + <label for="emailAddress" class="add-csr__label">Email Address</label> + <input class="add-csr__input-no-validation" id="emailAddress" ng-model="newCSR.emailAddress" type="text"></input> + </div> + + <div class="column medium-6"> + <div> + <label id="alternate-name-label" for="alternateName" class="add-csr__label">Alternate Name</label> + <input class="add-csr__input-no-validation" ng-model="newCSR.firstAlternativeName" id="alternateName" name="alternativeName" + type="text"></input> + </div> + <div class="add-csr__additional-alt-names" ng-repeat="name in names"> + <input id="alternate-name-input-{{$index}}" aria-describedby="alternate-name-label" class="add-csr__input-no-validation" + ng-model="name.Value" type="text"></input> + <button aria-label="Delete alternate name field" aria-controls="alternate-name-input-{{$index}}" class="btn btn-tertiary add-csr__alt-name-delete-btn" + ng-click="deleteOptionalRow($index)" ng-disabled="multiSelected"> + <icon aria-hidden="true" file="icon-trashcan.svg"> + </button> + </div> + </div> + + <div class="column medium-6"> + <button class="btn btn-tertiary add-csr__alt-name-add-btn" ng-click="addOptionalRow()"> + <icon file="icon-plus.svg"></icon> + Add another alternate name + </button> + </div> + </div> + </fieldset> + + <fieldset class="column medium-4 add-csr__section add-csr__section--border "> + <legend class="add-csr__section-title">Private key</legend> + <div class="add-csr__container-private-key"> + <div class="add-csr__content-private-key"> + <label for="keyPairAlgorithm" class="add-csr__label">Key Pair Algorithm *</label> + <select class="add-csr__select" ng-model="newCSR.keyPairAlgorithm" id="keyPairAlgorithm" name="keyPairAlgorithm" required> + <option class="courier-bold" ng-value="" ng-model="selectOption">Select an option</option> + <option class="courier-bold" ng-value="data" ng-repeat="data in keyPairAlgorithm">{{data}}</option> + </select> + <div ng-messages="add__csr__form.keyPairAlgorithm.$error" class="form-error" ng-class="{'visible' : add__csr__form.keyPairAlgorithm.$touched}"> + <p ng-message="required">Field is required</p> + </div> + + <div ng-if="newCSR.keyPairAlgorithm == 'EC'"> + <label for="keyCurveId" class="add-csr__label">Key Curve ID</label> + <select class="add-csr__select" ng-model="newCSR.keyCurveId" id="keyCurveId" name="keyCurveId" required> + <option class="courier-bold" ng-value="">None</option> + <option class="courier-bold" ng-value="data" ng-repeat="data in keyCurveId">{{data}}</option> + </select> + <div ng-messages="add__csr__form.keyCurveId.$error" class="form-error" ng-class="{'visible' : add__csr__form.keyCurveId.$touched}"> + <p ng-message="required">Field is required</p> + </div> + </div> + + + <div ng-if="newCSR.keyPairAlgorithm =='RSA'"> + <label for="keyBitLength" class="add-csr__label">Key Bit Length *</label> + <select class="add-csr__select" ng-model="newCSR.keyBitLength" id="keyBitLength" name="keyBitLength" required> + <option class="courier-bold" ng-value="">Select an option</option> + <option class="courier-bold" ng-value="data" ng-repeat="data in keyBitLength">{{data}}</option> + </select> + <div ng-messages="add__csr__form.keyBitLength.$error" class="form-error" ng-class="{'visible' : add__csr__form.keyBitLength.$touched}"> + <p ng-message="required">Field is required</p> + </div> + </div> + + </div> + </fieldset> + </div> + </div> + <div class="modal__button-wrapper"> + <button class="btn btn-secondary" ng-click="resetCSRModal();add__csr__form.$setUntouched();">Cancel</button> + <button class="btn btn-primary" ng-click="csrSubmitted = true; getCSRCode();add__csr__form.$setUntouched();" ng-disabled="add__csr__form.$invalid">Generate CSR</button> + </div> + </div> + </form> + </section> + <div class="modal-overlay" tabindex="-1" ng-class="{'active': addCertificateModal || addCSRModal}"></div>
\ No newline at end of file diff --git a/app/access-control/controllers/certificate-controller.js b/app/access-control/controllers/certificate-controller.js new file mode 100644 index 0000000..2e6a92c --- /dev/null +++ b/app/access-control/controllers/certificate-controller.js @@ -0,0 +1,238 @@ +/** + * Controller for Certificate Management + * + * @module app/access-control + * @exports certificateController + * @name certificateController + */ + +window.angular && (function(angular) { + 'use strict'; + + angular.module('app.accessControl').controller('certificateController', [ + '$scope', 'APIUtils', '$q', 'Constants', 'toastService', + function($scope, APIUtils, $q, Constants, toastService) { + $scope.loading = false; + $scope.certificates = []; + $scope.availableCertificateTypes = []; + $scope.allCertificateTypes = Constants.CERTIFICATE_TYPES; + $scope.addCertificateModal = false; + $scope.addCSRModal = false; + $scope.newCertificate = {}; + $scope.newCSR = {}; + $scope.submitted = false; + $scope.csrSubmitted = false; + $scope.csrCode = ''; + $scope.displayCSRCode = false; + $scope.keyBitLength = Constants.CERTIFICATE.KEY_BIT_LENGTH; + $scope.keyPairAlgorithm = Constants.CERTIFICATE.KEY_PAIR_ALGORITHM; + $scope.keyCurveId = Constants.CERTIFICATE.KEY_CURVE_ID; + $scope.countryList = Constants.COUNTRIES; + + + $scope.$on('$viewContentLoaded', () => { + getBmcTime(); + }) + + $scope.loadCertificates = function() { + $scope.certificates = []; + $scope.availableCertificateTypes = Constants.CERTIFICATE_TYPES; + $scope.loading = true; + // Use Certificate Service to get the locations of all the certificates, + // then add a promise for fetching each certificate + APIUtils.getCertificateLocations().then( + function(data) { + var promises = []; + var locations = data.Links.Certificates; + for (var i in locations) { + var location = locations[i]; + promises.push(getCertificatePromise(location['@odata.id'])); + } + $q.all(promises) + .catch(function(error) { + toastService.error('Failed to load certificates.'); + console.log(JSON.stringify(error)); + }) + .finally(function() { + $scope.loading = false; + }); + }, + function(error) { + $scope.loading = false; + $scope.availableCertificateTypes = []; + toastService.error('Failed to load certificates.'); + console.log(JSON.stringify(error)); + }); + }; + + $scope.uploadCertificate = function() { + if ($scope.newCertificate.file.name.split('.').pop() !== 'pem') { + toastService.error('Certificate must be a .pem file.'); + return; + } + $scope.addCertificateModal = false; + APIUtils + .addNewCertificate( + $scope.newCertificate.file, $scope.newCertificate.selectedType) + .then( + function(data) { + toastService.success( + $scope.newCertificate.selectedType.name + + ' was uploaded.'); + $scope.newCertificate = {}; + $scope.loadCertificates(); + }, + function(error) { + toastService.error( + $scope.newCertificate.selectedType.name + + ' failed upload.'); + console.log(JSON.stringify(error)); + }); + }; + + var getCertificatePromise = function(url) { + var promise = APIUtils.getCertificate(url).then(function(data) { + var certificate = data; + isExpiring(certificate); + updateAvailableTypes(certificate); + $scope.certificates.push(certificate); + }); + return promise; + }; + + var isExpiring = function(certificate) { + // convert certificate time to epoch time + // if ValidNotAfter is less than or equal to 30 days from bmc time + // (2592000000), isExpiring. If less than or equal to 0, is expired. + // dividing bmc time by 1000 converts epoch milliseconds to seconds + var difference = (new Date(certificate.ValidNotAfter).getTime()) - + ($scope.bmcTime) / 1000; + if (difference <= 0) { + certificate.isExpired = true; + } else if (difference <= 2592000000) { + certificate.isExpiring = true; + } else { + certificate.isExpired = false; + certificate.isExpiring = false; + } + }; + + // add optional name + $scope.names = []; + $scope.addOptionalRow = function() { + $scope.names.push({Value: ''}) + }; + + // remove optional name row + $scope.deleteOptionalRow = function(index) { + $scope.names.splice(index, 1); + if ($scope.names.length == 0) { + $scope.names = []; + } + }; + + + // create a CSR object to send to the backend + $scope.getCSRCode = function() { + var addCSR = {}; + let alternativeNames = $scope.names.map(name => name.Value); + + // if user provided a first alternative name then push to alternative + // names array + $scope.newCSR.firstAlternativeName ? + alternativeNames.push($scope.newCSR.firstAlternativeName) : + $scope.newCSR.firstAlternativeName = ''; + + + addCSR.CertificateCollection = { + '@odata.id': $scope.newCSR.certificateCollection.location + }; + addCSR.CommonName = $scope.newCSR.commonName; + addCSR.ContactPerson = $scope.newCSR.contactPerson || ''; + addCSR.City = $scope.newCSR.city; + addCSR.AlternativeNames = alternativeNames || []; + addCSR.ChallengePassword = $scope.newCSR.challengePassword || ''; + addCSR.Email = $scope.newCSR.emailAddress || ''; + addCSR.Country = $scope.newCSR.countryCode.code; + addCSR.Organization = $scope.newCSR.organization; + addCSR.OrganizationalUnit = $scope.newCSR.companyUnit; + addCSR.KeyCurveId = $scope.newCSR.keyCurveId || ''; + addCSR.KeyBitLength = $scope.newCSR.keyBitLength + addCSR.KeyPairAlgorithm = $scope.newCSR.keyPairAlgorithm || ''; + addCSR.State = $scope.newCSR.state; + + APIUtils.createCSRCertificate(addCSR).then( + function(data) { + $scope.displayCSRCode = true; + $scope.csrCode = data; + }, + function(error) { + $scope.addCSRModal = false; + toastService.error('Unable to generate CSR. Try again.'); + console.log(JSON.stringify(error)); + }) + }; + + // resetting the modal when user clicks cancel/closes the + // modal + $scope.resetCSRModal = function() { + $scope.addCSRModal = false; + $scope.displayCSRCode = false; + $scope.newCSR.certificateCollection = $scope.selectOption; + $scope.newCSR.commonName = ''; + $scope.newCSR.contactPerson = ''; + $scope.newCSR.city = ''; + $scope.names = []; + $scope.newCSR.challengePassword = ''; + $scope.newCSR.emailAddress = ''; + $scope.newCSR.countryCode = ''; + $scope.newCSR.keyCurveId = ''; + $scope.newCSR.firstAlternativeName = ''; + $scope.newCSR.keyBitLength = $scope.selectOption; + $scope.newCSR.keyPairAlgorithm = $scope.selectOption; + $scope.newCSR.organization = ''; + $scope.newCSR.companyUnit = ''; + $scope.newCSR.state = ''; + }; + + // copies the CSR code + $scope.copySuccess = function(event) { + $scope.copied = true; + $timeout(function() { + $scope.copied = false; + }, 5000); + }; + $scope.copyFailed = function(err) { + console.log(JSON.stringify(err)); + }; + + + var getBmcTime = function() { + APIUtils.getBMCTime().then(function(data) { + $scope.bmcTime = data.data.Elapsed; + }); + + return $scope.bmcTime; + }; + + var updateAvailableTypes = function(certificate) { + // TODO: at this time only one of each type of certificate is allowed. + // When this changes, this will need to be updated. + // Removes certificate type from available types to be added. + $scope.availableCertificateTypes = + $scope.availableCertificateTypes.filter(function(type) { + return type.Description !== certificate.Description; + }); + }; + + $scope.getDays = function(endDate) { + // finds number of days until certificate expiration + // dividing bmc time by 1000 converts milliseconds to seconds + var ms = (new Date(endDate).getTime()) - ($scope.bmcTime) / 1000; + return Math.floor(ms / (24 * 60 * 60 * 1000)); + }; + + $scope.loadCertificates(); + } + ]); +})(angular); diff --git a/app/access-control/controllers/ldap-controller.html b/app/access-control/controllers/ldap-controller.html new file mode 100644 index 0000000..294dbb3 --- /dev/null +++ b/app/access-control/controllers/ldap-controller.html @@ -0,0 +1,148 @@ +<loader loading="loading"></loader> +<div class="ldap" id="configuration-ldap"> + <div class="row column"> + <h1>LDAP</h1> + </div> + <div class="row column"> + <p>Configure LDAP settings and manage role groups.</p> + </div> + <div class="row column"> + <h2 class="subhead"> + Settings + </h2> + </div> + <div class="row column"> + <label class="control-check ldap__control-check"> + <input type="checkbox" id="enable-ldap-checkbox" + ng-change="updateServiceEnabled(); ldap__configuration.$setUntouched()" + ng-model="ldapProperties.ServiceEnabled" /> + <span class="control__indicator"></span> + <span class="control__label"> + <strong>Enable LDAP authentication</strong> <br> + LDAP authentication must be enabled to modify role groups. + </span> + </label> + </div> + <div class="row column"> + <form id="ldap__configuration" name="ldap__configuration" ng-class="{'submitted': submitted}" + class="ldap__configuration" novalidate> + <fieldset ng-disabled="!ldapProperties.ServiceEnabled"> + <div class="ldap__configure-settings row column"> + <div class="large-3 column ldap__ssl-column"> + <label class="control-check" ng-class="{'disabled' : certificates.length < 1}"> + <input id="secure-ldap-ssl" type="checkbox" ng-model="ldapProperties.useSSL" + ng-checked="ldapProperties.useSSL" ng-disabled="certificates.length < 1" /> + <span class="control__indicator"></span> + <span class="control__label">Secure LDAP using SSL</span> + </label> + <div> + <div class="ldap__certificate-info" ng-if="ldapProperties.ServiceEnabled"> + <p>Client certificate valid until:</p> + <small> + {{clientCertificateExpires ? (clientCertificateExpires | localeDate) : 'none available'}}</small> + </div> + </div> + <div class="ldap__certificate-info" ng-if="data.ValidNotAfter='' || !ldapProperties.ServiceEnabled"> + <span>SSL certificates must be uploaded to secure LDAP using SSL.</span> + </div> + <div class="ldap__certificate-info"> + <a href="#/access-control/ssl-certificates">Go to SSL certificates</a> + </div> + </div> + <div class="large-9 columns ldap__server-info"> + <div class="column service-type-column"> + <fieldset class="ldap__server-info-service-type"> + <legend class="content-label">Service Type</legend> + <label class="control-radio control__radio__label" for="open-ldap">Open LDAP + <input type="radio" name="service_enabled_type" id="open-ldap" value="ldap" + ng-checked="ldapProperties.LDAPServiceEnabled" + ng-change="ldapProperties.EnabledServiceUpdated = true" ng-model="ldapProperties.EnabledServiceType" + required /> + <span class="control__indicator control__indicator-on control__indicator-service-type"></span> + </label> + <label class="control-radio control__radio__label" for="active-directory">Active directory + <input type="radio" name="service_enabled_type" id="active-directory" + ng-change="ldapProperties.EnabledServiceUpdated = true" value="ad" + ng-checked="ldapProperties.ADServiceEnabled" ng-model="ldapProperties.EnabledServiceType" + required /> + <span class="control__indicator control__indicator-on control__indicator-service-type"></span> + </label> + </fieldset> + </div> + <div class="medium-6 large-4 columns"> + <label for="ldap__uri">Server uri</label> + <input id="ldap__uri" name="ldap__uri" type="text" + ng-change="ldapProperties.ServiceAddressesUpdated = true" ng-model="ldapProperties.ServiceAddresses[0]" + required /> + <div ng-messages="ldap__configuration.ldap__uri.$error" class="form-error" + ng-class="{'visible' : ldap__configuration.ldap__uri.$touched || submitted}"> + <p ng-message="required">Field is required</p> + </div> + </div> + <div class="medium-6 large-4 columns"> + <label for="ldap__bind__dn">Bind DN</label> + <input id="ldap__bind__dn" name="ldap__bind__dn" type="text" + ng-change="ldapProperties.UsernameUpdated = true" ng-model="ldapProperties.Username" required /> + <div ng-messages="ldap__configuration.ldap__bind__dn.$error" class="form-error" + ng-class="{'visible' : ldap__configuration.ldap__bind__dn.$touched || submitted}"> + <p ng-message="required">Field is required</p> + </div> + </div> + <div class="medium-6 large-4 columns"> + <label for="ldap__bind_pw">Bind password</label> + <input id="ldap__bind_pw" type="{{showpassword ? 'text' : 'password'}}" name="ldap__bind_pw" + ng-change="ldapProperties.PasswordUpdated = true" autocomplete="off" ng-model="ldapProperties.Password" + required /> + <button ng-model="showpassword" ng-class="{'disabled' : !ldap__configuration.$valid}" + ng-click="togglePassword = !togglePassword; showpassword = !showpassword;" class="password-toggle"> + <span ng-hide="togglePassword">Show</span> + <span ng-show="togglePassword">Hide</span> + </button> + <div ng-messages="ldap__configuration.ldap__bind_pw.$error" class="form-error" + ng-class="{'visible' : ldap__configuration.ldap__bind_pw.$touched || submitted}"> + <p ng-message="required">Field is required</p> + </div> + </div> + <div class="medium-6 large-4 columns"> + <label for="ldap__base__dn">Base DN</label> + <input id="ldap__base__dn" name="ldap__base__dn" type="text" + ng-change="ldapProperties.BaseDistinguishedNamesUpdated = true" + ng-model="ldapProperties.BaseDistinguishedNames[0]" required /> + <div ng-messages="ldap__configuration.ldap__base__dn.$error" class="form-error" + ng-class="{'visible' : ldap__configuration.ldap__base__dn.$touched || submitted}"> + <p ng-message="required">Field is required</p> + </div> + </div> + <div class="medium-6 large-4 columns"> + <label for="ldap__user_attribute">User id attribute (optional)</label> + <input id="ldap__user_attribute" name="ldap__user_attribute" type="text" + ng-change="ldapProperties.UsernameAttributeUpdated = true" ng-model="ldapProperties.UsernameAttribute" + class="ldap__optional-field" /> + </div> + <div class="medium-6 large-4 columns"> + <label for="ldap__group_attribute">Group id attribute (optional)</label> + <input id="ldap__group_attribute" name="ldap__group_attribute" type="text" + ng-change="ldapProperties.GroupsAttributeUpdated = true" ng-model="ldapProperties.GroupsAttribute" + class="ldap__optional-field" /> + </div> + <div class="column ldap__configuration-buttons"> + <button type="button" class="btn btn-primary" ng-disabled="!ldap__configuration.$valid" + ng-click="$parent.submitted=true; ldap__configuration.$valid && saveLdapSettings(); ldap__configuration.$setUntouched()">Save</button> + <button type="button" class="btn btn-secondary" + ng-click="loadLdap(); ldap__configuration.$setUntouched()">Reset</button> + </div> + </fieldset> + </form> + </div> +</div> +<div class="ldap-groups row column"> + <h2 class="small-12 subhead"> + Role groups + </h2> + <div class="row column"> + <div class="small-12"> + <ldap-user-roles role-groups="roleGroups" role-group-type="roleGroupType" enabled="ldapProperties.ServiceEnabled"> + </ldap-user-roles> + </div> + </div> +</div>
\ No newline at end of file diff --git a/app/access-control/controllers/ldap-controller.js b/app/access-control/controllers/ldap-controller.js new file mode 100644 index 0000000..cfdab50 --- /dev/null +++ b/app/access-control/controllers/ldap-controller.js @@ -0,0 +1,224 @@ +/** + * Controller for LDAP + * + * @module app/access-control + * @exports ldapController + * @name ldapController + */ + +window.angular && (function(angular) { + 'use strict'; + + angular.module('app.accessControl').controller('ldapController', [ + '$scope', 'APIUtils', '$q', 'toastService', + function($scope, APIUtils, $q, toastService) { + $scope.loading = false; + $scope.isSecure = false; + $scope.ldapProperties = {}; + $scope.originalProperties = {}; + $scope.submitted = false; + $scope.roleGroups = []; + $scope.roleGroupType = ''; + $scope.clientCertificateExpires = ''; + + $scope.$on('$viewContentLoaded', function() { + $scope.loadLdap(); + }); + + $scope.loadLdap = function() { + $scope.loading = true; + $scope.submitted = false; + var getLdapProperties = + APIUtils.getAllUserAccountProperties() + .then(function(data) { + $scope.ldapProperties = { + 'ServiceEnabled': data.LDAP.ServiceEnabled ? + data.LDAP.ServiceEnabled : + data.ActiveDirectory.ServiceEnabled ? + data.ActiveDirectory.ServiceEnabled : + false, + 'LDAPServiceEnabled': data.LDAP.ServiceEnabled, + 'ADServiceEnabled': data.ActiveDirectory.ServiceEnabled, + 'EnabledServiceType': data.LDAP.ServiceEnabled ? + 'ldap' : + data.ActiveDirectory.ServiceEnabled ? 'ad' : '', + 'ServiceAddresses': data.LDAP.ServiceEnabled ? + data.LDAP.ServiceAddresses : + data.ActiveDirectory.ServiceEnabled ? + data.ActiveDirectory.ServiceAddresses : + [], + 'useSSL': $scope.isSSL( + data.LDAP.ServiceEnabled ? + data.LDAP.ServiceAddresses[0] : + data.ActiveDirectory.ServiceAddresses[0]), + 'Username': data.LDAP.ServiceEnabled ? + data.LDAP.Authentication.Username : + data.ActiveDirectory.ServiceEnabled ? + data.ActiveDirectory.Authentication.Username : + '', + 'BaseDistinguishedNames': data.LDAP.ServiceEnabled ? + data.LDAP.LDAPService.SearchSettings + .BaseDistinguishedNames : + data.ActiveDirectory.ServiceEnabled ? + data.ActiveDirectory.LDAPService.SearchSettings + .BaseDistinguishedNames : + [], + 'GroupsAttribute': data.LDAP.ServiceEnabled ? + data.LDAP.LDAPService.SearchSettings.GroupsAttribute : + data.ActiveDirectory.ServiceEnabled ? + data.ActiveDirectory.LDAPService.SearchSettings + .GroupsAttribute : + '', + 'UsernameAttribute': data.LDAP.ServiceEnabled ? + data.LDAP.LDAPService.SearchSettings.UsernameAttribute : + data.ActiveDirectory.ServiceEnabled ? + data.ActiveDirectory.LDAPService.SearchSettings + .UsernameAttribute : + '', + 'AuthenticationType': data.LDAP.ServiceEnabled ? + data.LDAP.Authentication.AuthenticationType : + data.ActiveDirectory.Authentication.AuthenticationType, + }; + + $scope.roleGroupType = + $scope.ldapProperties.EnabledServiceType; + + if ($scope.ldapProperties.ServiceEnabled) { + if ($scope.ldapProperties.LDAPServiceEnabled) { + $scope.roleGroups = data.LDAP.RemoteRoleMapping; + } else if ($scope.ldapProperties.ADServiceEnabled) { + $scope.roleGroups = + data.ActiveDirectory.RemoteRoleMapping; + } + } + }) + .catch(function(error) { + console.log(JSON.stringify(error)); + }); + var getClientCertificate = + APIUtils + .getCertificate('/redfish/v1/AccountService/LDAP/Certificates') + .then(function(data) { + if (data.Members) { + var certificate = data.Members[0]; + APIUtils.getCertificate(certificate['@odata.id']) + .then( + function(data) { + $scope.clientCertificateExpires = + data.ValidNotAfter; + }, + function(error) { + console.log(JSON.stringify(error)); + }) + } + }) + .catch(function(error) { + console.log(JSON.stringify(error)); + }); + + var promises = [getLdapProperties, getClientCertificate]; + $q.all(promises).finally(function() { + $scope.loading = false; + }); + }; + + $scope.saveLdapSettings = function() { + for (var i in $scope.ldapProperties.ServiceAddresses) { + if ($scope.ldapProperties.useSSL !== + $scope.isSSL($scope.ldapProperties.ServiceAddresses[i])) { + toastService.error( + 'Server URI ' + $scope.ldapProperties.ServiceAddresses[i] + + ' must begin with ' + + ($scope.ldapProperties.useSSL ? 'ldaps:// ' : 'ldap:// ') + + 'when SSL is ' + + ($scope.ldapProperties.useSSL ? 'configured. ' : + 'not configured.')); + } + } + + // Default LDAP and AD Attributes + let LDAP = {}; + + let ActiveDirectory = {}; + + // Data to pass to request + let data = {}; + data.LDAP = LDAP; + data.ActiveDirectory = ActiveDirectory; + + // Values to update the service type object + let Authentication = {}; + Authentication.Username = $scope.ldapProperties.Username; + Authentication.Password = $scope.ldapProperties.Password; + Authentication.AuthenticationType = + $scope.ldapProperties.AuthenticationType; + + let LDAPService = {}; + LDAPService.SearchSettings = {}; + LDAPService.SearchSettings.BaseDistinguishedNames = + $scope.ldapProperties.BaseDistinguishedNames; + LDAPService.SearchSettings.GroupsAttribute = + $scope.ldapProperties.GroupsAttribute; + LDAPService.SearchSettings.UsernameAttribute = + $scope.ldapProperties.UsernameAttribute; + + let ServiceAddresses = $scope.ldapProperties.ServiceAddresses; + if ($scope.ldapProperties.EnabledServiceType == 'ldap') { + ActiveDirectory.ServiceEnabled = false; + LDAP.ServiceEnabled = true; + LDAP.Authentication = Authentication; + LDAP.LDAPService = LDAPService; + LDAP.ServiceAddresses = ServiceAddresses; + } else if ($scope.ldapProperties.EnabledServiceType == 'ad') { + ActiveDirectory.ServiceEnabled = true; + LDAP.ServiceEnabled = false; + ActiveDirectory.Authentication = Authentication; + ActiveDirectory.LDAPService = LDAPService; + ActiveDirectory.ServiceAddresses = ServiceAddresses; + } + + APIUtils.saveLdapProperties(data).then( + function(response) { + if (!response.data.hasOwnProperty('error')) { + toastService.success('Successfully updated LDAP settings.'); + $scope.loadLdap(); + } else { + toastService.error('Unable to update LDAP settings.'); + console.log(JSON.stringify(response.data.error.message)); + } + }, + function(error) { + toastService.error('Unable to update LDAP settings.'); + console.log(JSON.stringify(error)); + }); + }; + + $scope.isSSL = function(uri) { + return uri.startsWith('ldaps://'); + }; + $scope.updateServiceEnabled = function() { + if (!$scope.ldapProperties.ServiceEnabled) { + $scope.ldapProperties.EnabledServiceType = ''; + let data = {}; + let LDAP = {}; + data.LDAP = LDAP; + LDAP.ServiceEnabled = false; + let ActiveDirectory = {}; + data.ActiveDirectory = ActiveDirectory; + ActiveDirectory.ServiceEnabled = false; + + APIUtils.saveLdapProperties(data).then( + function(response) { + toastService.success('Successfully disabled LDAP.'); + $scope.roleGroups = []; + $scope.loadLdap(); + }, + function(error) { + toastService.error('Unable to disable LDAP.'); + console.log(JSON.stringify(error)); + }); + } + } + } + ]); +})(angular); diff --git a/app/access-control/controllers/user-accounts-modal-remove.html b/app/access-control/controllers/user-accounts-modal-remove.html new file mode 100644 index 0000000..4a3efce --- /dev/null +++ b/app/access-control/controllers/user-accounts-modal-remove.html @@ -0,0 +1,22 @@ +<div class="uib-modal__content modal__local-users-remove"> + <div class="modal-header"> + <h2 class="modal-title" id="dialog_label"> + Remove user + </h2> + <button type="button" class="btn btn--close float-right" ng-click="$dismiss()" aria-label="Close"> + <icon file="icon-close.svg" aria-hidden="true"></icon> + </button> + </div> + <div class="modal-body"> + <p ng-if="modalCtrl.users.length > 1">Are you sure you want to remove {{modalCtrl.users.length}} users? This action cannot be undone.</p> + <p ng-if="modalCtrl.users.length === 1">Are you sure you want to remove user '{{modalCtrl.users[0].UserName}}'? This action cannot be undone.</p> + </div> + <div class="modal-footer"> + <button class="btn btn-secondary" ng-click="$dismiss()" type="button"> + Cancel + </button> + <button class="btn btn-primary" ng-click="$close()" type="button"> + Remove + </button> + </div> +</div> diff --git a/app/access-control/controllers/user-accounts-modal-settings.html b/app/access-control/controllers/user-accounts-modal-settings.html new file mode 100644 index 0000000..d48809f --- /dev/null +++ b/app/access-control/controllers/user-accounts-modal-settings.html @@ -0,0 +1,85 @@ +<div class="uib-modal__content modal__local-users-settings"> + <div class="modal-header"> + <h2 class="modal-title" id="dialog_label">Account policy settings</h2> + <button type="button" class="btn btn--close float-right" ng-click="$dismiss()" aria-label="Close"> + <icon file="icon-close.svg" aria-hidden="true"></icon> + </button> + </div> + <form name="form"> + <div class="modal-body"> + <div class="row"> + <div class="column medium-6"> + <!-- Max login attempts --> + <div class="field-group-container"> + <label for="maxLogin">Max failed login attempts</label> + <p class="label__helper-text">Value must be between <span class="nowrap">0 – 65535</span></p> + <input id="maxLogin" + name="maxLogin" + type="number" + required + min="0" + max="65535" + ng-model="modalCtrl.settings.maxLogin" /> + <div ng-if="form.maxLogin.$invalid && form.maxLogin.$dirty" class="form__validation-message"> + <span ng-show="form.maxLogin.$error.required"> + Field is required</span> + <span ng-show="form.maxLogin.$error.min || form.maxLogin.$error.max"> + Value must be between <span class="nowrap">1 - 65535</span></span> + </div> + </div> + </div> + <div class="column medium-6"> + <!-- User unlock method --> + <fieldset class="field-group-container"> + <legend>User unlock method</legend> + <!-- Automatic radio option --> + <label class="radio-label"> + <input name="lockoutMethod" + type="radio" + ng-value="1" + ng-model="modalCtrl.settings.lockoutMethod"> + Automatic after timeout + </label> + <!-- Automatic timeout value --> + <div class="field-group-container radio-option__input-field-group"> + <label for="lockoutMethod1">Timeout duration (seconds)</label> + <p class="label__helper-text" id="lockoutMethod1Helper">Must be at least 1</p> + <input id="lockoutMethod1" + name="timeoutDuration" + type="number" + aria-describedby="lockoutMethod1Helper" + ng-min="modalCtrl.settings.lockoutMethod ? 1 : null" + ng-disabled="!modalCtrl.settings.lockoutMethod" + ng-required="modalCtrl.settings.lockoutMethod" + ng-model="modalCtrl.settings.timeoutDuration"/> + <div ng-if="form.timeoutDuration.$invalid && form.timeoutDuration.$touched" class="form__validation-message"> + <span ng-show="form.timeoutDuration.$error.required"> + Field is required</span> + <span ng-show="form.timeoutDuration.$error.min"> + Value must be at least 1</span> + </div> + </div> + <!-- Manual radio option --> + <label class="radio-label"> + <input name="lockoutMethod" + type="radio" + ng-value="0" + ng-model="modalCtrl.settings.lockoutMethod"> + Manual + </label> + </fieldset> + </div> + </div> + </div> + <div class="modal-footer"> + <button class="btn btn-secondary" ng-click="$dismiss()" type="button">Cancel</button> + <button class="btn btn-primary" + type="submit" + ng-click="$close(form)" + ng-disabled="form.$invalid || form.$pristine" + ng-class="{'disabled': form.$invalid}"> + Save + </button> + </div> + </form> +</div> diff --git a/app/access-control/controllers/user-accounts-modal-user.html b/app/access-control/controllers/user-accounts-modal-user.html new file mode 100644 index 0000000..4e646b1 --- /dev/null +++ b/app/access-control/controllers/user-accounts-modal-user.html @@ -0,0 +1,174 @@ +<div class="uib-modal__content modal__local-users"> + <div class="modal-header"> + <h2 class="modal-title" id="dialog_label"> + {{ modalCtrl.user.new ? 'Add user' : 'Modify user' }} + </h2> + <button type="button" class="btn btn--close" ng-click="$dismiss()" aria-label="Close"> + <icon file="icon-close.svg" aria-hidden="true"></icon> + </button> + </div> + <form name="form"> + <div class="modal-body"> + <!-- Manual unlock --> + <div class="row" ng-if="modalCtrl.user.locked && !modalCtrl.automaticLockout"> + <div class="column medium-9"> + <div class="notification-banner" + aria-live="polite" + ng-class="{'notification-banner--warning': !form.lock.$dirty, + 'notification-banner--information': form.lock.$dirty}"> + <p class="notification-banner__text" ng-if="!form.lock.$dirty">Account locked</p> + <p class="notification-banner__text" ng-if="form.lock.$dirty">Click "Save" to unlock account</p> + </div> + </div> + <div class="column medium-3"> + <input + type="hidden" + name="lock" + ng-model="modalCtrl.manualUnlockProperty" + value="false"> + <button class="btn btn-primary" + type="button" + ng-click="form.lock.$setDirty()" + ng-disabled="form.lock.$dirty">Unlock</button> + </div> + </div> + <div class="row"> + <div class="column medium-6"> + <!-- Account Status --> + <fieldset class="field-group-container"> + <legend>Account Status</legend> + <label class="radio-label"> + <input type="radio" + name="accountStatus" + ng-value="true" + ng-model="modalCtrl.user.accountStatus" + ng-disabled="modalCtrl.user.isRoot"> + Enabled + </label> + <label class="radio-label"> + <input type="radio" + name="accountStatus1" + ng-value="false" + ng-model="modalCtrl.user.accountStatus" + ng-disabled="modalCtrl.user.isRoot"> + Disabled + </label> + </fieldset> + <!-- Username --> + <div class="field-group-container"> + <label for="username">Username</label> + <p class="label__helper-text">Cannot start with a number</p> + <p class="label__helper-text">No special characters except underscore</p> + <input id="username" + name="username" + type="text" + required + minlength="1" + maxlength="16" + ng-pattern="'^([a-zA-Z_][a-zA-Z0-9_]*)'" + ng-readonly="modalCtrl.user.isRoot" + ng-model="modalCtrl.user.username" + username-validator + existing-usernames="modalCtrl.existingUsernames"/> + <div ng-if="form.username.$invalid && form.username.$touched" class="form__validation-message"> + <span ng-show="form.username.$error.required"> + Field is required</span> + <span ng-show="form.username.$error.minlength || form.username.$error.maxlength"> + Length must be between <span class="nowrap">1 – 16</span> characters</span> + <span ng-show="form.username.$error.pattern"> + Invalid format</span> + <span ng-show="form.username.$error.duplicateUsername"> + Username already exists</span> + </div> + </div> + <!-- Privlege --> + <div class="field-group-container"> + <label for="privilege">Privilege</label> + <select id="privilege" + name="privilege" + required + ng-disabled="modalCtrl.user.isRoot" + ng-model="modalCtrl.user.privilege"> + <option ng-if="modalCtrl.user.new" + ng-selected="modalCtrl.user.new" + value="" + disabled> + Select an option + </option> + <option ng-value="role" + ng-repeat="role in modalCtrl.privilegeRoles"> + {{role}} + </option> + </select> + </div> + </div> + <div class="column medium-6"> + <!-- Password --> + <div class="field-group-container"> + <label for="password">User password</label> + <p class="label__helper-text">Password must between <span class="nowrap">{{modalCtrl.minPasswordLength}} – {{modalCtrl.maxPasswordLength}}</span> characters</p> + <input id="password" + name="password" + type="password" + ng-minlength="modalCtrl.minPasswordLength" + ng-maxlength="modalCtrl.maxPasswordLength" + autocomplete="new-password" + ng-required="modalCtrl.user.new || form.password.$touched || form.passwordConfirm.$touched" + ng-model="modalCtrl.user.password" + password-visibility-toggle + ng-click="form.password.$setTouched()" + placeholder="{{ + (modalCtrl.user.new || + form.password.$touched || + form.passwordConfirm.$touched) ? '' : '******'}}"/> + <div ng-if="form.password.$invalid && form.password.$dirty" class="form__validation-message"> + <span ng-show="form.password.$error.required"> + Field is required</span> + <span ng-show="form.password.$error.minlength || form.password.$error.maxlength"> + Length must be between <span class="nowrap">{{modalCtrl.minPasswordLength}} – {{modalCtrl.maxPasswordLength}}</span> characters</span> + </div> + </div> + <!-- Password confirm --> + <div class="field-group-container"> + <label for="passwordConfirm">Confirm user password</label> + <input id="passwordConfirm" + name="passwordConfirm" + type="password" + autocomplete="new-password" + ng-required="modalCtrl.user.new || form.password.$touched || form.passwordConfirm.$touched" + ng-model="modalCtrl.user.passwordConfirm" + password-visibility-toggle + password-confirm + first-password="form.password.$modelValue" + ng-click="form.passwordConfirm.$setTouched()" + placeholder="{{( + modalCtrl.user.new || + form.password.$touched || + form.passwordConfirm.$touched) ? '' : '******'}}"/> + <div ng-if="form.passwordConfirm.$invalid && form.passwordConfirm.$dirty" class="form__validation-message"> + <span ng-show="form.passwordConfirm.$error.required"> + Field is required</span> + <span ng-show="form.passwordConfirm.$error.passwordConfirm" + ng-hide="form.passwordConfirm.$error.required"> + Passwords do not match</span> + <span ng-show="form.passwordConfirm.$error.minlength || form.passwordConfirm.$error.maxlength"> + Length must be between <span class="nowrap">1 – 16</span> characters</span> + </div> + </div> + </div> + </div> + </div> + <div class="modal-footer"> + <button class="btn btn-secondary" ng-click="$dismiss()" type="button"> + Cancel + </button> + <button class="btn btn-primary" + type="submit" + ng-click="$close(form)" + ng-disabled="form.$invalid || form.$pristine" + ng-class="{'disabled': form.$invalid}"> + {{ modalCtrl.user.new ? 'Add user' : 'Save' }} + </button> + </div> + </form> +</div> diff --git a/app/access-control/controllers/user-controller.html b/app/access-control/controllers/user-controller.html new file mode 100644 index 0000000..31ba62d --- /dev/null +++ b/app/access-control/controllers/user-controller.html @@ -0,0 +1,43 @@ +<loader loading="loading"></loader> +<div class="page local-users"> + <div class="row column"> + <div class="column small-12"> + <h1 class="page-title">Local user management</h1> + </div> + </div> + <div class="row column"> + <div class="column small-12 medium-10"> + <div class="local-users__actions"> + <button ng-disabled="accountSettings === null" + ng-click="onClickAccountSettingsPolicy()" + class="btn btn-tertiary"> + <icon file="icon-config.svg"></icon> + Account policy settings + </button> + <button ng-disabled="userRoles === null || localUsers.length >= 15" + ng-click="onClickAddUser()" + class="btn btn-primary"> + <icon file="icon-plus.svg"></icon> + Add user + </button> + </div> + <!-- Local user table --> + <bmc-table + data="tableData" + header="tableHeader" + row-actions-enabled="true" + selectable="true" + batch-actions="tableBatchActions" + emit-row-action="onEmitRowAction(value)" + emit-batch-action="onEmitBatchAction(value)" + class="local-users__table"> + </bmc-table> + </div> + </div> + <div class="row column"> + <div class="column small-12 medium-9"> + <!-- Role table --> + <role-table></role-table> + </div> + </div> +</div>
\ No newline at end of file diff --git a/app/access-control/controllers/user-controller.js b/app/access-control/controllers/user-controller.js new file mode 100644 index 0000000..8ee8f88 --- /dev/null +++ b/app/access-control/controllers/user-controller.js @@ -0,0 +1,481 @@ +/** + * Controller for user Accounts + * + * @module app/access-control + * @exports userController + * @name userController + */ + +window.angular && (function(angular) { + 'use strict'; + + angular.module('app.accessControl').controller('userController', [ + '$scope', 'APIUtils', 'toastService', '$uibModal', '$q', + function($scope, APIUtils, toastService, $uibModal, $q) { + $scope.loading; + $scope.accountSettings; + $scope.userRoles; + $scope.localUsers; + + $scope.tableData = []; + $scope.tableHeader = [ + {label: 'Username'}, {label: 'Privilege'}, {label: 'Account status'} + ]; + $scope.tableBatchActions = [ + {type: 'delete', label: 'Remove'}, + {type: 'enable', label: 'Enable'}, + {type: 'disable', label: 'Disable'}, + ]; + + /** + * Returns true if username is 'root' + * @param {*} user + */ + function checkIfRoot(user) { + return user.UserName === 'root' ? true : false; + } + + /** + * Data table mapper + * @param {*} user + * @returns user + */ + function mapTableData(user) { + const accountStatus = + user.Locked ? 'Locked' : user.Enabled ? 'Enabled' : 'Disabled'; + const editAction = {type: 'Edit', enabled: true, file: 'icon-edit.svg'}; + const deleteAction = { + type: 'Delete', + enabled: checkIfRoot(user) ? false : true, + file: 'icon-trashcan.svg' + }; + user.selectable = checkIfRoot(user) ? false : true; + user.actions = [editAction, deleteAction]; + user.uiData = [user.UserName, user.RoleId, accountStatus]; + return user; + } + + /** + * Returns lockout method based on the lockout duration property + * If the lockoutDuration is greater than 0 the lockout method + * is automatic otherwise the lockout method is manual + * @param {number} lockoutDuration + * @returns {number} : returns the account lockout method + * 1(automatic) / 0(manual) + */ + function mapLockoutMethod(lockoutDuration) { + return lockoutDuration > 0 ? 1 : 0; + } + + /** + * API call to get all user accounts + */ + function getLocalUsers() { + $scope.loading = true; + APIUtils.getAllUserAccounts() + .then((users) => { + $scope.localUsers = users; + $scope.tableData = users.map(mapTableData); + }) + .catch((error) => { + console.log(JSON.stringify(error)); + toastService.error('Failed to load users.'); + }) + .finally(() => { + $scope.loading = false; + }) + } + + /** + * API call to get current Account settings + */ + function getAccountSettings() { + APIUtils.getAllUserAccountProperties() + .then((settings) => { + $scope.accountSettings = settings; + }) + .catch((error) => { + console.log(JSON.stringify(error)); + $scope.accountSettings = null; + }) + } + + /** + * API call to get local user roles + */ + function getUserRoles() { + APIUtils.getAccountServiceRoles() + .then((roles) => { + $scope.userRoles = roles; + }) + .catch((error) => { + console.log(JSON.stringify(error)); + $scope.userRoles = null; + }) + } + + /** + * API call to create new user + * @param {*} user + */ + function createUser(username, password, role, enabled) { + $scope.loading = true; + APIUtils.createUser(username, password, role, enabled) + .then(() => { + getLocalUsers(); + toastService.success(`User '${username}' has been created.`); + }) + .catch((error) => { + console.log(JSON.stringify(error)); + toastService.error(`Failed to create new user '${username}'.`); + }) + .finally(() => { + $scope.loading = false; + }); + } + + /** + * API call to update existing user + */ + function updateUser( + originalUsername, username, password, role, enabled, locked) { + $scope.loading = true; + APIUtils + .updateUser( + originalUsername, username, password, role, enabled, locked) + .then(() => { + getLocalUsers(); + toastService.success('User has been updated successfully.') + }) + .catch((error) => { + console.log(JSON.stringify(error)); + toastService.error(`Unable to update user '${originalUsername}'.`) + }) + .finally(() => { + $scope.loading = false; + }) + } + + /** + * API call to delete users + * @param {*} users : Array of users to delete + */ + function deleteUsers(users = []) { + $scope.loading = true; + const promises = + users.map((user) => APIUtils.deleteUser(user.UserName)); + $q.all(promises) + .then(() => { + let message; + if (users.length > 1) { + message = 'Users have been removed.' + } else { + message = `User '${users[0].UserName}' has been removed.` + } + toastService.success(message); + }) + .catch((error) => { + console.log(JSON.stringify(error)); + let message; + if (users.length > 1) { + message = 'Failed to remove users.' + } else { + message = `Failed to remove user '${users[0].UserName}'.` + } + toastService.error(message); + }) + .finally(() => { + getLocalUsers(); + $scope.loading = false; + }); + } + + /** + * API call to update user status enabled/disabled + * @param {*} users : Array of users to update + * @param {boolean} enabled : status + */ + function updateUserStatus(users = [], enabled = true) { + $scope.loading = true; + const promises = users.map( + (user) => APIUtils.updateUser( + user.UserName, null, null, null, enabled, null)); + $q.all(promises) + .then(() => { + let message; + let statusLabel = enabled ? 'enabled' : 'disabled'; + if (users.length > 1) { + message = `Users ${statusLabel}.` + } else { + message = `User '${users[0].UserName}' ${statusLabel}.`; + } + toastService.success(message); + }) + .catch((error) => { + console.log(JSON.stringify(error)); + let message; + let statusLabel = enabled ? 'enable' : 'disable'; + if (users.length > 1) { + message = `Failed to ${statusLabel} users.` + } else { + message = + `Failed to ${statusLabel} user '${users[0].UserName}'.` + } + toastService.error(message); + }) + .finally(() => { + getLocalUsers(); + $scope.loading = false; + }); + } + + /** + * API call to save account policy settings + * @param {number} lockoutDuration + * @param {number} lockoutThreshold + */ + function updateAccountSettings(lockoutDuration, lockoutThreshold) { + $scope.loading = true; + APIUtils.saveUserAccountProperties(lockoutDuration, lockoutThreshold) + .then(() => { + $scope.accountSettings['AccountLockoutDuration'] = + lockoutDuration; + $scope.accountSettings['AccountLockoutThreshold'] = + lockoutThreshold; + toastService.success( + 'Account policy settings have been updated.'); + }) + .catch((error) => { + console.log(JSON.stringify(error)); + toastService.error('Failed to update account policy settings.'); + }) + .finally(() => { + $scope.loading = false; + }); + } + + /** + * Initiate account settings modal + */ + function initAccountSettingsModal() { + const template = require('./user-accounts-modal-settings.html'); + $uibModal + .open({ + template, + windowTopClass: 'uib-modal', + ariaLabelledBy: 'dialog_label', + controllerAs: 'modalCtrl', + controller: function() { + const lockoutMethod = mapLockoutMethod( + $scope.accountSettings.AccountLockoutDuration); + + this.settings = {}; + this.settings.maxLogin = + $scope.accountSettings.AccountLockoutThreshold; + this.settings.lockoutMethod = lockoutMethod; + this.settings.timeoutDuration = !lockoutMethod ? + null : + $scope.accountSettings.AccountLockoutDuration; + } + }) + .result + .then((form) => { + if (form.$valid) { + const lockoutDuration = form.lockoutMethod.$modelValue ? + form.timeoutDuration.$modelValue : + 0; + const lockoutThreshold = form.maxLogin.$modelValue; + updateAccountSettings(lockoutDuration, lockoutThreshold); + } + }) + .catch( + () => { + // do nothing + }) + } + + /** + * Initiate user modal + * Can be triggered by clicking edit in table or 'Add user' button + * If triggered from the table, user parameter will be provided + * If triggered by add user button, user parameter will be undefined + * @optional @param {*} user + */ + function initUserModal(user) { + if ($scope.userRoles === null || $scope.userRoles === undefined) { + // If userRoles failed to load, do not allow add/edit + // functionality + return; + } + const newUser = user ? false : true; + const originalUsername = user ? angular.copy(user.UserName) : null; + const template = require('./user-accounts-modal-user.html'); + $uibModal + .open({ + template, + windowTopClass: 'uib-modal', + ariaLabelledBy: 'dialog_label', + controllerAs: 'modalCtrl', + controller: function() { + // Set default status to Enabled + const status = newUser ? true : user.Enabled; + // Check if UserName is root + // Some form controls will be disabled for root users: + // edit enabled status, edit username, edit role + const isRoot = + newUser ? false : checkIfRoot(user) ? true : false; + // Array of existing usernames (excluding current user instance) + const existingUsernames = + $scope.localUsers.reduce((acc, val) => { + if (user && (val.UserName === user.UserName)) { + return acc; + } + acc.push(val.UserName); + return acc; + }, []); + + this.user = {}; + this.user.isRoot = isRoot; + this.user.new = newUser; + this.user.accountStatus = status; + this.user.username = newUser ? '' : user.UserName; + this.user.privilege = newUser ? '' : user.RoleId; + this.user.locked = newUser ? null : user.Locked; + + this.manualUnlockProperty = false; + this.automaticLockout = mapLockoutMethod( + $scope.accountSettings.AccountLockoutDuration); + this.privilegeRoles = $scope.userRoles; + this.existingUsernames = existingUsernames; + this.minPasswordLength = $scope.accountSettings ? + $scope.accountSettings.MinPasswordLength : + null; + this.maxPasswordLength = $scope.accountSettings ? + $scope.accountSettings.MaxPasswordLength : + null; + } + }) + .result + .then((form) => { + if (form.$valid) { + // If form control is pristine set property to null + // this will make sure only changed values are updated when + // modifying existing users + // API utils checks for null values + const username = + form.username.$pristine ? null : form.username.$modelValue; + const password = + form.password.$pristine ? null : form.password.$modelValue; + const role = form.privilege.$pristine ? + null : + form.privilege.$modelValue; + const enabled = (form.accountStatus.$pristine && + form.accountStatus1.$pristine) ? + null : + form.accountStatus.$modelValue; + const locked = (form.lock && form.lock.$dirty) ? + form.lock.$modelValue : + null; + + if (!newUser) { + updateUser( + originalUsername, username, password, role, enabled, + locked); + } else { + createUser( + username, password, role, form.accountStatus.$modelValue); + } + } + }) + .catch( + () => { + // do nothing + }) + } + + /** + * Intiate remove users modal + * @param {*} users + */ + function initRemoveModal(users) { + const template = require('./user-accounts-modal-remove.html'); + $uibModal + .open({ + template, + windowTopClass: 'uib-modal', + ariaLabelledBy: 'dialog_label', + controllerAs: 'modalCtrl', + controller: function() { + this.users = users; + } + }) + .result + .then(() => { + deleteUsers(users); + }) + .catch( + () => { + // do nothing + }) + } + + /** + * Callback when action emitted from table + * @param {*} value + */ + $scope.onEmitRowAction = (value) => { + switch (value.action) { + case 'Edit': + initUserModal(value.row); + break; + case 'Delete': + initRemoveModal([value.row]); + break; + default: + } + }; + + /** + * Callback when batch action emitted from table + */ + $scope.onEmitBatchAction = (value) => { + switch (value.action) { + case 'delete': + initRemoveModal(value.filteredRows); + break; + case 'enable': + updateUserStatus(value.filteredRows, true) + break; + case 'disable': + updateUserStatus(value.filteredRows, false) + break; + default: + break; + } + }; + + /** + * Callback when 'Account settings policy' button clicked + */ + $scope.onClickAccountSettingsPolicy = () => { + initAccountSettingsModal(); + }; + + /** + * Callback when 'Add user' button clicked + */ + $scope.onClickAddUser = () => { + initUserModal(); + }; + + /** + * Callback when controller view initially loaded + */ + $scope.$on('$viewContentLoaded', () => { + getLocalUsers(); + getUserRoles(); + getAccountSettings(); + }) + } + ]); +})(angular); diff --git a/app/access-control/directives/role-table.html b/app/access-control/directives/role-table.html new file mode 100644 index 0000000..95b4c31 --- /dev/null +++ b/app/access-control/directives/role-table.html @@ -0,0 +1,15 @@ +<div class="role-table"> + <button class="btn btn-tertiary accordion-trigger" + ng-click="roleTableCtrl.onClick()" + ng-class="{'accordion-trigger--expanded' : !roleTableCtrl.isCollapsed}"> + <icon file="icon-chevron-right.svg" aria-hidden="true"></icon> + <span ng-if="roleTableCtrl.isCollapsed">View privilege role descriptions</span> + <span ng-if="!roleTableCtrl.isCollapsed">Hide privilege role descriptions</span> + </button> + <div uib-collapse="roleTableCtrl.isCollapsed"> + <bmc-table data="roleTableCtrl.tableData" + header="roleTableCtrl.tableHeader" + size="'small'"> + </bmc-table> + </div> +</div>
\ No newline at end of file diff --git a/app/access-control/directives/role-table.js b/app/access-control/directives/role-table.js new file mode 100644 index 0000000..0a3169f --- /dev/null +++ b/app/access-control/directives/role-table.js @@ -0,0 +1,68 @@ +window.angular && (function(angular) { + 'use strict'; + + /** + * Role table + * Table of privilege role descriptions + */ + angular.module('app.accessControl').directive('roleTable', [ + '$sce', + function($sce) { + return { + restrict: 'E', + template: require('./role-table.html'), + controllerAs: 'roleTableCtrl', + controller: function() { + // TODO: This is a workaround to render the checkmark svg icon + // Would eventually like to enhance <bmc-table> component to + // compile custom directives as table items + const svg = require('../../assets/icons/icon-check.svg'); + const check = + $sce.trustAsHtml(`<span class="icon__check-mark">${svg}<span>`); + + this.tableHeader = [ + {label: ''}, {label: 'Admin'}, {label: 'Operator'}, {label: 'User'}, + {label: 'Callback'} + ]; + + // TODO: When API changed from D-Bus to Redfish, 'Operator' role + // should have 'Configure components managed by this service' + // privilege checked + // TODO: When 'Operator' and 'User' roles have ability to change + // own account's passwords, should have 'Update password for + // current user account' privilege checked + this.tableData = [ + { + uiData: [ + 'Configure components managed by this service', check, '', '', + '' + ] + }, + {uiData: ['Configure manager resources', check, '', '', '']}, + { + uiData: [ + 'Update password for current user account', check, '', '', '' + ] + }, + {uiData: ['Configure users and their accounts', check, '', '', '']}, + { + uiData: [ + 'Log in to the service and read resources', check, check, check, + '' + ] + }, + {uiData: ['IPMI access point', check, check, check, check]}, + {uiData: ['Redfish access point', check, check, check, '']}, + {uiData: ['SSH access point', check, check, check, '']}, + {uiData: ['WebUI access point', check, check, check, '']}, + ]; + + this.isCollapsed = true; + this.onClick = () => { + this.isCollapsed = !this.isCollapsed; + }; + } + }; + } + ]); +})(window.angular); diff --git a/app/access-control/directives/username-validator.js b/app/access-control/directives/username-validator.js new file mode 100644 index 0000000..395e635 --- /dev/null +++ b/app/access-control/directives/username-validator.js @@ -0,0 +1,39 @@ +window.angular && (function(angular) { + 'use strict'; + + /** + * Username validator + * + * Checks if entered username is a duplicate + * Provide existingUsernames scope that should be an array of + * existing usernames + * + * <input username-validator existing-usernames="[]"/> + * + */ + angular.module('app.accessControl') + .directive('usernameValidator', function() { + return { + restrict: 'A', require: 'ngModel', scope: {existingUsernames: '='}, + link: function(scope, element, attrs, controller) { + if (scope.existingUsernames === undefined) { + return; + } + controller.$validators.duplicateUsername = + (modelValue, viewValue) => { + const enteredUsername = modelValue || viewValue; + const matchedExisting = scope.existingUsernames.find( + (username) => username === enteredUsername); + if (matchedExisting) { + return false; + } else { + return true; + } + }; + element.on('blur', () => { + controller.$validate(); + }); + } + } + }); +})(window.angular); diff --git a/app/access-control/index.js b/app/access-control/index.js new file mode 100644 index 0000000..45668ed --- /dev/null +++ b/app/access-control/index.js @@ -0,0 +1,41 @@ +/** + * A module for the access control + * + * @module app/access-control/index + * @exports app/access-control/index + */ + +window.angular && (function(angular) { + 'use strict'; + + angular + .module('app.accessControl', ['ngRoute', 'app.common.services']) + // Route access-control + .config([ + '$routeProvider', + function($routeProvider) { + $routeProvider + .when('/access-control', { + 'template': require('./controllers/ldap-controller.html'), + 'controller': 'ldapController', + authenticated: true + }) + .when('/access-control/ldap', { + 'template': require('./controllers/ldap-controller.html'), + 'controller': 'ldapController', + authenticated: true + }) + .when('/access-control/local-users', { + 'template': require('./controllers/user-controller.html'), + 'controller': 'userController', + authenticated: true + }) + .when('/access-control/ssl-certificates', { + 'template': + require('./controllers/certificate-controller.html'), + 'controller': 'certificateController', + authenticated: true + }); + } + ]); +})(window.angular); diff --git a/app/access-control/styles/certificate.scss b/app/access-control/styles/certificate.scss new file mode 100644 index 0000000..a7c57f2 --- /dev/null +++ b/app/access-control/styles/certificate.scss @@ -0,0 +1,250 @@ +.certificate__table { + border-left: 1px solid $border-color-01; + border-right: 1px solid $border-color-01; + margin-top: 0.5em; + .table__row-header { + width: 100%; + border-bottom: 1px solid $border-color-01; + background-color: $primary-dark; + color: $primary-light; + font-weight: 700; + } + .table__row-value { + width: 100%; + border-bottom: 1px solid $border-color-01; + } + .certificate__type-header { + @include mediaQuery(small) { + width: 20%; + background: transparent; + } + width: 100%; + padding: 0.8em; + padding-left: 1.5em; + } + .certificate__issue-header { + display: none; + padding: 0.8em; + @include mediaQuery(small) { + width: 20%; + display: block; + } + } + .certificate__date-header { + padding: 0.8em; + @include mediaQuery(small) { + width: 12%; + display: block; + } + display: none; + } + .certificate__status-header { + padding: 0.8em; + @include mediaQuery(small) { + width: 5%; + display: block; + } + display: none; + } + .certificate__type-cell { + width: 100%; + padding: 0.8em 0.8em 0.8em 1.5em; + word-wrap: break-word; + background: $background-02; + @include mediaQuery(small) { + width: 20%; + background: transparent; + } + } + .certificate__issue-cell { + padding: 0.8em; + word-wrap: break-word; + @include mediaQuery(small) { + width: 20%; + } + width: 70%; + } + .certificate__date-cell { + width: 70%; + padding: 0.8em; + word-wrap: break-word; + @include mediaQuery(small) { + width: 12%; + } + } + .certificate__status-cell { + padding: 0.8em; + @include mediaQuery(small) { + display: block; + width: 5%; + } + display: none; + } + .certificate__status-icon { + width: 1.2em; + svg { + margin-bottom: .2em; + } + } + .certificate__buttons-cell { + @include mediaQuery(small) { + width: 10%; + padding-top: 0.5em; + } + width: 100%; + text-align: right; + padding: 0 1.5em 0 0; + } + .certificate__title-inline { + @include mediaQuery(small) { + display: none; + } + width: 30%; + display: block; + padding: 0.8em 0.8em 0.8em 1.5em; + } + .upload__certificate { + border-top: 1px solid $border-color-01; + width: 100%; + background: $background-02; + padding: 0.8em; + } +} +.certificate__upload-chooser { + background: $background-02; +} + + +.certificate__close-modal { + float: right; + position: relative; + bottom: 1rem; + left: 2rem; +} +.certificate__table__icon { + margin-left: 1.5em; + margin-bottom: .4em; +} + +.add__certificate__modal { + select { + margin-bottom: 0; + } + .file__upload { + margin-bottom: 2em; + } + .select__new-label { + margin-bottom: 1em; + } + .select__new-button { + font-size: 1.2em; + } + .file__name { + background-color: $background-02; + padding: 0.5em; + } +} + +.add-certificate__section { + padding-left: 0; +} + +// Combinator needed to match specificity +// of default modal settings +.modal.add-csr__modal { + width: 100%; + max-height: 100vh; + overflow-y: auto; + z-index: 1001; +} + +.add-csr__section:first-of-type { + padding-left: 0; +} + +.add-csr__section:last-of-type { + margin-top: 2rem; + padding-right: 0; + + @media (min-width: 640px) { + margin-top: 0; + } +} + +.add-csr__section-title { + margin-bottom: 1rem; + font-weight: 700; +} + +.add-csr__section--border { + @media (min-width: 640px) { + padding-left: 2rem; + border-left: 1px solid $base-02--04; + } +} + +.add-csr__label { + white-space: nowrap; + display: inline-block; +} + +.add-csr__text-helper { + color: $base-02--02; + font-weight: 400; + font-size: 14px; + line-height: 1.2; + margin-bottom: .5em; +} + +input.add-csr__input, +select.add-csr__select { + width: 100%; + margin-bottom: 0; + max-height: none; + height: auto; +} + +.select.add-csr__select { + line-height: 1.15; +} + +input.add-csr__input-no-validation { + margin-bottom: 1.7rem; +} + +.add-csr__additional-alt-names { + display: flex; +} + +.add-csr__alt-name-add-btn { + padding: 0; + @media (min-width: 640px) { + margin: 1.9rem 0; + } +} + +.add-csr__alt-name-delete-btn { + width: 20px; + height: 30px; + padding: 0; + + icon { + margin-right: 0; + } +} + +.add-csr-code__header { + margin-top: 1em; +} + +.add-csr__container-csr-code { + white-space: pre-wrap; +} + +.add-csr__text-download { + color: $base-02--08; +} + +select.add-csr__multiselect { + height: 14rem; +} diff --git a/app/access-control/styles/index.scss b/app/access-control/styles/index.scss new file mode 100644 index 0000000..dff0b5d --- /dev/null +++ b/app/access-control/styles/index.scss @@ -0,0 +1,3 @@ +@import "./user-accounts.scss"; +@import "./certificate.scss"; +@import "./ldap.scss"; diff --git a/app/access-control/styles/ldap.scss b/app/access-control/styles/ldap.scss new file mode 100644 index 0000000..a18ac70 --- /dev/null +++ b/app/access-control/styles/ldap.scss @@ -0,0 +1,269 @@ +// LDAP SCSS + +.ldap__optional-field { + margin-bottom: 1.7em; +} + +.ldap__configure-settings { + background-color: $base-02--06; + padding-top: 1.5em; + padding-bottom: 1.5em; + margin-top: 1em; + margin-bottom: 3em; +} + +.ldap__server-info { + @media (min-width: 1024px) { + border-left: 1px solid $border-color-01; + } + + .control-radio { + margin-bottom: 6px; + display: block; + } + + .service-type-column { + margin-bottom: 1.2em; + } +} + +.ldap__ssl-column { + padding-left: 1.5em; + .control__label { + text-transform: none; + font-weight: 400; + font-size: 16px; + color: $primary-dark; + } + .control__indicator { + top: 5px; + } +} + +.ldap__configuration-buttons { + margin-top: 1rem; + + @media (min-width: 1024px) { + margin-top: 0; + } + + .btn { + float: right; + margin-left: 0.5em; + margin-top: 0.5em; + } + + .btn-secondary { + background-color: $primary-light; + } + + .btn-secondary:disabled { + color: $base-02--03; + border-color: $border-color-02; + } +} + +.ldap__server-info-service-type { + .content-label { + margin-bottom: 1rem; + } +} + +.ldap__certificate-info { + padding-top: 0.5em; + small { + font-size: 14px; + } + p { + color: $base-02--02; + text-transform: uppercase; + font-weight: 700; + font-size: 0.75em; + margin-bottom: 0; + } +} + +.control__radio__label { + padding: 0.2em 1em 0 2em; + text-transform: none; + font-weight: 400; + font-size: 16px; + color: $primary-dark; +} + +.ldap__control-check { + text-transform: none; + font-weight: 400; + font-size: 16px; + color: $primary-dark; + + .control__indicator { + top: 11px; + } + + .control__label { + margin-left: 30px; + } +} + +.control-radio .control__indicator-service-type { + width: 20px; + height: 20px; +} + +.control-radio .control__indicator-service-type:after { + top: 3px; + left: 3px; + width: 10px; + height: 10px; +} + +.control-radio input:disabled ~ .control__indicator-service-type:after { + top: 0; + left: 0; + width: 20px; + height: 20px; +} + +.password-toggle { + color: $base-01--03; + font-size: 0.8em; + float: right; + position: relative; + z-index: 2; + padding: 6px 0 0 0; +} + +.password-toggle.disabled { + background: transparent; + color: $base-02--03; + border: none; +} + +.ldap-groups { + .ldap__table { + border-left: 1px solid $border-color-01; + border-right: 1px solid $border-color-01; + + .empty__logs { + margin-top: 0; + } + + .table__row-header { + width: 100%; + border-bottom: 1px solid $border-color-01; + background-color: $primary-dark; + color: $primary-light; + font-weight: 700; + padding: 0; + } + + .table__row-header.disabled { + opacity: 0.8; + } + + .table__row-value { + width: 100%; + border-bottom: 1px solid $border-color-01; + } + + .table__cell-ldap { + width: 30%; + padding: 1.3em 1.5em 0.8em 1.5em; + } + + .table__cell-select { + width: 8%; + padding: 1.3em 1.5em 0.8em 1.5em; + .select-header { + padding-top: 1em; + } + } + + .table__cell-sort { + padding: 0.4em 1em 0 0; + margin: 0 -25px 0 -13px; + } + + .table__cell-ldap-group { + width: 29%; + padding: 1.2em 0.5em 0.8em 0.75em; + } + + .table__cell-ldap-role { + width: 30%; + padding: 1.2em 0.5em 0.8em 0.75em; + } + + .table__cell-buttons { + width: 32%; + text-align: right; + padding: 0.8em; + .btn { + padding-left: 0; + padding-right: 0; + } + } + } + + .btn-add-group, + .btn-remove-group { + color: $base-01--03; + padding: 0.75em 0; + } + + .modal__content-ldap { + margin-bottom: 2em; + margin-top: 2em; + input[type="text"] { + max-height: 2.4em; + } + select { + margin: 0 0 0; + } + } + + .edit-group-name { + padding-bottom: 1em; + } + + .form-actions { + width: 100%; + padding-top: 2em; + margin-top: 1.5em; + border-top: 1px solid $border-color-01; + button { + display: block; + float: right; + margin: 0 0 0 1em; + } + } + + .sort-ascending, + .sort-descending { + display: block; + padding: 0; + color: $primary-light; + font-size: 1em; + transform: rotate(-90deg); + + &:hover { + color: $primary-accent; + } + + &:after { + content: "\276F"; + } + + &:focus { + outline: 0; + color: $primary-accent; + } + } + + .sort-descending { + &:after { + content: "\276e"; + } + } +} diff --git a/app/access-control/styles/user-accounts.scss b/app/access-control/styles/user-accounts.scss new file mode 100644 index 0000000..fa0c5d7 --- /dev/null +++ b/app/access-control/styles/user-accounts.scss @@ -0,0 +1,55 @@ +.local-users { + margin-bottom: 50px; +} + +.local-users__actions { + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.modal__local-users, +.modal__local-users-settings { + .modal-body { + padding-left: 0; + padding-right: 0; + } +} + +.modal__local-users { + input[type="password"] { + &::placeholder { + color: $primary-dark; + font-weight: bold; + } + &::-ms-placeholder { + color: $primary-dark; + font-weight: bold; + } + } +} + +.role-table { + margin-top: 30px; + .bmc-table__cell:not(:first-of-type) { + text-align: center; + } + .bmc-table__column-header { + text-align: center; + } + + // Bootstrap collapse directive override + // The expanded element gets 'in' class instead of 'show' class + // Bootstrap changes the display property for 'show' but not 'in' + .collapse.in { + display: block!important; + } +} + +.icon__check-mark { + display: inline-block; + svg { + width: 16px; + fill: $primary-dark; + } +}
\ No newline at end of file |

