KEYCLOAK-186 added UI for password policy settings

This commit is contained in:
vrockai 2013-12-04 17:48:10 +01:00
parent a7b653de28
commit 2596dcc9c3
4 changed files with 223 additions and 15 deletions

View file

@ -13,12 +13,12 @@ select:-moz-focusring {
input[type="text"], input[type="text"],
input[type="password"], input[type="password"],
input[type="email"], input[type="email"],
input[type="number"],
textarea { textarea {
font-size: 1.1em; font-size: 1.1em;
padding: 0 0.545454545454545em; padding: 0 0.545454545454545em;
height: 2.36363636363636em; height: 2.36363636363636em;
/* 26px */ /* 26px */
border: 1px #b6b6b6 solid; border: 1px #b6b6b6 solid;
border-radius: 2px; border-radius: 2px;
box-shadow: inset 0px 2px 2px rgba(0, 0, 0, 0.1); box-shadow: inset 0px 2px 2px rgba(0, 0, 0, 0.1);
@ -28,12 +28,14 @@ textarea {
input[type="text"]:hover, input[type="text"]:hover,
input[type="password"]:hover, input[type="password"]:hover,
input[type="email"]:hover, input[type="email"]:hover,
input[type="number"]:hover,
textarea:hover { textarea:hover {
border-color: #62afdb; border-color: #62afdb;
} }
input[type="text"]:focus, input[type="text"]:focus,
input[type="password"]:focus, input[type="password"]:focus,
input[type="email"]:focus, input[type="email"]:focus,
input[type="number"]:focus,
textarea:focus { textarea:focus {
border-color: #62afdb; border-color: #62afdb;
box-shadow: #62afdb 0 0 5px; box-shadow: #62afdb 0 0 5px;
@ -41,6 +43,7 @@ textarea:focus {
input[type="text"].error, input[type="text"].error,
input[type="password"].error, input[type="password"].error,
input[type="email"].error, input[type="email"].error,
input[type="number"].error,
textarea.error { textarea.error {
border-color: #ba1212; border-color: #ba1212;
transition: all 0.33s ease-in-out; transition: all 0.33s ease-in-out;
@ -50,36 +53,42 @@ textarea.error {
input[type="text"].error:focus, input[type="text"].error:focus,
input[type="password"].error:focus, input[type="password"].error:focus,
input[type="email"].error:focus, input[type="email"].error:focus,
input[type="number"].error:focus,
textarea.error:focus { textarea.error:focus {
box-shadow: 0 0 5px #ba1212; box-shadow: 0 0 5px #ba1212;
} }
input[type="text"].tiny, input[type="text"].tiny,
input[type="password"].tiny, input[type="password"].tiny,
input[type="email"].tiny, input[type="email"].tiny,
input[type="number"].tiny,
textarea.tiny { textarea.tiny {
width: 4.54545454545455em; width: 4.54545454545455em;
} }
input[type="text"].small, input[type="text"].small,
input[type="password"].small, input[type="password"].small,
input[type="email"].small, input[type="email"].small,
input[type="number"].small,
textarea.small { textarea.small {
width: 9.09090909090909em; width: 9.09090909090909em;
} }
input[type="text"].medium, input[type="text"].medium,
input[type="password"].medium, input[type="password"].medium,
input[type="email"].medium, input[type="email"].medium,
input[type="number"].medium,
textarea.medium { textarea.medium {
width: 18.1818em; width: 18.1818em;
} }
input[type="text"].large, input[type="text"].large,
input[type="password"].large, input[type="password"].large,
input[type="email"].large, input[type="email"].large,
input[type="number"].large,
textarea.large { textarea.large {
width: 27.2727272727273em; width: 27.2727272727273em;
} }
input[type="text"].xlarge, input[type="text"].xlarge,
input[type="password"].xlarge, input[type="password"].xlarge,
input[type="email"].xlarge, input[type="email"].xlarge,
input[type="number"].xlarge,
textarea.xlarge { textarea.xlarge {
width: 36.3636363636364em; width: 36.3636363636364em;
} }
@ -90,18 +99,21 @@ textarea {
input[type="text"][readonly], input[type="text"][readonly],
input[type="password"][readonly], input[type="password"][readonly],
input[type="email"][readonly], input[type="email"][readonly],
input[type="number"][readonly],
textarea[readonly] { textarea[readonly] {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
input[type="text"][readonly]:hover, input[type="text"][readonly]:hover,
input[type="password"][readonly]:hover, input[type="password"][readonly]:hover,
input[type="email"][readonly]:hover, input[type="email"][readonly]:hover,
input[type="number"][readonly]:hover,
textarea[readonly]:hover { textarea[readonly]:hover {
border-color: #62afdb; border-color: #62afdb;
} }
input[type="text"][readonly]:focus, input[type="text"][readonly]:focus,
input[type="password"][readonly]:focus, input[type="password"][readonly]:focus,
input[type="email"][readonly]:focus, input[type="email"][readonly]:focus,
input[type="number"][readonly]:focus,
textarea[readonly]:focus { textarea[readonly]:focus {
border-color: #b6b6b6; border-color: #b6b6b6;
box-shadow: none; box-shadow: none;

View file

@ -20,6 +20,7 @@ select:-moz-focusring {
input[type="text"], input[type="text"],
input[type="password"], input[type="password"],
input[type="email"], input[type="email"],
input[type="number"],
textarea { textarea {
font-size: 1.1em; font-size: 1.1em;
padding: 0 0.545454545454545em; padding: 0 0.545454545454545em;
@ -79,6 +80,7 @@ textarea {
input[type="text"][readonly], input[type="text"][readonly],
input[type="password"][readonly], input[type="password"][readonly],
input[type="email"][readonly], input[type="email"][readonly],
input[type="number"][readonly],
textarea[readonly] { textarea[readonly] {
background-color: #f0f0f0; background-color: #f0f0f0;

View file

@ -181,13 +181,131 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm,
registrationAllowed : realm.registrationAllowed registrationAllowed : realm.registrationAllowed
}; };
if (realm.hasOwnProperty('passwordPolicy')){
$scope.realm['passwordPolicy'] = realm.passwordPolicy;
} else {
$scope.realm['passwordPolicy'] = "";
realm['passwordPolicy'] = "";
}
var oldCopy = angular.copy($scope.realm);
/* Map used in the table when hovering over (i) icon */
$scope.policyMessages = {
length: "Minimal password length. Default value is 8.",
digits: "Minimal number of digits in password. Default value is 1.",
lowerCase: "Minimal number of lowercase characters in password. Default value is 1.",
upperCase: "Minimal number of uppercase characters in password. Default value is 1.",
specialChars: "Minimal number of special characters in password. Default value is 1."
}
// $scope.policy is an object representing passwordPolicy string
$scope.policy = {};
// All available policies
$scope.allPolicies = ['length', 'digits', 'lowerCase', 'upperCase', 'specialChars'];
// List of configured policies
$scope.configuredPolicies = [];
// List of not configured policies
$scope.availablePolicies = $scope.allPolicies.slice(0);
$scope.addPolicy = function(){
$scope.policy[$scope.newPolicyId] = "";
updateConfigured();
}
$scope.removePolicy = function(pId){
delete $scope.policy[pId];
updateConfigured();
}
// Updating lists of configured and non-configured policies based on the $scope.policy object
var updateConfigured = function(){
for (var i = 0; i < $scope.allPolicies.length; i++){
var policy = $scope.allPolicies[i];
if($scope.policy.hasOwnProperty(policy)){
var ind = $scope.configuredPolicies.indexOf(policy);
if(ind < 0){
$scope.configuredPolicies.push(policy);
}
ind = $scope.availablePolicies.indexOf(policy);
if(ind > -1){
$scope.availablePolicies.splice(ind, 1);
}
} else {
var ind = $scope.configuredPolicies.indexOf(policy);
if(ind > -1){
$scope.configuredPolicies.splice(ind, 1);
}
ind = $scope.availablePolicies.indexOf(policy);
if(ind < 0){
$scope.availablePolicies.push(policy);
}
}
}
if ($scope.availablePolicies.length > 0){
$scope.newPolicyId = $scope.availablePolicies[0];
}
}
// Creating object from policy string
var evaluatePolicy = function(policyString){
var policyObject = {};
if (!policyString || policyString.length == 0){
return policyObject;
}
var policyArray = policyString.split(" and ");
for (var i = 0; i < policyArray.length; i ++){
var policyToken = policyArray[i];
var re = /(\w+)\(*(\d*)\)*/;
var policyEntry = re.exec(policyToken);
policyObject[policyEntry[1]] = policyEntry[2];
}
return policyObject;
}
// Creating policy string based on policy object
var generatePolicy = function(policyObject){
var policyString = "";
for (var key in policyObject){
policyString += key;
var value = policyObject[key];
if ( value != ""){
policyString += "("+value+")";
}
policyString += " and ";
}
policyString = policyString.substring(0, policyString.length - 5);
return policyString;
}
$scope.policy = evaluatePolicy(realm.passwordPolicy);
updateConfigured();
$scope.userCredentialOptions = { $scope.userCredentialOptions = {
'multiple' : true, 'multiple' : true,
'simple_tags' : true, 'simple_tags' : true,
'tags' : ['password', 'totp', 'cert'] 'tags' : ['password', 'totp', 'cert']
}; };
var oldCopy = angular.copy($scope.realm);
$scope.changed = false; $scope.changed = false;
$scope.$watch('realm', function() { $scope.$watch('realm', function() {
@ -196,17 +314,31 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm,
} }
}, true); }, true);
$scope.$watch('policy', function() {
$scope.realm.passwordPolicy = generatePolicy($scope.policy);
if ($scope.realm.passwordPolicy != realm.passwordPolicy){
$scope.changed = true;
}
}, true);
$scope.save = function() { $scope.save = function() {
var realmCopy = angular.copy($scope.realm);
$scope.changed = false; $scope.changed = false;
Realm.update(realmCopy, function () {
Realm.update($scope.realm, function () {
$location.url("/realms/" + realm.id + "/required-credentials"); $location.url("/realms/" + realm.id + "/required-credentials");
Notifications.success("Your changes have been saved to the realm."); Notifications.success("Your changes have been saved to the realm.");
oldCopy = angular.copy($scope.realm);
}); });
}; };
$scope.reset = function() { $scope.reset = function() {
$scope.realm = angular.copy(oldCopy); $scope.realm = angular.copy(oldCopy);
$scope.configuredPolicies = [];
$scope.availablePolicies = $scope.allPolicies.slice(0);
$scope.policy = evaluatePolicy(oldCopy.passwordPolicy);
updateConfigured();
$scope.changed = false; $scope.changed = false;
}; };
}); });

View file

@ -1,4 +1,4 @@
<div id="wrapper" class="container"> <div id="wrapper" class="container realm-policy">
<div class="row"> <div class="row">
<div class="bs-sidebar col-md-3 clearfix" data-ng-include data-src="'partials/realm-menu.html'"></div> <div class="bs-sidebar col-md-3 clearfix" data-ng-include data-src="'partials/realm-menu.html'"></div>
<div id="content-area" class="col-md-9" role="main"> <div id="content-area" class="col-md-9" role="main">
@ -23,6 +23,7 @@
<h2><span>{{realm.realm}}</span> Credentials</h2> <h2><span>{{realm.realm}}</span> Credentials</h2>
<form name="realmForm" novalidate> <form name="realmForm" novalidate>
<fieldset class="border-top"> <fieldset class="border-top">
<legend uncollapsed><span class="text">Realm Credentials Settings</span></legend>
<div class="form-group clearfix"> <div class="form-group clearfix">
<label for="user" class="control-label two-lines">Required User Credentials</label> <label for="user" class="control-label two-lines">Required User Credentials</label>
@ -44,24 +45,85 @@
<input id="oauth" type="text" ui-select2="userCredentialOptions" ng-model="realm.requiredOAuthClientCredentials" placeholder="Type a role and enter"> <input id="oauth" type="text" ui-select2="userCredentialOptions" ng-model="realm.requiredOAuthClientCredentials" placeholder="Type a role and enter">
</div> </div>
</div> </div>
<div class="form-group">
<label for="policy">Password Policy <span class="required" data-ng-show="createRealm">*</span></label>
<div class="controls">
<input class="xlarge" type="text" id="policy" name="policy" data-ng-model="realm.passwordPolicy" autofocus required>
</div>
</div>
</fieldset> </fieldset>
<div class="form-actions"> <fieldset class="border-top">
<legend uncollapsed><span class="text">Realm Password Policy</span></legend>
<div class="form-group clearfix">
<table>
<caption class="hidden">Table of Password Policies</caption>
<thead>
<tr ng-show="availablePolicies.length > 0">
<th colspan="5" class="rcue-table-actions">
<div class="actions">
<div class="select-rcue">
<select ng-model="newPolicyId"
ng-options="name as name for name in availablePolicies"
placeholder="Please select">
</select>
</div>
<div>
<button ng-click="addPolicy()" ng-disabled="">Add Policy</button>
</div>
</div>
</th>
</tr>
<tr>
<th>Policy Type</th>
<th>Policy Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="name in configuredPolicies">
<td>
<div class="clearfix">
<input class="input-small disabled" type="text" value="{{name}}" readonly>
</div>
</td>
<td>
<input ng-model="policy[name]" type="number" placeholder="No value assigned" class="input-small">
</td>
<td>
<div class="action-div"><i class="icon-question" popover="{{policyMessages[name]}}"
popover-placement="left" popover-trigger="mouseenter"></i></div>
<div class="action-div"><i class="icon-remove" ng-click="removePolicy(name)"></i></div>
</td>
</tr>
</tbody>
</table>
</div>
</fieldset>
<div class="form-actions">
<button type="submit" kc-save class="primary" data-ng-show="changed">Save <button type="submit" kc-save class="primary" data-ng-show="changed">Save
</button> </button>
<button type="submit" kc-reset data-ng-show="changed">Clear changes <button type="submit" kc-reset data-ng-show="changed">Clear changes
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<div id="container-right-bg"></div> <div id="container-right-bg"></div>
</div> </div>
</div> </div>
<!-- TODO remove once this page is properly styled -->
<style type="text/css">
.realm-policy .actions > div {
display: inline-block;
overflow: hidden;
}
.realm-policy td {
font-size: 10px;
}
.realm-policy .action-div {
display: inline-block;
margin: 5px;
}
.realm-policy .icon-remove, .realm-policy .icon-question {
cursor: pointer;
}
</style>