Merge pull request #177 from stianst/master

Fixes to password policy
This commit is contained in:
stianst 2014-01-24 06:55:15 -08:00
commit 3e15747836
5 changed files with 147 additions and 134 deletions

View file

@ -256,7 +256,7 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, $ht
};
});
module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications) {
module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications, PasswordPolicy) {
console.log('RealmRequiredCredentialsCtrl');
$scope.realm = {
@ -264,128 +264,25 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm,
requiredCredentials : realm.requiredCredentials,
requiredApplicationCredentials : realm.requiredApplicationCredentials,
requiredOAuthClientCredentials : realm.requiredOAuthClientCredentials,
registrationAllowed : realm.registrationAllowed
registrationAllowed : realm.registrationAllowed,
passwordPolicy: realm.passwordPolicy
};
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 (integer type). Default value is 8.",
digits: "Minimal number (integer type) of digits in password. Default value is 1.",
lowerCase: "Minimal number (integer type) of lowercase characters in password. Default value is 1.",
upperCase: "Minimal number (integer type) of uppercase characters in password. Default value is 1.",
specialChars: "Minimal number (integer type) of special characters in password. Default value is 1."
$scope.allPolicies = PasswordPolicy.allPolicies;
$scope.policyMessages = PasswordPolicy.policyMessages;
$scope.policy = PasswordPolicy.parse(realm.passwordPolicy);
$scope.addPolicy = function(policy){
$scope.policy.push(policy);
}
// $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(index){
$scope.policy.splice(index, 1);
}
$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 = {
'multiple' : true,
'simple_tags' : true,
@ -400,9 +297,9 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm,
}
}, true);
$scope.$watch('policy', function() {
$scope.realm.passwordPolicy = generatePolicy($scope.policy);
if ($scope.realm.passwordPolicy != realm.passwordPolicy){
$scope.$watch('policy', function(oldVal, newVal) {
if (oldVal != newVal) {
$scope.realm.passwordPolicy = PasswordPolicy.toString($scope.policy);
$scope.changed = true;
}
}, true);
@ -419,11 +316,8 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm,
$scope.reset = function() {
$scope.realm = angular.copy(oldCopy);
$scope.configuredPolicies = [];
$scope.availablePolicies = $scope.allPolicies.slice(0);
$scope.policy = evaluatePolicy(oldCopy.passwordPolicy);
updateConfigured();
$scope.policy = PasswordPolicy.parse(oldCopy.passwordPolicy);
console.debug(realm.passwordPolicy);
$scope.changed = false;
};

View file

@ -386,3 +386,68 @@ module.factory('TimeUnit', function() {
return t;
});
module.factory('PasswordPolicy', function() {
var p = {};
p.policyMessages = {
length: "Minimal password length (integer type). Default value is 8.",
digits: "Minimal number (integer type) of digits in password. Default value is 1.",
lowerCase: "Minimal number (integer type) of lowercase characters in password. Default value is 1.",
upperCase: "Minimal number (integer type) of uppercase characters in password. Default value is 1.",
specialChars: "Minimal number (integer type) of special characters in password. Default value is 1."
}
p.allPolicies = [
{ name: 'length', value: 8 },
{ name: 'digits', value: 1 },
{ name: 'lowerCase', value: 1 },
{ name: 'upperCase', value: 1 },
{ name: 'specialChars', value: 1 }
];
p.parse = function(policyString) {
var policies = [];
if (!policyString || policyString.length == 0){
return policies;
}
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);
policies.push({ name: policyEntry[1], value: parseInt(policyEntry[2]) });
}
return policies;
};
p.toString = function(policies) {
if (!policies || policies.length == 0) {
return null;
}
var policyString = "";
for (var i in policies){
policyString += policies[i].name;
if ( policies[i].value ){
policyString += '(' + policies[i].value + ')';
}
policyString += " and ";
}
policyString = policyString.substring(0, policyString.length - 5);
return policyString;
};
return p;
});

View file

@ -52,18 +52,16 @@
<table>
<caption class="hidden">Table of Password Policies</caption>
<thead>
<tr ng-show="availablePolicies.length > 0">
<tr ng-show="(allPolicies|remove:policy:'name').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 ng-model="selectedPolicy"
ng-options="p.name for p in (allPolicies|remove:policy:'name')"
data-ng-change="addPolicy(selectedPolicy); selectedAllPolicies = null">
<option value="" disabled selected>Add policy...</option>
</select>
</div>
<div>
<button ng-click="addPolicy()" ng-disabled="">Add Policy</button>
</div>
</div>
</th>
</tr>
@ -74,19 +72,19 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="name in configuredPolicies">
<tr ng-repeat="p in policy">
<td>
<div class="clearfix">
<input class="input-small disabled" type="text" value="{{name}}" readonly>
<input class="input-small disabled" type="text" value="{{p.name}}" readonly>
</div>
</td>
<td>
<input ng-model="policy[name]" type="number" placeholder="No value assigned" class="input-small">
<input ng-model="p.value" type="number" placeholder="No value assigned" class="input-small" min="1" max="50">
</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>
<div class="action-div"><i class="icon-remove" ng-click="removePolicy($index)"></i></div>
</td>
</tr>
</tbody>

View file

@ -168,6 +168,11 @@ public class RequiredActionsService {
return forms.setError(Messages.NOTMATCH_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
}
String error = realm.getPasswordPolicy().validate(passwordNew);
if (error != null) {
return forms.setError(error).forwardToAction(RequiredAction.UPDATE_PASSWORD);
}
UserCredentialModel credentials = new UserCredentialModel();
credentials.setType(CredentialRepresentation.PASSWORD);
credentials.setValue(passwordNew);

View file

@ -25,6 +25,9 @@ import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
@ -125,4 +128,52 @@ public class ResetPasswordTest {
Assert.assertEquals("Invalid email.", resetPasswordPage.getMessage());
}
@Test
public void resetPasswordWithPasswordPolicy() throws IOException, MessagingException {
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.setPasswordPolicy(new PasswordPolicy("length"));
}
});
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("test-user@localhost");
resetPasswordPage.assertCurrent();
Assert.assertEquals("Success!", resetPasswordPage.getMessage());
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
String changePasswordUrl = body.split("\n")[3];
driver.navigate().to(changePasswordUrl.trim());
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword("invalid", "invalid");
Assert.assertNotEquals("Success!", resetPasswordPage.getMessage());
Assert.assertEquals("Invalid password: minimum length 8", resetPasswordPage.getMessage());
updatePasswordPage.changePassword("new-password", "new-password");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
oauth.openLogout();
loginPage.open();
loginPage.login("test-user@localhost", "new-password");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
}
}