diff --git a/.gitignore b/.gitignore index eb5a316..683f09c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,12 @@ +# Maven target +pom.xml.versionsBackup + +# Eclipse +.project +.classpath +.settings + +# VS Code +.factorypath +.vscode diff --git a/README.md b/README.md index 9f4b774..b435012 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,30 @@ The extension can be installed just like any keycloak extension. Either copy it `keycloak/standalone/deployments` folder, or load it via the jboss command line tool. ## Usage -To use the plugin you create a new password policy entry on the realm's password policy sub-page -with the `Group Policy` type, then enter a group attribute name as the configuration. -On a password change request, the extension will then check all the user's groups for this -attribute name and parse the corresponding attribute value as a serialized password policy. +There are multiple steps you will want to take to use this plugin. First, you need to determine +what password policies you will want for all users and for each group of users. Once you have +that, you will need to come up with an ID where you will specify group password policies. For +the purposes of this documentation we will use the ID `passwordPolicy`. + +Go to the realm's password policy page. In the latest versions of Keycloak, this can be found +by navigating to the "Authentication" menu item in the vertical menu on the left side of the +realm's user interface. You will then need to navigate to the "Password Policy" tab along the +menu of tabs on the top of the page. + +This interface provides you the OOTB ability to specify password policies for **all** users. +This is still true with the plugin installed. You will now have an additional option: **Group +Policy**. To use the plugin, you must add that password policy. The "Policy Value" should be +set to the ID we came up with earlier: `passwordPolicy`. + +If you intend to use group-specific password expiration (`forceExpiredPasswordChange`), you will +need to perform an additional step in the configuration. In the same "Authentication" section, +navigate to the "Required Actions" tab along the menu of tabs on the top of the page. Use the +"Register" button to add the "Group-based Expired Password" action. Move it up in the order to +after the "Update Password" action. + +At this point, you will need to add an attribute (with key `passwordPolicy`) to each group you +want to have additional password policies. The format of that text is defined by Keycloak +documentation and covered in the section below. ### Password policy format All policies are represented by a short string immediately followed by parenthesis, optionally @@ -36,7 +56,8 @@ The [policies provided with KeyCloak](https://www.keycloak.org/docs/6.0/server_a | `regexPattern(string)` | regular expression | ✓ | | `notUsername()` | | ✓ | | `passwordBlacklist(string)` | file name | - | -| `passwordHistory(int)` | number of last used passwords to disallow | - | +| `passwordHistory(int)` | number of last used passwords to disallow | ✓ | +| `forceExpiredPasswordChange(string)` | number of days to expire password after | ✓ | On the realm model the password policy attribute is also used for other purposes. There are some registered "policies", that do not actually implement a policy that @@ -46,11 +67,9 @@ If these currently work is completely untested. | Identifier | Description | Tested | | ------------- |:------------------------------------ | ------ | -| `forceExpiredPasswordChange(int)` | number of days to expire password after | - | | `hashAlgorithm(string)` | hash algorithm to use when hashing the password | - | | `hashIterations(int)` | number of hash iterations | - | - ## Implementation To minimize code duplication the extension uses as much of the built-in KeyCloak code as possible. The parsing and instantiation of the policy provider classes is used as-is. diff --git a/pom.xml b/pom.xml index 47b7e07..a46bf3b 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.github.jpicht.keycloak.policy keycloak-group-password-policy - 0.1-SNAPSHOT + 0.2-SNAPSHOT jar @@ -13,8 +13,8 @@ ${java.version} ${java.version} - 6.0.1 - 1.0-rc5 + ${keycloak.majorVersion}.0.0 + 1.0 @@ -33,6 +33,11 @@ jboss-logging 3.4.0.Final + + org.keycloak + keycloak-services + ${keycloak.version} + org.keycloak keycloak-core @@ -45,23 +50,6 @@ provided true - - org.keycloak.testsuite - integration-arquillian-tests-base - 6.0.1 - test - - - org.keycloak - keycloak-test-helper - 6.0.1 - test - - - org.keycloak - keycloak-services - 6.0.1 - @@ -71,7 +59,336 @@ maven-jar-plugin 3.1.1 + + org.codehaus.mojo + build-helper-maven-plugin + 3.2.0 + + + + + org.apache.maven.plugins + maven-jar-plugin + + keycloak-v${keycloak.majorVersion} + + + + org.keycloak.keycloak-services + + + + + + + + + keycloak-v6 + + 6 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source-keycloak-v6 + add-source + + + ${basedir}/src/keycloak-v6-common/java + ${basedir}/src/keycloak-v6-v11/java + ${basedir}/src/keycloak-v6/java + + + + + + + + + + keycloak-v7 + + true + + + 7 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source-keycloak-v7 + add-source + + + ${basedir}/src/keycloak-v6-common/java + ${basedir}/src/keycloak-v6-v11/java + ${basedir}/src/keycloak-v6/java + + + + + + + + + + keycloak-v8 + + 8 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source-keycloak-v8 + add-source + + + ${basedir}/src/keycloak-v6-common/java + ${basedir}/src/keycloak-v8-common/java + ${basedir}/src/keycloak-v6-v11/java + ${basedir}/src/keycloak-v8/java + + + + + + + + + + keycloak-v9 + + 9 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source-keycloak-v9 + add-source + + + ${basedir}/src/keycloak-v6-common/java + ${basedir}/src/keycloak-v8-common/java + ${basedir}/src/keycloak-v9-common/java + ${basedir}/src/keycloak-v6-v11/java + ${basedir}/src/keycloak-v9/java + + + + + + + + + + keycloak-v10 + + 10 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source-keycloak-v10 + add-source + + + ${basedir}/src/keycloak-v6-common/java + ${basedir}/src/keycloak-v8-common/java + ${basedir}/src/keycloak-v9-common/java + ${basedir}/src/keycloak-v10-common/java + ${basedir}/src/keycloak-v6-v11/java + ${basedir}/src/keycloak-v10/java + + + + + + + + + + keycloak-v11 + + 11 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source-keycloak-v11 + add-source + + + ${basedir}/src/keycloak-v6-common/java + ${basedir}/src/keycloak-v8-common/java + ${basedir}/src/keycloak-v9-common/java + ${basedir}/src/keycloak-v10-common/java + ${basedir}/src/keycloak-v11-common/java + ${basedir}/src/keycloak-v6-v11/java + ${basedir}/src/keycloak-v11/java + + + + + + + + + + keycloak-v12 + + 12 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source-keycloak-v12 + add-source + + + ${basedir}/src/keycloak-v6-common/java + ${basedir}/src/keycloak-v8-common/java + ${basedir}/src/keycloak-v9-common/java + ${basedir}/src/keycloak-v10-common/java + ${basedir}/src/keycloak-v11-common/java + ${basedir}/src/keycloak-v12-common/java + ${basedir}/src/keycloak-v12/java + + + + + + + + + + keycloak-v13 + + 13 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source-keycloak-v13 + add-source + + + ${basedir}/src/keycloak-v6-common/java + ${basedir}/src/keycloak-v8-common/java + ${basedir}/src/keycloak-v9-common/java + ${basedir}/src/keycloak-v10-common/java + ${basedir}/src/keycloak-v11-common/java + ${basedir}/src/keycloak-v12-common/java + ${basedir}/src/keycloak-v13-common/java + ${basedir}/src/keycloak-v13/java + + + + + + + + + + keycloak-v14 + + 14 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source-keycloak-v14 + add-source + + + ${basedir}/src/keycloak-v6-common/java + ${basedir}/src/keycloak-v8-common/java + ${basedir}/src/keycloak-v9-common/java + ${basedir}/src/keycloak-v10-common/java + ${basedir}/src/keycloak-v11-common/java + ${basedir}/src/keycloak-v12-common/java + ${basedir}/src/keycloak-v13-common/java + ${basedir}/src/keycloak-v14-common/java + ${basedir}/src/keycloak-v14/java + + + + + + + + + + keycloak-v15 + + 15 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source-keycloak-v15 + add-source + + + ${basedir}/src/keycloak-v6-common/java + ${basedir}/src/keycloak-v8-common/java + ${basedir}/src/keycloak-v9-common/java + ${basedir}/src/keycloak-v10-common/java + ${basedir}/src/keycloak-v11-common/java + ${basedir}/src/keycloak-v12-common/java + ${basedir}/src/keycloak-v13-common/java + ${basedir}/src/keycloak-v14-common/java + ${basedir}/src/keycloak-v15-common/java + ${basedir}/src/keycloak-v15/java + + + + + + + + + diff --git a/src/keycloak-v10-common/java/com/github/jpicht/keycloak/policy/FakeRealmV10.java b/src/keycloak-v10-common/java/com/github/jpicht/keycloak/policy/FakeRealmV10.java new file mode 100644 index 0000000..11fee0a --- /dev/null +++ b/src/keycloak-v10-common/java/com/github/jpicht/keycloak/policy/FakeRealmV10.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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; + +public abstract class FakeRealmV10 extends FakeRealmV9 { + + @Override + public int getClientSessionIdleTimeout() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setClientSessionIdleTimeout(int seconds) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int getClientSessionMaxLifespan() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setClientSessionMaxLifespan(int seconds) { + throw new UnsupportedOperationException("Not supported yet."); + } + +} diff --git a/src/keycloak-v10/java/com/github/jpicht/keycloak/policy/FakeRealm.java b/src/keycloak-v10/java/com/github/jpicht/keycloak/policy/FakeRealm.java new file mode 100644 index 0000000..7d79c71 --- /dev/null +++ b/src/keycloak-v10/java/com/github/jpicht/keycloak/policy/FakeRealm.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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 org.keycloak.models.PasswordPolicy; + +public class FakeRealm extends FakeRealmV10 { + + private PasswordPolicy passwordPolicy; + + @Override + public PasswordPolicy getPasswordPolicy() { + return passwordPolicy; + } + + @Override + public void setPasswordPolicy(PasswordPolicy policy) { + passwordPolicy = policy; + } + +} diff --git a/src/keycloak-v11-common/java/com/github/jpicht/keycloak/policy/FakeRealmV11.java b/src/keycloak-v11-common/java/com/github/jpicht/keycloak/policy/FakeRealmV11.java new file mode 100644 index 0000000..ed6de27 --- /dev/null +++ b/src/keycloak-v11-common/java/com/github/jpicht/keycloak/policy/FakeRealmV11.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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; + +public abstract class FakeRealmV11 extends FakeRealmV10 { + + @Override + public int getClientOfflineSessionIdleTimeout() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setClientOfflineSessionIdleTimeout(int seconds) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int getClientOfflineSessionMaxLifespan() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setClientOfflineSessionMaxLifespan(int seconds) { + throw new UnsupportedOperationException("Not supported yet."); + } + +} diff --git a/src/keycloak-v11/java/com/github/jpicht/keycloak/policy/FakeRealm.java b/src/keycloak-v11/java/com/github/jpicht/keycloak/policy/FakeRealm.java new file mode 100644 index 0000000..6ad6c88 --- /dev/null +++ b/src/keycloak-v11/java/com/github/jpicht/keycloak/policy/FakeRealm.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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 org.keycloak.models.PasswordPolicy; + +public class FakeRealm extends FakeRealmV11 { + + private PasswordPolicy passwordPolicy; + + @Override + public PasswordPolicy getPasswordPolicy() { + return passwordPolicy; + } + + @Override + public void setPasswordPolicy(PasswordPolicy policy) { + passwordPolicy = policy; + } + +} diff --git a/src/keycloak-v12-common/java/com/github/jpicht/keycloak/policy/FakeRealmV12.java b/src/keycloak-v12-common/java/com/github/jpicht/keycloak/policy/FakeRealmV12.java new file mode 100644 index 0000000..d3960e2 --- /dev/null +++ b/src/keycloak-v12-common/java/com/github/jpicht/keycloak/policy/FakeRealmV12.java @@ -0,0 +1,202 @@ +/* + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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.util.Map; +import java.util.stream.Stream; + +import org.keycloak.component.ComponentModel; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.RequiredActionProviderModel; +import org.keycloak.models.RequiredCredentialModel; +import org.keycloak.models.RoleModel; + +public abstract class FakeRealmV12 extends FakeRealmV11 { + + @Override + public Stream getRequiredCredentialsStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getDefaultGroupsStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getClientsStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getClientsStream(Integer firstResult, Integer maxResults) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getAlwaysDisplayInConsoleClientsStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream searchClientByClientIdStream(String clientId, Integer firstResult, Integer maxResults) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getAuthenticationFlowsStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getAuthenticationExecutionsStream(String flowId) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getAuthenticatorConfigsStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getRequiredActionProvidersStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getIdentityProvidersStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getIdentityProviderMappersStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getIdentityProviderMappersByAliasStream(String brokerAlias) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getComponentsStream(String parentId, String providerType) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getComponentsStream(String parentId) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getComponentsStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getEventsListenersStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getEnabledEventTypesStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getSupportedLocalesStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getGroupsStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getTopLevelGroupsStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getTopLevelGroupsStream(Integer first, Integer max) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream searchForGroupByNameStream(String search, Integer first, Integer max) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getClientScopesStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void patchRealmLocalizationTexts(String locale, Map localizationTexts) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean removeRealmLocalizationTexts(String locale) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Map> getRealmLocalizationTexts() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Map getRealmLocalizationTextsByLocale(String locale) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getDefaultClientScopesStream(boolean defaultScope) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getRolesStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getRolesStream(Integer firstResult, Integer maxResults) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream searchForRolesStream(String search, Integer first, Integer max) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getDefaultRolesStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + +} diff --git a/src/keycloak-v12-common/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyFinder.java b/src/keycloak-v12-common/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyFinder.java new file mode 100644 index 0000000..b203f61 --- /dev/null +++ b/src/keycloak-v12-common/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyFinder.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long + * + * 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.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; + +import org.jboss.logging.Logger; +import org.keycloak.models.GroupModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +public class GroupPasswordPolicyFinder { + + private static final Logger logger = Logger.getLogger(GroupPasswordPolicyFinder.class); + + public List 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 policyDefinitions = new LinkedList<>(); + + // Iterate groups and collect policy strings + user.getGroupsStream().forEach(new Consumer() { + @Override + public void accept(GroupModel group) { + logger.debugf("group: %s", group.getName()); + group.getAttributeStream(groupAttribute).forEach(new Consumer() { + @Override + public void accept(String policyString) { + logger.infof("adding group password policy: %s", policyString); + policyDefinitions.add(policyString); + } + }); + } + }); + + return policyDefinitions; + } + +} diff --git a/src/keycloak-v12/java/com/github/jpicht/keycloak/policy/FakeRealm.java b/src/keycloak-v12/java/com/github/jpicht/keycloak/policy/FakeRealm.java new file mode 100644 index 0000000..e450f75 --- /dev/null +++ b/src/keycloak-v12/java/com/github/jpicht/keycloak/policy/FakeRealm.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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 org.keycloak.models.PasswordPolicy; + +public class FakeRealm extends FakeRealmV12 { + + private PasswordPolicy passwordPolicy; + + @Override + public PasswordPolicy getPasswordPolicy() { + return passwordPolicy; + } + + @Override + public void setPasswordPolicy(PasswordPolicy policy) { + passwordPolicy = policy; + } + +} diff --git a/src/keycloak-v13-common/java/com/github/jpicht/keycloak/policy/FakeRealmV13.java b/src/keycloak-v13-common/java/com/github/jpicht/keycloak/policy/FakeRealmV13.java new file mode 100644 index 0000000..db28517 --- /dev/null +++ b/src/keycloak-v13-common/java/com/github/jpicht/keycloak/policy/FakeRealmV13.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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.util.stream.Stream; + +import org.keycloak.models.CibaConfig; +import org.keycloak.models.ClientInitialAccessModel; +import org.keycloak.models.OAuth2DeviceConfig; +import org.keycloak.models.RoleModel; + +public abstract class FakeRealmV13 extends FakeRealmV12 { + + @Override + public OAuth2DeviceConfig getOAuth2DeviceConfig() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CibaConfig getCibaPolicy() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public RoleModel getDefaultRole() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setDefaultRole(RoleModel role) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public ClientInitialAccessModel createClientInitialAccessModel(int expiration, int count) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public ClientInitialAccessModel getClientInitialAccessModel(String id) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void removeClientInitialAccessModel(String id) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream getClientInitialAccesses() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void decreaseRemainingCount(ClientInitialAccessModel clientInitialAccess) { + throw new UnsupportedOperationException("Not supported yet."); + } + +} diff --git a/src/keycloak-v13/java/com/github/jpicht/keycloak/policy/FakeRealm.java b/src/keycloak-v13/java/com/github/jpicht/keycloak/policy/FakeRealm.java new file mode 100644 index 0000000..cdef8d4 --- /dev/null +++ b/src/keycloak-v13/java/com/github/jpicht/keycloak/policy/FakeRealm.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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 org.keycloak.models.PasswordPolicy; + +public class FakeRealm extends FakeRealmV13 { + + private PasswordPolicy passwordPolicy; + + @Override + public PasswordPolicy getPasswordPolicy() { + return passwordPolicy; + } + + @Override + public void setPasswordPolicy(PasswordPolicy policy) { + passwordPolicy = policy; + } + +} diff --git a/src/keycloak-v14-common/java/com/github/jpicht/keycloak/policy/FakeRealmV14.java b/src/keycloak-v14-common/java/com/github/jpicht/keycloak/policy/FakeRealmV14.java new file mode 100644 index 0000000..092f7e9 --- /dev/null +++ b/src/keycloak-v14-common/java/com/github/jpicht/keycloak/policy/FakeRealmV14.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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.util.Map; +import java.util.stream.Stream; + +import org.keycloak.models.ClientModel; + +public abstract class FakeRealmV14 extends FakeRealmV13 { + + @Override + public Stream searchClientByAttributes(Map attributes, Integer firstResult, Integer maxResults) { + throw new UnsupportedOperationException("Not supported yet."); + } + +} diff --git a/src/keycloak-v14/java/com/github/jpicht/keycloak/policy/FakeRealm.java b/src/keycloak-v14/java/com/github/jpicht/keycloak/policy/FakeRealm.java new file mode 100644 index 0000000..f482c08 --- /dev/null +++ b/src/keycloak-v14/java/com/github/jpicht/keycloak/policy/FakeRealm.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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 org.keycloak.models.PasswordPolicy; + +public class FakeRealm extends FakeRealmV14 { + + private PasswordPolicy passwordPolicy; + + @Override + public PasswordPolicy getPasswordPolicy() { + return passwordPolicy; + } + + @Override + public void setPasswordPolicy(PasswordPolicy policy) { + passwordPolicy = policy; + } + +} diff --git a/src/keycloak-v15-common/java/com/github/jpicht/keycloak/policy/FakeRealmV15.java b/src/keycloak-v15-common/java/com/github/jpicht/keycloak/policy/FakeRealmV15.java new file mode 100644 index 0000000..9197a5a --- /dev/null +++ b/src/keycloak-v15-common/java/com/github/jpicht/keycloak/policy/FakeRealmV15.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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 org.keycloak.models.ParConfig; + +public class FakeRealmV15 extends FakeRealmV14 { + + @Override + public ParConfig getParPolicy() { + throw new UnsupportedOperationException("Not supported yet."); + } + +} diff --git a/src/keycloak-v15/java/com/github/jpicht/keycloak/policy/FakeRealm.java b/src/keycloak-v15/java/com/github/jpicht/keycloak/policy/FakeRealm.java new file mode 100644 index 0000000..d335bf9 --- /dev/null +++ b/src/keycloak-v15/java/com/github/jpicht/keycloak/policy/FakeRealm.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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 org.keycloak.models.PasswordPolicy; + +public class FakeRealm extends FakeRealmV15 { + + private PasswordPolicy passwordPolicy; + + @Override + public PasswordPolicy getPasswordPolicy() { + return passwordPolicy; + } + + @Override + public void setPasswordPolicy(PasswordPolicy policy) { + passwordPolicy = policy; + } + +} diff --git a/src/main/java/com/github/jpicht/keycloak/policy/FakeRealm.java b/src/keycloak-v6-common/java/com/github/jpicht/keycloak/policy/FakeRealmV6.java similarity index 99% rename from src/main/java/com/github/jpicht/keycloak/policy/FakeRealm.java rename to src/keycloak-v6-common/java/com/github/jpicht/keycloak/policy/FakeRealmV6.java index 1fd552a..6d92de7 100644 --- a/src/main/java/com/github/jpicht/keycloak/policy/FakeRealm.java +++ b/src/keycloak-v6-common/java/com/github/jpicht/keycloak/policy/FakeRealmV6.java @@ -1,5 +1,6 @@ /* * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long (brian@inteligr8.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +37,8 @@ import org.keycloak.models.RequiredActionProviderModel; import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.RoleModel; -public class FakeRealm implements RealmModel { - +public abstract class FakeRealmV6 implements RealmModel { + @Override public String getId() { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. @@ -508,15 +509,14 @@ public class FakeRealm implements RealmModel { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } - private PasswordPolicy passwordPolicy; @Override public PasswordPolicy getPasswordPolicy() { - return passwordPolicy; + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override public void setPasswordPolicy(PasswordPolicy policy) { - passwordPolicy = policy; + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override diff --git a/src/keycloak-v6-v11/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyFinder.java b/src/keycloak-v6-v11/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyFinder.java new file mode 100644 index 0000000..5c1f41e --- /dev/null +++ b/src/keycloak-v6-v11/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyFinder.java @@ -0,0 +1,51 @@ +/* + * 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.util.LinkedList; +import java.util.List; + +import org.jboss.logging.Logger; +import org.keycloak.models.GroupModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +public class GroupPasswordPolicyFinder { + + private static final Logger logger = Logger.getLogger(GroupPasswordPolicyFinder.class); + + protected List 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 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); + policyDefinitions.add(policyString); + } + } + + return policyDefinitions; + } + +} diff --git a/src/keycloak-v6/java/com/github/jpicht/keycloak/policy/FakeRealm.java b/src/keycloak-v6/java/com/github/jpicht/keycloak/policy/FakeRealm.java new file mode 100644 index 0000000..efcb777 --- /dev/null +++ b/src/keycloak-v6/java/com/github/jpicht/keycloak/policy/FakeRealm.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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 org.keycloak.models.PasswordPolicy; + +public class FakeRealm extends FakeRealmV6 { + + private PasswordPolicy passwordPolicy; + + @Override + public PasswordPolicy getPasswordPolicy() { + return passwordPolicy; + } + + @Override + public void setPasswordPolicy(PasswordPolicy policy) { + passwordPolicy = policy; + } + +} diff --git a/src/keycloak-v8-common/java/com/github/jpicht/keycloak/policy/FakeRealmV8.java b/src/keycloak-v8-common/java/com/github/jpicht/keycloak/policy/FakeRealmV8.java new file mode 100644 index 0000000..0736533 --- /dev/null +++ b/src/keycloak-v8-common/java/com/github/jpicht/keycloak/policy/FakeRealmV8.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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 org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.WebAuthnPolicy; + +public abstract class FakeRealmV8 extends FakeRealmV6 { + + @Override + public WebAuthnPolicy getWebAuthnPolicy() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setWebAuthnPolicy(WebAuthnPolicy policy) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public AuthenticationExecutionModel getAuthenticationExecutionByFlowId(String flowId) { + throw new UnsupportedOperationException("Not supported yet."); + } + +} diff --git a/src/keycloak-v8/java/com/github/jpicht/keycloak/policy/FakeRealm.java b/src/keycloak-v8/java/com/github/jpicht/keycloak/policy/FakeRealm.java new file mode 100644 index 0000000..edb6df1 --- /dev/null +++ b/src/keycloak-v8/java/com/github/jpicht/keycloak/policy/FakeRealm.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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 org.keycloak.models.PasswordPolicy; + +public class FakeRealm extends FakeRealmV8 { + + private PasswordPolicy passwordPolicy; + + @Override + public PasswordPolicy getPasswordPolicy() { + return passwordPolicy; + } + + @Override + public void setPasswordPolicy(PasswordPolicy policy) { + passwordPolicy = policy; + } + +} diff --git a/src/keycloak-v9-common/java/com/github/jpicht/keycloak/policy/FakeRealmV9.java b/src/keycloak-v9-common/java/com/github/jpicht/keycloak/policy/FakeRealmV9.java new file mode 100644 index 0000000..55919d5 --- /dev/null +++ b/src/keycloak-v9-common/java/com/github/jpicht/keycloak/policy/FakeRealmV9.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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.util.List; +import java.util.Set; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.WebAuthnPolicy; + +public abstract class FakeRealmV9 extends FakeRealmV8 { + + @Override + public WebAuthnPolicy getWebAuthnPolicyPasswordless() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setWebAuthnPolicyPasswordless(WebAuthnPolicy policy) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public List getClients(Integer firstResult, Integer maxResults) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Long getClientsCount() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public List getAlwaysDisplayInConsoleClients() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public List searchClientByClientId(String clientId, Integer firstResult, Integer maxResults) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public GroupModel createGroup(String id, String name, GroupModel toParent) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Set getRoles(Integer firstResult, Integer maxResults) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Set searchForRoles(String search, Integer first, Integer max) { + throw new UnsupportedOperationException("Not supported yet."); + } + +} diff --git a/src/keycloak-v9/java/com/github/jpicht/keycloak/policy/FakeRealm.java b/src/keycloak-v9/java/com/github/jpicht/keycloak/policy/FakeRealm.java new file mode 100644 index 0000000..7fcc17a --- /dev/null +++ b/src/keycloak-v9/java/com/github/jpicht/keycloak/policy/FakeRealm.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long (brian@inteligr8.com) + * + * 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 org.keycloak.models.PasswordPolicy; + +public class FakeRealm extends FakeRealmV9 { + + private PasswordPolicy passwordPolicy; + + @Override + public PasswordPolicy getPasswordPolicy() { + return passwordPolicy; + } + + @Override + public void setPasswordPolicy(PasswordPolicy policy) { + passwordPolicy = policy; + } + +} diff --git a/src/main/java/com/github/jpicht/keycloak/policy/GroupExpiredPasswordRequiredActionFactory.java b/src/main/java/com/github/jpicht/keycloak/policy/GroupExpiredPasswordRequiredActionFactory.java new file mode 100644 index 0000000..1cf455a --- /dev/null +++ b/src/main/java/com/github/jpicht/keycloak/policy/GroupExpiredPasswordRequiredActionFactory.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021 Brian Long + * + * 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 org.jboss.logging.Logger; +import org.keycloak.Config.Scope; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +import com.google.auto.service.AutoService; + +/** + * @author brian@inteligr8.com + */ +@AutoService(RequiredActionFactory.class) +public class GroupExpiredPasswordRequiredActionFactory implements RequiredActionFactory { + + private final Logger logger = Logger.getLogger(GroupExpiredPasswordRequiredActionFactory.class); + + private static final String ID = "groupExpirePassswordAction"; + private static final String DISPLAY = "Group-based Expired Password"; + + @Override + public String getId() { + return ID; + } + + @Override + public RequiredActionProvider create(KeycloakSession session) { + this.logger.trace("create()"); + return new GroupExpiredPasswordRequiredActionProvider(session); + } + + @Override + public void init(Scope config) { + this.logger.trace("init()"); + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public String getDisplayText() { + return DISPLAY; + } + + @Override + public void close() { + this.logger.trace("close()"); + } + +} diff --git a/src/main/java/com/github/jpicht/keycloak/policy/GroupExpiredPasswordRequiredActionProvider.java b/src/main/java/com/github/jpicht/keycloak/policy/GroupExpiredPasswordRequiredActionProvider.java new file mode 100644 index 0000000..350e28c --- /dev/null +++ b/src/main/java/com/github/jpicht/keycloak/policy/GroupExpiredPasswordRequiredActionProvider.java @@ -0,0 +1,80 @@ +/* + * Copyright 2021 Brian Long + * + * 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.util.List; + +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +/** + * @author brian@inteligr8.com + */ +public class GroupExpiredPasswordRequiredActionProvider extends RequiredActionMultiplexer { + + private final Logger logger = Logger.getLogger(RequiredActionMultiplexer.class); + private final GroupPasswordPolicyFinder finder = new GroupPasswordPolicyFinder(); + private final KeycloakSession session; + + public GroupExpiredPasswordRequiredActionProvider(KeycloakSession session) { + this.session = session; + } + + @Override + protected int findDaysToExpire(RealmModel realm, UserModel user) { + if (this.logger.isTraceEnabled()) + this.logger.tracef("findDaysToExpire(%s, %s)", realm == null ? null : realm.getName(), user == null ? null : user.getId()); + + List policyStrs = this.finder.findPolicies(realm, user); + if (policyStrs == null || policyStrs.isEmpty()) + return -1; + this.logger.debugf("found policies: [%s]", policyStrs.toString()); + + Integer minDaysToExpire = null; + + for (String policyStr : policyStrs) { + this.logger.tracef("inspecting policy: %s", policyStr); + + PasswordPolicy policy = PasswordPolicy.parse(this.session, policyStr); + int daysToExpire = policy.getDaysToExpirePassword(); + if (daysToExpire < 0) + // policy does not have an expiration characteristic; so ignoring ... + continue; + + this.logger.debugf("found password expiration policy: %d days", daysToExpire); + + if (minDaysToExpire == null) { + // days to expire was set + minDaysToExpire = daysToExpire; + } else { + // days to expire was set; we want the most restrictive + minDaysToExpire = Math.min(minDaysToExpire, daysToExpire); + } + } + + this.logger.debugf("determined password expiration policy: %d days", minDaysToExpire); + return minDaysToExpire == null ? -1 : minDaysToExpire; + } + + @Override + public void close() { + } + +} 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 0da4575..eee8b76 100644 --- a/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java +++ b/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java @@ -1,5 +1,6 @@ /* * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,41 +17,24 @@ package com.github.jpicht.keycloak.policy; -import java.util.LinkedList; -import org.jboss.logging.Logger; -import org.keycloak.models.GroupModel; +import java.util.List; + import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.policy.PasswordPolicyConfigException; public class GroupPasswordPolicyProvider extends PolicyProviderMultiplexer { - - private static final Logger logger = Logger.getLogger(GroupPasswordPolicyProvider.class); + + private GroupPasswordPolicyFinder finder = new GroupPasswordPolicyFinder(); public GroupPasswordPolicyProvider(KeycloakSession session) { super(session); } @Override - 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 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); - policyDefinitions.add(policyString); - } - } - - return policyDefinitions; + protected List findPolicies(RealmModel realm, UserModel user) { + return this.finder.findPolicies(realm, user); } @Override @@ -64,4 +48,5 @@ public class GroupPasswordPolicyProvider extends PolicyProviderMultiplexer { @Override public void close() { } + } diff --git a/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProviderFactory.java b/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProviderFactory.java index 1ff1792..abaff5d 100644 --- a/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProviderFactory.java +++ b/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProviderFactory.java @@ -17,6 +17,8 @@ package com.github.jpicht.keycloak.policy; import com.google.auto.service.AutoService; + +import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -25,6 +27,8 @@ import org.keycloak.policy.PasswordPolicyProviderFactory; @AutoService(PasswordPolicyProviderFactory.class) public class GroupPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory { + + private final Logger logger = Logger.getLogger(GroupPasswordPolicyProviderFactory.class); static final String ID = "groupPasswordPolicy"; @@ -40,6 +44,7 @@ public class GroupPasswordPolicyProviderFactory implements PasswordPolicyProvide @Override public void init(Config.Scope config) { + this.logger.trace("init()"); } @Override @@ -68,5 +73,6 @@ public class GroupPasswordPolicyProviderFactory implements PasswordPolicyProvide @Override public void close() { + this.logger.trace("close()"); } } diff --git a/src/main/java/com/github/jpicht/keycloak/policy/PolicyProviderMultiplexer.java b/src/main/java/com/github/jpicht/keycloak/policy/PolicyProviderMultiplexer.java index 00a245c..fa0bb8e 100644 --- a/src/main/java/com/github/jpicht/keycloak/policy/PolicyProviderMultiplexer.java +++ b/src/main/java/com/github/jpicht/keycloak/policy/PolicyProviderMultiplexer.java @@ -22,6 +22,7 @@ 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; @@ -38,7 +39,7 @@ abstract public class PolicyProviderMultiplexer implements PasswordPolicyProvide this.session = session; } - abstract protected LinkedList findPolicies(RealmModel realm, UserModel user); + protected abstract List findPolicies(RealmModel realm, UserModel user); @Override public PolicyError validate(String username, String password) { @@ -65,7 +66,6 @@ abstract public class PolicyProviderMultiplexer implements PasswordPolicyProvide // 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; } diff --git a/src/main/java/com/github/jpicht/keycloak/policy/RequiredActionMultiplexer.java b/src/main/java/com/github/jpicht/keycloak/policy/RequiredActionMultiplexer.java new file mode 100644 index 0000000..ac82f5b --- /dev/null +++ b/src/main/java/com/github/jpicht/keycloak/policy/RequiredActionMultiplexer.java @@ -0,0 +1,96 @@ +/* + * Copyright 2021 Brian Long + * + * 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.util.concurrent.TimeUnit; + +import org.jboss.logging.Logger; +import org.keycloak.authentication.InitiatedActionSupport; +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.common.util.Time; +import org.keycloak.credential.CredentialModel; +import org.keycloak.credential.CredentialProvider; +import org.keycloak.credential.PasswordCredentialProvider; +import org.keycloak.credential.PasswordCredentialProviderFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +/** + * @author brian@inteligr8.com + */ +abstract class RequiredActionMultiplexer implements RequiredActionProvider { + + private final Logger logger = Logger.getLogger(RequiredActionMultiplexer.class); + + protected abstract int findDaysToExpire(RealmModel realm, UserModel user); + + @Override + public InitiatedActionSupport initiatedActionSupport() { + return InitiatedActionSupport.SUPPORTED; + } + + /** + * This is a re-implementation of what is found in the default + * implementation in Keycloak. It just makes days-to-expire abstract so it + * can be implemented any number of ways. + * + * https://github.com/keycloak/keycloak/blob/af3b573d196af882dfb25cdccb98361746e85481/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java#L67 + */ + @Override + public void evaluateTriggers(RequiredActionContext context) { + this.logger.tracef("evaluateTriggers(%s)", context.getUser() != null ? context.getUser().getUsername() : null); + + int daysToExpirePassword = this.findDaysToExpire(context.getRealm(), context.getUser()); + if (daysToExpirePassword > -1) { + this.logger.debugf("Found password expiration: %d days", daysToExpirePassword); + + PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider)context.getSession() + .getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID); + CredentialModel password = passwordProvider.getPassword(context.getRealm(), context.getUser()); + if (password != null) { + this.logger.tracef("Found password credentials; created: %d ms", password.getCreatedDate()); + + if(password.getCreatedDate() == null) { + context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); + this.logger.debug("User is required to update password"); + } else { + long timeElapsed = Time.currentTimeMillis() - password.getCreatedDate(); + long timeToExpire = TimeUnit.DAYS.toMillis(daysToExpirePassword); + + if(timeElapsed > timeToExpire) { + context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); + this.logger.debug("User is required to update password"); + } else { + this.logger.tracef("Password credentials expire in %d ms", timeToExpire); + } + } + } + } + } + + @Override + public void requiredActionChallenge(RequiredActionContext context) { + this.logger.tracef("requiredActionChallenge(%s)", context.getUser() != null ? context.getUser().getUsername() : null); + } + + @Override + public void processAction(RequiredActionContext context) { + this.logger.tracef("processAction(%s)", context.getUser() != null ? context.getUser().getUsername() : null); + } + +}