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
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