mirror of
https://github.com/bmlong137/keycloak-group-password-policy.git
synced 2025-09-10 22:21:07 +00:00
extract message collection logic
This commit is contained in:
@@ -16,149 +16,43 @@
|
|||||||
|
|
||||||
package com.github.jpicht.keycloak.policy;
|
package com.github.jpicht.keycloak.policy;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.text.MessageFormat;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.models.GroupModel;
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.policy.PasswordPolicyConfigException;
|
import org.keycloak.policy.PasswordPolicyConfigException;
|
||||||
import org.keycloak.policy.PasswordPolicyProvider;
|
|
||||||
import org.keycloak.policy.PolicyError;
|
|
||||||
import org.keycloak.theme.Theme;
|
|
||||||
|
|
||||||
public class GroupPasswordPolicyProvider implements PasswordPolicyProvider {
|
public class GroupPasswordPolicyProvider extends PolicyProviderMultiplexer {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(GroupPasswordPolicyProvider.class);
|
private static final Logger logger = Logger.getLogger(GroupPasswordPolicyProvider.class);
|
||||||
private static final String ERROR_MESSAGE = "invalidGroupPasswordPolicy";
|
|
||||||
|
|
||||||
private KeycloakSession session;
|
|
||||||
|
|
||||||
public GroupPasswordPolicyProvider(KeycloakSession session) {
|
public GroupPasswordPolicyProvider(KeycloakSession session) {
|
||||||
this.session = session;
|
super(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PolicyError validate(String username, String password) {
|
protected LinkedList<String> findPolicies(RealmModel realm, UserModel user) {
|
||||||
return null;
|
// First get the name of the attribute
|
||||||
}
|
|
||||||
|
|
||||||
private class PrefixRemover {
|
|
||||||
public LinkedList<String> messages;
|
|
||||||
public String prefix;
|
|
||||||
|
|
||||||
PrefixRemover() {
|
|
||||||
messages = new LinkedList<>();
|
|
||||||
prefix = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void add(String str) {
|
|
||||||
messages.add(str);
|
|
||||||
|
|
||||||
if (prefix == null) {
|
|
||||||
prefix = str;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (str.startsWith(prefix)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> strParts = Arrays.asList(str.split(" "));
|
|
||||||
List<String> prefixParts = Arrays.asList(prefix.split(" "));
|
|
||||||
|
|
||||||
int minLength = Math.min(strParts.size(), prefixParts.size());
|
|
||||||
for (int i = 0; i < minLength; i++) {
|
|
||||||
if (!strParts.get(i).equals(prefixParts.get(i))) {
|
|
||||||
prefix = String.join(" ", prefixParts.subList(0, i));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPrefix() {
|
|
||||||
return prefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LinkedList<String> getMessagesWithoutPrefix() {
|
|
||||||
LinkedList<String> out = new LinkedList<>();
|
|
||||||
for (String msg : messages) {
|
|
||||||
out.add(msg.substring(prefix.length()));
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PolicyError validate(RealmModel realm, UserModel user, String password) {
|
|
||||||
String groupAttribute = realm.getPasswordPolicy().getPolicyConfig(GroupPasswordPolicyProviderFactory.ID);
|
String groupAttribute = realm.getPasswordPolicy().getPolicyConfig(GroupPasswordPolicyProviderFactory.ID);
|
||||||
logger.debugf("groupAttribute %s", groupAttribute);
|
logger.debugf("groupAttribute %s", groupAttribute);
|
||||||
logger.debugf("user %s", user.getUsername());
|
logger.debugf("user %s", user.getUsername());
|
||||||
|
|
||||||
LinkedList<PolicyError> list = new LinkedList<>();
|
LinkedList<String> policyDefinitions = new LinkedList<>();
|
||||||
|
|
||||||
|
// Iterate groups and collect policy strings
|
||||||
for (GroupModel group : user.getGroups()) {
|
for (GroupModel group : user.getGroups()) {
|
||||||
logger.debugf("group %s", group.getName());
|
logger.debugf("group %s", group.getName());
|
||||||
for (String policyString : group.getAttribute(groupAttribute)) {
|
for (String policyString : group.getAttribute(groupAttribute)) {
|
||||||
logger.infof("adding group password policy: %s", policyString);
|
logger.infof("adding group password policy: %s", policyString);
|
||||||
PasswordPolicy policy = parsePolicy(policyString);
|
policyDefinitions.add(policyString);
|
||||||
list.addAll(validateSubPolicy(policy, realm, user, password));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (list.isEmpty()) {
|
return policyDefinitions;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Properties messageProps;
|
|
||||||
try {
|
|
||||||
messageProps = session.theme().getTheme(Theme.Type.ACCOUNT).getMessages(session.getContext().resolveLocale(user));
|
|
||||||
} catch (IOException e) {
|
|
||||||
return new PolicyError(e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
PrefixRemover messages = new PrefixRemover();
|
|
||||||
|
|
||||||
for (PolicyError e : list) {
|
|
||||||
messages.add(MessageFormat.format(messageProps.getProperty(e.getMessage(), e.getMessage()), e.getParameters()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new PolicyError(messages.getPrefix() + String.join("\n", messages.getMessagesWithoutPrefix()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private PasswordPolicy parsePolicy(String policy) {
|
|
||||||
LinkedList<PasswordPolicyProvider> list = new LinkedList<>();
|
|
||||||
PasswordPolicy parsedPolicy = PasswordPolicy.parse(session, policy);
|
|
||||||
return parsedPolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
private LinkedList<PolicyError> validateSubPolicy(PasswordPolicy policy, RealmModel realm, UserModel user, String password) {
|
|
||||||
RealmModel realRealm = session.getContext().getRealm();
|
|
||||||
LinkedList<PolicyError> list = new LinkedList<>();
|
|
||||||
try {
|
|
||||||
for (String id : policy.getPolicies()) {
|
|
||||||
FakeRealm fakeRealm = new FakeRealm();
|
|
||||||
fakeRealm.setPasswordPolicy(policy);
|
|
||||||
|
|
||||||
session.getContext().setRealm(fakeRealm);
|
|
||||||
|
|
||||||
PasswordPolicyProvider provider = session.getProvider(PasswordPolicyProvider.class, id);
|
|
||||||
PolicyError error = provider.validate(realm, user, password);
|
|
||||||
if (null != error) {
|
|
||||||
list.add(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
session.getContext().setRealm(realRealm);
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object parseConfig(String value) {
|
public Object parseConfig(String value) {
|
||||||
if (value == null || value.isEmpty()) {
|
if (value == null || value.isEmpty()) {
|
||||||
|
@@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Julian Picht
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.jpicht.keycloak.policy;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.PasswordPolicy;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.policy.PasswordPolicyProvider;
|
||||||
|
import org.keycloak.policy.PolicyError;
|
||||||
|
import org.keycloak.theme.Theme;
|
||||||
|
|
||||||
|
abstract public class PolicyProviderMultiplexer implements PasswordPolicyProvider {
|
||||||
|
|
||||||
|
protected KeycloakSession session;
|
||||||
|
|
||||||
|
public PolicyProviderMultiplexer(KeycloakSession session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected LinkedList<String> findPolicies(RealmModel realm, UserModel user);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PolicyError validate(String username, String password) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PolicyError validate(RealmModel realm, UserModel user, String password) {
|
||||||
|
LinkedList<PolicyError> list = new LinkedList<>();
|
||||||
|
|
||||||
|
// Iterate policies and evaluate them
|
||||||
|
// TODO: check for duplicates
|
||||||
|
for (String policyString : findPolicies(realm, user)) {
|
||||||
|
PasswordPolicy policy = parsePolicy(policyString);
|
||||||
|
list.addAll(validateSubPolicy(policy, realm, user, password));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translateMessges(list, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// use org.keycloak.models.PasswordPolicy to parse the policy string
|
||||||
|
protected PasswordPolicy parsePolicy(String policy) {
|
||||||
|
LinkedList<PasswordPolicyProvider> list = new LinkedList<>();
|
||||||
|
PasswordPolicy parsedPolicy = PasswordPolicy.parse(session, policy);
|
||||||
|
return parsedPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate policies
|
||||||
|
protected LinkedList<PolicyError> validateSubPolicy(PasswordPolicy policy, RealmModel realm, UserModel user, String password) {
|
||||||
|
LinkedList<PolicyError> list = new LinkedList<>();
|
||||||
|
|
||||||
|
for (String id : policy.getPolicies()) {
|
||||||
|
PolicyError error = validateSubPolicyProvider(policy, id, realm, user, password);
|
||||||
|
if (null != error) {
|
||||||
|
list.add(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the FakeRealm RealmModel implementation to configure the password policy classes
|
||||||
|
// and check the password against them.
|
||||||
|
protected PolicyError validateSubPolicyProvider(PasswordPolicy policy, String id, RealmModel realm, UserModel user, String password) {
|
||||||
|
// store the current realm locally, as we need to manipulate the session
|
||||||
|
RealmModel realRealm = session.getContext().getRealm();
|
||||||
|
|
||||||
|
// try->finally to ensure the original realm is always restored
|
||||||
|
try {
|
||||||
|
FakeRealm fakeRealm = new FakeRealm();
|
||||||
|
fakeRealm.setPasswordPolicy(policy);
|
||||||
|
|
||||||
|
session.getContext().setRealm(fakeRealm);
|
||||||
|
|
||||||
|
PasswordPolicyProvider provider = session.getProvider(PasswordPolicyProvider.class, id);
|
||||||
|
return provider.validate(realm, user, password);
|
||||||
|
} finally {
|
||||||
|
session.getContext().setRealm(realRealm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate the messages and remove the common prefix.
|
||||||
|
// We wan't to return ONE message with ALL the problems, not one problem at a time.
|
||||||
|
// The messages have common prefixes in most languages.
|
||||||
|
protected PolicyError translateMessges(LinkedList<PolicyError> list, UserModel user) {
|
||||||
|
Properties messageProps;
|
||||||
|
try {
|
||||||
|
messageProps = session.theme().getTheme(Theme.Type.ACCOUNT).getMessages(session.getContext().resolveLocale(user));
|
||||||
|
} catch (IOException e) {
|
||||||
|
return new PolicyError(e.getLocalizedMessage());
|
||||||
|
}
|
||||||
|
PrefixRemover messages = new PrefixRemover();
|
||||||
|
|
||||||
|
for (PolicyError e : list) {
|
||||||
|
messages.add(MessageFormat.format(messageProps.getProperty(e.getMessage(), e.getMessage()), e.getParameters()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PolicyError(messages.getPrefix() + String.join("\n", messages.getMessagesWithoutPrefix()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrefixRemover is used to remove the common prefix from multiple error messages.
|
||||||
|
// This is sadly currently necessary, because KeyCloak only supports one message
|
||||||
|
// at a time, which is not very user friendly.
|
||||||
|
private class PrefixRemover {
|
||||||
|
public LinkedList<String> messages;
|
||||||
|
public String prefix;
|
||||||
|
|
||||||
|
PrefixRemover() {
|
||||||
|
messages = new LinkedList<>();
|
||||||
|
prefix = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(String str) {
|
||||||
|
messages.add(str);
|
||||||
|
|
||||||
|
// handle first element
|
||||||
|
if (prefix == null) {
|
||||||
|
prefix = str;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the current prefix is a prefix to the new message no changes are needed.
|
||||||
|
if (str.startsWith(prefix)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split strings into words, we only consider whole words, when trying to
|
||||||
|
// find a common prefix. Otherwise weird stuff happens.
|
||||||
|
// I don't know JAVA, so this can probably be done much more efficiently.
|
||||||
|
List<String> strParts = Arrays.asList(str.split(" "));
|
||||||
|
List<String> prefixParts = Arrays.asList(prefix.split(" "));
|
||||||
|
|
||||||
|
int minLength = Math.min(strParts.size(), prefixParts.size());
|
||||||
|
for (int i = 0; i < minLength; i++) {
|
||||||
|
if (!strParts.get(i).equals(prefixParts.get(i))) {
|
||||||
|
prefix = String.join(" ", prefixParts.subList(0, i));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPrefix() {
|
||||||
|
return prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMessagesWithoutPrefix returns all messages removing the common prefix.
|
||||||
|
public LinkedList<String> getMessagesWithoutPrefix() {
|
||||||
|
LinkedList<String> out = new LinkedList<>();
|
||||||
|
for (String msg : messages) {
|
||||||
|
out.add(msg.substring(prefix.length()));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user