diff options
author | AppaRao Puli <apparao.puli@linux.intel.com> | 2018-10-17 16:07:55 +0530 |
---|---|---|
committer | Ed Tanous <ed.tanous@intel.com> | 2019-01-02 01:12:37 +0000 |
commit | 28711a6ad2cff8277bfa537c3b140caf690ab575 (patch) | |
tree | c14285dbdb00d511f4b10909955513b7925c2912 | |
parent | 0f2f981e3218a57f89995aa6cb6b684b2ec0ba8f (diff) | |
download | phosphor-webui-28711a6ad2cff8277bfa537c3b140caf690ab575.tar.gz phosphor-webui-28711a6ad2cff8277bfa537c3b140caf690ab575.zip |
WebUI: User management full implementation.
Added webui user accounts management. This support
both redfish and rest based backend API calls
depending on redfishSupportEnabled flag.
It does following actions:
- View all user list and there properties like
name, privilege, enabled state, Locked etc..
- Create new user account.
- Delete existing user account.
- Update the existing user properties like
password, privilege, enabled state.
Unit Test:
- Viewed all user information is proper or not.
- Created new user and validated.
- Deleted specific user and checked.
- Modified user info and validated the change.
All tests are done by enabling and disabling
redfishSupportEnabled flag using conifg.json.
Change-Id: Ifecf63844dc42c44771509958bf75947a92997ac
Signed-off-by: AppaRao Puli <apparao.puli@linux.intel.com>
-rw-r--r-- | app/common/directives/app-navigation.html | 2 | ||||
-rw-r--r-- | app/common/services/api-utils.js | 284 | ||||
-rw-r--r-- | app/users/controllers/user-accounts-controller.html | 103 | ||||
-rw-r--r-- | app/users/controllers/user-accounts-controller.js | 180 | ||||
-rw-r--r-- | app/users/styles/index.scss | 2 | ||||
-rw-r--r-- | app/users/styles/user-accounts.scss | 26 |
6 files changed, 511 insertions, 86 deletions
diff --git a/app/common/directives/app-navigation.html b/app/common/directives/app-navigation.html index 877f030..2f3ded7 100644 --- a/app/common/directives/app-navigation.html +++ b/app/common/directives/app-navigation.html @@ -98,6 +98,6 @@ </ul> <ul class="nav__second-level btn-users" ng-style="navStyle" ng-class="{opened: (showSubMenu && firstLevel == 'users')}"> <li ng-class="{'active': (path == '/users' || path == '/users/manage-accounts')}"> - <a href="#/users/manage-accounts" tabindex="18" ng-click="closeSubnav()">Manage user account</a></li> + <a href="#/users/manage-accounts" tabindex="18" ng-click="closeSubnav()">Manage user accounts</a></li> </ul> </nav> diff --git a/app/common/services/api-utils.js b/app/common/services/api-utils.js index 31a2958..beb63ba 100644 --- a/app/common/services/api-utils.js +++ b/app/common/services/api-utils.js @@ -475,19 +475,6 @@ window.angular && (function(angular) { console.log(error); }); }, - testPassword: function(username, password) { - // Calls /login without the current session to verify the given - // password is correct ignore the interceptor logout on a bad password - return $http({ - method: 'POST', - url: DataService.getHost() + '/login', - withCredentials: false, - data: JSON.stringify({'data': [username, password]}) - }) - .then(function(response) { - return response.data; - }); - }, logout: function(callback) { $http({ method: 'POST', @@ -509,7 +496,7 @@ window.angular && (function(angular) { }); }, changePassword: function(user, newPassword) { - if (DataService.configJson.redfishSupportEnabled) { + if (DataService.configJson.redfishSupportEnabled == true) { return $http({ method: 'PATCH', url: DataService.getHost() + @@ -539,6 +526,272 @@ window.angular && (function(angular) { return deferred.promise; } }, + getAllUserAccounts: function(members) { + var deferred = $q.defer(); + var promises = []; + var users = []; + + if (DataService.configJson.redfishSupportEnabled == true) { + $http({ + method: 'GET', + url: + DataService.getHost() + '/redfish/v1/AccountService/Accounts', + withCredentials: true + }) + .then( + function(response) { + var members = response.data['Members']; + angular.forEach(members, function(member) { + promises.push( + $http({ + method: 'GET', + url: DataService.getHost() + member['@odata.id'], + withCredentials: true + }).then(function(res) { + return res.data; + })); + }); + + $q.all(promises).then( + function(results) { + deferred.resolve(results); + }, + function(errors) { + deferred.reject(errors); + }); + }, + function(error) { + console.log(error); + deferred.reject(error); + }); + } else { + $http({ + method: 'GET', + url: + DataService.getHost() + '/xyz/openbmc_project/user/enumerate', + withCredentials: true + }) + .then( + function(response) { + var json = JSON.stringify(response.data); + var content = JSON.parse(json); + + function convertPrivToRoleId(priv) { + if (priv == 'priv-admin') { + return 'Administrator'; + } else if (priv == 'priv-user') { + return 'User'; + } else if (priv == 'priv-operator') { + return 'Operator'; + } else if (priv == 'priv-callback') { + return 'Callback'; + } + return ''; + } + + for (var key in content.data) { + var username = key.split('/').pop(); + if (content.data.hasOwnProperty(key) && + content.data[key].hasOwnProperty('UserPrivilege')) { + var val = content.data[key]; + users.push(Object.assign({ + Id: username, + UserName: username, + Locked: val['UserLockedForFailedAttempt'], + RoleId: convertPrivToRoleId(val['UserPrivilege']), + Enabled: val['UserEnabled'], + Password: null + })); + } + } + deferred.resolve(users); + }, + function(error) { + console.log(error); + deferred.reject(error); + }); + } + return deferred.promise; + }, + createUser: function(user, passwd, role, enabled) { + if (DataService.configJson.redfishSupportEnabled == true) { + var data = {}; + data['UserName'] = user; + data['Password'] = passwd; + data['RoleId'] = role; + data['Enabled'] = enabled; + + return $http({ + method: 'POST', + url: + DataService.getHost() + '/redfish/v1/AccountService/Accounts', + withCredentials: true, + data: data + }); + } else { + function convertRoleIdToPriv(roleId) { + if (roleId == 'Administrator') { + return 'priv-admin'; + } else if (roleId == 'User') { + return 'priv-user'; + } else if (roleId == 'Operator') { + return 'priv-operator'; + } else if (roleId == 'Callback') { + return 'priv-callback'; + } + return ''; + } + function setUserPassword(user, passwd) { + return $http({ + method: 'POST', + url: DataService.getHost() + + '/xyz/openbmc_project/user/' + user + + '/action/SetPassword', + withCredentials: true, + data: JSON.stringify({'data': [passwd]}), + responseType: 'arraybuffer' + }) + .then(function(response) { + return response.data; + }); + } + var priv = convertRoleIdToPriv(role); + return $http({ + method: 'POST', + url: DataService.getHost() + + '/xyz/openbmc_project/user/action/CreateUser', + withCredentials: true, + data: JSON.stringify({ + 'data': + [user, ['web', 'redfish', 'ssh'], priv, enabled] + }), + responseType: 'arraybuffer' + }) + .then(function(response) { + return setUserPassword(user, passwd); + }); + } + }, + updateUser: function(user, newUser, passwd, role, enabled) { + if (DataService.configJson.redfishSupportEnabled == true) { + var data = {}; + if ((newUser !== undefined) && (newUser != null)) { + data['UserName'] = newUser; + } + if ((role !== undefined) && (role != null)) { + data['RoleId'] = role; + } + if ((enabled !== undefined) && (enabled != null)) { + data['Enabled'] = enabled; + } + if ((passwd !== undefined) && (passwd != null)) { + data['Password'] = passwd; + } + return $http({ + method: 'PATCH', + url: DataService.getHost() + + '/redfish/v1/AccountService/Accounts/' + user, + withCredentials: true, + data: data + }); + } else { + var deferred = $q.defer(); + var promises = []; + function convertRoleIdToPriv(roleId) { + if (roleId == 'Administrator') { + return 'priv-admin'; + } else if (roleId == 'User') { + return 'priv-user'; + } else if (roleId == 'Operator') { + return 'priv-operator'; + } else if (roleId == 'Callback') { + return 'priv-callback'; + } + return ''; + } + function setUserProperty(user, propKey, propVal) { + return $http({ + method: 'PUT', + url: DataService.getHost() + + '/xyz/openbmc_project/user/' + user + '/attr/' + + propKey, + withCredentials: true, + data: JSON.stringify({'data': propVal}) + }) + .then(function(response) { + return response.data; + }); + } + function setUserPassword(user, passwd) { + return $http({ + method: 'POST', + url: DataService.getHost() + + '/xyz/openbmc_project/user/' + user + + '/action/SetPassword', + withCredentials: true, + data: JSON.stringify({'data': [passwd]}), + responseType: 'arraybuffer' + }) + .then(function(response) { + return response.data; + }); + } + function renameUser(user, newUser) { + return $http({ + method: 'POST', + url: DataService.getHost() + + '/xyz/openbmc_project/user/action/RenameUser', + withCredentials: true, + data: JSON.stringify({'data': [user, newUser]}) + }) + .then(function(response) { + return response.data; + }); + } + if ((role !== undefined) && (role != null)) { + var priv = convertRoleIdToPriv(role); + promises.push(setUserProperty(user, 'UserPrivilege', priv)); + } + if ((enabled !== undefined) && (enabled != null)) { + promises.push(setUserProperty(user, 'UserEnabled', enabled)); + } + if ((passwd !== undefined) && (passwd != null)) { + promises.push(setUserPassword(user, passwd)); + } + if ((newUser !== undefined) && (newUser != null)) { + promises.push(renameUser(user, newUser)); + } + $q.all(promises).then( + function(results) { + deferred.resolve(results); + }, + function(errors) { + deferred.reject(errors); + }); + return deferred.promise; + } + }, + deleteUser: function(user) { + if (DataService.configJson.redfishSupportEnabled == true) { + return $http({ + method: 'DELETE', + url: DataService.getHost() + + '/redfish/v1/AccountService/Accounts/' + user, + withCredentials: true, + }); + } else { + return $http({ + method: 'POST', + url: DataService.getHost() + '/xyz/openbmc_project/user/' + + user + '/action/Delete', + withCredentials: true, + data: JSON.stringify({'data': []}) + }) + .then(function(response) { + return response.data; + }); + } + }, chassisPowerOff: function() { var deferred = $q.defer(); $http({ @@ -1028,7 +1281,8 @@ window.angular && (function(angular) { var activationStatus = ''; // If the image is "Functional" use that for the - // activation status, else use the value of "Activation" + // activation status, else use the value of + // "Activation" // github.com/openbmc/phosphor-dbus-interfaces/blob/master/xyz/openbmc_project/Software/Activation.interface.yaml if (content.data[key].Activation) { activationStatus = diff --git a/app/users/controllers/user-accounts-controller.html b/app/users/controllers/user-accounts-controller.html index 9d388bc..d37c5e1 100644 --- a/app/users/controllers/user-accounts-controller.html +++ b/app/users/controllers/user-accounts-controller.html @@ -1,46 +1,95 @@ +<loader loading="loading"></loader> <div id="user-accounts"> <div class="row column"> - <h1>Manage user account</h1> - <div class="column small-12 page-header"> - <h2 class="h4">Change password</h2> + <h1>User account information</h1> + </div> + <div class="table row column" ng-show="users.length != 0"> + <div class="table__head"> + <div class="table__row"> + <div class="table__cell"> Username </div> + <div class="table__cell"> Enabled </div> + <div class="table__cell"> Role </div> + <div class="table__cell"> Locked </div> + <div class="table__cell"> Action </div> + </div> + </div> + <div class="table__body"> + <div class="table__row" ng-repeat="user in users"> + <div class="table__cell"> {{user.UserName}} </div> + <div class="table__cell"> {{user.Enabled}} </div> + <div class="table__cell"> {{user.RoleId}} </div> + <div class="table__cell"> {{user.Locked}} </div> + <div class="table__cell"> + <button type="button" class="btn-primary inline" ng-disabled="isUserSelected" ng-click="setSelectedUser(user)">Edit</button> + <button type="button" class="btn-primary inline" ng-disabled="isUserSelected" ng-click="deleteUser(user.UserName)">Delete</button> + </div> + </div> </div> </div> - <section class="row column" aria-label="change password form"> - <form class="user-manage__form" role="form"> - <fieldset> - <legend aria-label="user manager" class="accessible-text">Change password form</legend> - <div class="row column"> - <label for="user-manage__current-password">Current password</label> - <input id="user-manage__current-password" type="password" ng-model="oldPassword" class="user-manage__current-password inline" autocomplete="off"/> + <div class="table row column" ng-show="users.length == 0"> + <span>No users exist in system</span> + </div> + + <form role="form" class="user-manage__form"> + <section class="row column" aria-label="user manage form"> + <div class="column small-12 page-header"> + <h2 class="inline h4">User account settings</h2> + </div> + <div class='col-sm-12'> + <label class="col-md-1 control-label">UserName</label> + <div class="col-md-3"> + <input type="text" class="form-control" name="UserName" ng-model="selectedUser.UserName" /> + </div> </div> - <div class="inline"> - <label for="user-manage__new-password">New password</label> - <div class="user-manage__input-wrapper inline"> - <input id="user-manage__new-password" type="{{showpassword ? 'text' : 'password'}}" ng-model="password" class="user-manage__new-password inline" autocomplete="off"/> + <div class='col-sm-12 inline'> + <label class="col-md-1 control-label">Password</label> + <div class="col-md-3 user-manage__input-wrapper inline"> + <input type="{{showpassword ? 'text' : 'password'}}" class="form-control user-manage__new-password inline" name="Password" ng-model="selectedUser.Password" autocomplete="off"/> <button ng-model="showpassword" ng-click="togglePassword = !togglePassword; showpassword = !showpassword;" class="password-toggle"> <span ng-hide="togglePassword">Show</span> <span ng-show="togglePassword">Hide</span> </button> </div> </div> - <div class="inline"> - <label for="user-manage__verify-password">Retype new password</label> - <div class="user-manage__input-wrapper inline"> - <input id="user-manage__verify-password" type="{{showpasswordVerify ? 'text' : 'password'}}" ng-model="passwordVerify" class="user-manage__verify-password inline" autocomplete="off"/> + <div class='col-sm-12'> + <label class="col-md-1 control-label">Retype Password</label> + <div class="col-md-3 user-manage__input-wrapper inline"> + <input type="{{showpasswordVerify ? 'text' : 'password'}}" class="form-control user-manage__verify-password inline" name="VerifyPassword" ng-model="selectedUser.VerifyPassword" autocomplete="off"> <button ng-model="showpasswordVerify" ng-click="toggleVerify = !toggleVerify; showpasswordVerify = !showpasswordVerify;" class="password-toggle"> <span ng-hide="toggleVerify">Show</span> <span ng-show="toggleVerify">Hide</span> </button> </div> </div> - </fieldset> - <div class="user-manage__submit-wrapper"> - <button class="btn-primary inline" ng-click="changePassword(oldPassword, password, passwordVerify)">Save change</button> + <div class='col-sm-12'> + <label class="col-md-1 control-label">Role</label> + <div class="col-md-3 user-manage__input-wrapper inline"> + <select ng-model="selectedUser.RoleId" class="form-control inline"> + <option ng-repeat="role in roles" class="inline">{{role}}</option> + </select> + </div> + </div> + <div class='col-sm-12'> + <label class="col-md-1 control-label">Enabled</label> + <div class="col-md-3 user-manage__input-wrapper inline"> + <label class="control-check"> + <input type="checkbox" name="Enabled" ng-model="selectedUser.Enabled"/> + <span class="control__indicator"></span> + </label> + </div> + </div> + <div class="user-manage__submit-wrapper"> + <button type="button" class="btn-primary inline" ng-if="!isUserSelected" ng-click="createNewUser()">Create User</button> + <button type="button" class="btn-primary inline" ng-if="isUserSelected" ng-click="updateUserInfo()">Save</button> + <button type="button" class="btn-primary inline" ng-if="isUserSelected" ng-click="cancel()">Cancel</button> + </div> + </section> + <section class="row column"> + <div class='col-sm-12'> + <p ng-class="'user-manage__' + state" role="alert"> + {{outMsg}} + </p> </div> - <p ng-class="'change-password__' + state" role="alert"> - {{state === 'error' ? errorMsg : ''}} - {{state === 'success' ? 'Success! User Password has been changed!' : ''}} - </p> - </form> - </section> + </section> + </form> </div> diff --git a/app/users/controllers/user-accounts-controller.js b/app/users/controllers/user-accounts-controller.js index 24a93cb..5f90844 100644 --- a/app/users/controllers/user-accounts-controller.js +++ b/app/users/controllers/user-accounts-controller.js @@ -10,58 +10,168 @@ window.angular && (function(angular) { 'use strict'; angular.module('app.users').controller('userAccountsController', [ - '$scope', '$window', 'APIUtils', 'dataService', - function($scope, $window, APIUtils, dataService) { - $scope.dataService = dataService; + '$scope', 'APIUtils', + function($scope, APIUtils) { + // TODO: Get the roles using Roles redfish URI. + $scope.roles = ['Administrator', 'Operator', 'User', 'Callback']; $scope.state = 'none'; - $scope.errorMsg = ''; + $scope.outMsg = ''; + $scope.loading = true; - $scope.changePassword = function( - oldPassword, newPassword, confirmNewPassword) { - var user = $scope.dataService.getUser(); - if (!oldPassword || !newPassword || !confirmNewPassword) { + function loadUserInfo() { + $scope.users = []; + $scope.loading = true; + $scope.isUserSelected = false; + $scope.selectedUser = null; + + APIUtils.getAllUserAccounts() + .then( + function(res) { + $scope.users = res; + }, + function(error) { + console.log(JSON.stringify(error)); + }) + .finally(function() { + $scope.loading = false; + }); + }; + $scope.cancel = function() { + $scope.state = 'none'; + $scope.outMsg = ''; + loadUserInfo(); + }; + $scope.setSelectedUser = function(user) { + $scope.state = 'none'; + $scope.outMsg = ''; + + $scope.isUserSelected = true; + $scope.selectedUser = angular.copy(user); + $scope.selectedUser.VerifyPassword = null; + // Used while renaming the user. + $scope.selectedUser.CurrentUserName = $scope.selectedUser.UserName; + }; + $scope.createNewUser = function() { + $scope.state = 'none'; + $scope.outMsg = ''; + + if (!$scope.selectedUser.UserName || !$scope.selectedUser.Password) { $scope.state = 'error'; - $scope.errorMsg = 'Field is required!'; - return false; + $scope.outMsg = 'Username or Password can\'t be empty'; + return; } - if (newPassword !== confirmNewPassword) { + if ($scope.selectedUser.Password !== + $scope.selectedUser.VerifyPassword) { $scope.state = 'error'; - $scope.errorMsg = 'New passwords do not match!'; - return false; + $scope.outMsg = 'Passwords do not match'; + return; + } + var user = $scope.selectedUser.UserName; + var passwd = $scope.selectedUser.Password; + var role = $scope.selectedUser.RoleId; + var enabled = false; + if ($scope.selectedUser.Enabled != null) { + enabled = $scope.selectedUser.Enabled; } - if (newPassword === oldPassword) { + + $scope.loading = true; + APIUtils.createUser(user, passwd, role, enabled) + .then( + function(response) { + $scope.state = 'success'; + $scope.outMsg = 'User has been created successfully'; + }, + function(error) { + $scope.state = 'error'; + if ((error.data.error['@Message.ExtendedInfo'] != + undefined) && + (error.data.error['@Message.ExtendedInfo'].length != 0)) { + $scope.outMsg = + error.data.error['@Message.ExtendedInfo'][0].Message; + } else { + $scope.outMsg = 'Failed to create new user.'; + } + }) + .finally(function() { + loadUserInfo(); + $scope.loading = false; + }); + }; + $scope.updateUserInfo = function() { + $scope.state = 'none'; + $scope.outMsg = ''; + if ($scope.selectedUser.Password !== + $scope.selectedUser.VerifyPassword) { $scope.state = 'error'; - $scope.errorMsg = 'New password and old password match!'; - return false; + $scope.outMsg = 'Passwords do not match'; + return; + } + var data = {}; + if ($scope.selectedUser.UserName !== + $scope.selectedUser.CurrentUserName) { + data['UserName'] = $scope.selectedUser.UserName; } + $scope.selectedUser.VerifyPassword = null; + if ($scope.selectedUser.Password != null) { + data['Password'] = $scope.selectedUser.Password; + } + data['RoleId'] = $scope.selectedUser.RoleId; + data['Enabled'] = $scope.selectedUser.Enabled; + + $scope.loading = true; + APIUtils + .updateUser( + $scope.selectedUser.CurrentUserName, data['UserName'], + data['Password'], data['RoleId'], data['Enabled']) + .then( + function(response) { + $scope.state = 'success'; + $scope.outMsg = 'User has been updated successfully'; + }, + function(error) { + $scope.state = 'error'; + if ((error.data.error['@Message.ExtendedInfo'] != + undefined) && + (error.data.error['@Message.ExtendedInfo'].length != 0)) { + $scope.outMsg = + error.data.error['@Message.ExtendedInfo'][0].Message; + } else { + $scope.outMsg = 'Updating user failed.'; + } + }) + .finally(function() { + loadUserInfo(); + $scope.loading = false; + }); + }; + $scope.deleteUser = function(userName) { + $scope.state = 'none'; + $scope.outMsg = ''; - // Verify the oldPassword is correct - dataService.ignoreHttpError = true; - APIUtils.testPassword(user, oldPassword) + $scope.loading = true; + APIUtils.deleteUser(userName) .then( - function(state) { - APIUtils.changePassword(user, newPassword) - .then( - function(response) { - // Clear the textboxes on a success - $scope.passwordVerify = ''; - $scope.password = ''; - $scope.oldPassword = ''; - $scope.state = 'success'; - }, - function(error) { - $scope.state = 'error'; - $scope.errorMsg = 'Error changing password!'; - }); + function(response) { + $scope.state = 'success'; + $scope.outMsg = 'User has been deleted successfully'; }, function(error) { $scope.state = 'error'; - $scope.errorMsg = 'Old password is not correct!'; + if ((error.data.error['@Message.ExtendedInfo'] != + undefined) && + (error.data.error['@Message.ExtendedInfo'].length != 0)) { + $scope.outMsg = + error.data.error['@Message.ExtendedInfo'][0].Message; + } else { + $scope.outMsg = 'Deleting user failed.'; + } }) .finally(function() { - dataService.ignoreHttpError = false; + loadUserInfo(); + $scope.loading = false; }); }; + loadUserInfo(); } ]); })(angular); diff --git a/app/users/styles/index.scss b/app/users/styles/index.scss index 5162e09..da9c25e 100644 --- a/app/users/styles/index.scss +++ b/app/users/styles/index.scss @@ -1 +1 @@ -@import "./user-accounts.scss";
\ No newline at end of file +@import "./user-accounts.scss"; diff --git a/app/users/styles/user-accounts.scss b/app/users/styles/user-accounts.scss index 891b3a9..fb6d162 100644 --- a/app/users/styles/user-accounts.scss +++ b/app/users/styles/user-accounts.scss @@ -2,6 +2,9 @@ $userInputHeight: 40px; .user-manage__form { width: 100%; + .dropdown__button { + margin-bottom: 1.2em; + } label { width: 100%; min-width: 210px; @@ -12,13 +15,16 @@ $userInputHeight: 40px; width: 225px; height: $userInputHeight; } + select { + width: 225px; + height: $userInputHeight; + } fieldset { display: block; padding-left: 1.5em; margin-bottom: 1em; border-bottom: 1px solid $medgrey; } - .user-manage__input-wrapper { position: relative; height: $userInputHeight; @@ -31,15 +37,22 @@ $userInputHeight: 40px; position: absolute; right: 5px; padding: 3px; + margin-right: 20px; color: $primebtn__bg; font-size: .8em; @include vertCenter; } - .user-manage__submit-wrapper button { - float: right; - margin-left: .5em; + .user-manage__submit-wrapper { + width: 100%; + margin-top: 3em; + padding-top: 1em; + border-top: 1px solid $medgrey; + button { + float: right; + margin: .5em; + } } - .change-password__error + .user-manage__error { background: lighten($error-color, 20%); padding: 1em; @@ -49,9 +62,8 @@ $userInputHeight: 40px; color: $black; font-family: "Courier New", Helvetica, Arial, sans-serif; font-weight: 700; - width: 325px; } - .change-password__success + .user-manage__success { color: $primebtn__bg; padding: 1em; |