From 7f6eb7b7c322082cdc6c3fd1bd5539d8845de887 Mon Sep 17 00:00:00 2001 From: Julian Picht Date: Sat, 28 Sep 2019 18:07:39 +0200 Subject: [PATCH] extract message collection logic --- .../policy/GroupPasswordPolicyProvider.java | 122 +----------- .../policy/PolicyProviderMultiplexer.java | 180 ++++++++++++++++++ 2 files changed, 188 insertions(+), 114 deletions(-) create mode 100644 src/main/java/com/github/jpicht/keycloak/policy/PolicyProviderMultiplexer.java diff --git a/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java b/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java index e4e1a64..0da4575 100644 --- a/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java +++ b/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java @@ -16,149 +16,43 @@ 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.jboss.logging.Logger; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; 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 String ERROR_MESSAGE = "invalidGroupPasswordPolicy"; - - private KeycloakSession session; public GroupPasswordPolicyProvider(KeycloakSession session) { - this.session = session; + super(session); } @Override - public PolicyError validate(String username, String password) { - return null; - } - - private class PrefixRemover { - public LinkedList 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 strParts = Arrays.asList(str.split(" ")); - List 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 getMessagesWithoutPrefix() { - LinkedList 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) { + protected LinkedList findPolicies(RealmModel realm, UserModel user) { + // First get the name of the attribute String groupAttribute = realm.getPasswordPolicy().getPolicyConfig(GroupPasswordPolicyProviderFactory.ID); logger.debugf("groupAttribute %s", groupAttribute); logger.debugf("user %s", user.getUsername()); - LinkedList list = new LinkedList<>(); + LinkedList policyDefinitions = new LinkedList<>(); + // Iterate groups and collect policy strings for (GroupModel group : user.getGroups()) { logger.debugf("group %s", group.getName()); for (String policyString : group.getAttribute(groupAttribute)) { logger.infof("adding group password policy: %s", policyString); - PasswordPolicy policy = parsePolicy(policyString); - list.addAll(validateSubPolicy(policy, realm, user, password)); + policyDefinitions.add(policyString); } } - if (list.isEmpty()) { - 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())); + return policyDefinitions; } - private PasswordPolicy parsePolicy(String policy) { - LinkedList list = new LinkedList<>(); - PasswordPolicy parsedPolicy = PasswordPolicy.parse(session, policy); - return parsedPolicy; - } - - private LinkedList validateSubPolicy(PasswordPolicy policy, RealmModel realm, UserModel user, String password) { - RealmModel realRealm = session.getContext().getRealm(); - LinkedList 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 public Object parseConfig(String value) { if (value == null || value.isEmpty()) { diff --git a/src/main/java/com/github/jpicht/keycloak/policy/PolicyProviderMultiplexer.java b/src/main/java/com/github/jpicht/keycloak/policy/PolicyProviderMultiplexer.java new file mode 100644 index 0000000..00a245c --- /dev/null +++ b/src/main/java/com/github/jpicht/keycloak/policy/PolicyProviderMultiplexer.java @@ -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 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 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 list = new LinkedList<>(); + PasswordPolicy parsedPolicy = PasswordPolicy.parse(session, policy); + return parsedPolicy; + } + + // iterate policies + protected LinkedList validateSubPolicy(PasswordPolicy policy, RealmModel realm, UserModel user, String password) { + LinkedList 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 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 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 strParts = Arrays.asList(str.split(" ")); + List 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 getMessagesWithoutPrefix() { + LinkedList out = new LinkedList<>(); + for (String msg : messages) { + out.add(msg.substring(prefix.length())); + } + return out; + } + } +}