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