Merge pull request #8 from bmlong137/develop

Version compatibility and improved functionality by @bmlong137
This commit is contained in:
Julian Picht 2021-11-22 18:35:45 +01:00 committed by GitHub
commit dffb643b19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1603 additions and 57 deletions

11
.gitignore vendored
View File

@ -1 +1,12 @@
# Maven
target
pom.xml.versionsBackup
# Eclipse
.project
.classpath
.settings
# VS Code
.factorypath
.vscode

View File

@ -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.

357
pom.xml
View File

@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.jpicht.keycloak.policy</groupId>
<artifactId>keycloak-group-password-policy</artifactId>
<version>0.1-SNAPSHOT</version>
<version>0.2-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
@ -13,8 +13,8 @@
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<keycloak.version>6.0.1</keycloak.version>
<auto-service.version>1.0-rc5</auto-service.version>
<keycloak.version>${keycloak.majorVersion}.0.0</keycloak.version>
<auto-service.version>1.0</auto-service.version>
</properties>
<dependencies>
@ -33,6 +33,11 @@
<artifactId>jboss-logging</artifactId>
<version>3.4.0.Final</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
@ -45,23 +50,6 @@
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-base</artifactId>
<version>6.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-test-helper</artifactId>
<version>6.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>6.0.1</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
@ -71,7 +59,336 @@
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.1</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.2.0</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<classifier>keycloak-v${keycloak.majorVersion}</classifier>
<archive>
<manifestEntries>
<!-- This is required in order for the classloader of this JAR to access the specified library at runtime -->
<Dependencies>org.keycloak.keycloak-services</Dependencies>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>keycloak-v6</id>
<properties>
<keycloak.majorVersion>6</keycloak.majorVersion>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source-keycloak-v6</id>
<goals><goal>add-source</goal></goals>
<configuration>
<sources>
<source>${basedir}/src/keycloak-v6-common/java</source>
<source>${basedir}/src/keycloak-v6-v11/java</source>
<source>${basedir}/src/keycloak-v6/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>keycloak-v7</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<keycloak.majorVersion>7</keycloak.majorVersion>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source-keycloak-v7</id>
<goals><goal>add-source</goal></goals>
<configuration>
<sources>
<source>${basedir}/src/keycloak-v6-common/java</source>
<source>${basedir}/src/keycloak-v6-v11/java</source>
<source>${basedir}/src/keycloak-v6/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>keycloak-v8</id>
<properties>
<keycloak.majorVersion>8</keycloak.majorVersion>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source-keycloak-v8</id>
<goals><goal>add-source</goal></goals>
<configuration>
<sources>
<source>${basedir}/src/keycloak-v6-common/java</source>
<source>${basedir}/src/keycloak-v8-common/java</source>
<source>${basedir}/src/keycloak-v6-v11/java</source>
<source>${basedir}/src/keycloak-v8/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>keycloak-v9</id>
<properties>
<keycloak.majorVersion>9</keycloak.majorVersion>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source-keycloak-v9</id>
<goals><goal>add-source</goal></goals>
<configuration>
<sources>
<source>${basedir}/src/keycloak-v6-common/java</source>
<source>${basedir}/src/keycloak-v8-common/java</source>
<source>${basedir}/src/keycloak-v9-common/java</source>
<source>${basedir}/src/keycloak-v6-v11/java</source>
<source>${basedir}/src/keycloak-v9/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>keycloak-v10</id>
<properties>
<keycloak.majorVersion>10</keycloak.majorVersion>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source-keycloak-v10</id>
<goals><goal>add-source</goal></goals>
<configuration>
<sources>
<source>${basedir}/src/keycloak-v6-common/java</source>
<source>${basedir}/src/keycloak-v8-common/java</source>
<source>${basedir}/src/keycloak-v9-common/java</source>
<source>${basedir}/src/keycloak-v10-common/java</source>
<source>${basedir}/src/keycloak-v6-v11/java</source>
<source>${basedir}/src/keycloak-v10/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>keycloak-v11</id>
<properties>
<keycloak.majorVersion>11</keycloak.majorVersion>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source-keycloak-v11</id>
<goals><goal>add-source</goal></goals>
<configuration>
<sources>
<source>${basedir}/src/keycloak-v6-common/java</source>
<source>${basedir}/src/keycloak-v8-common/java</source>
<source>${basedir}/src/keycloak-v9-common/java</source>
<source>${basedir}/src/keycloak-v10-common/java</source>
<source>${basedir}/src/keycloak-v11-common/java</source>
<source>${basedir}/src/keycloak-v6-v11/java</source>
<source>${basedir}/src/keycloak-v11/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>keycloak-v12</id>
<properties>
<keycloak.majorVersion>12</keycloak.majorVersion>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source-keycloak-v12</id>
<goals><goal>add-source</goal></goals>
<configuration>
<sources>
<source>${basedir}/src/keycloak-v6-common/java</source>
<source>${basedir}/src/keycloak-v8-common/java</source>
<source>${basedir}/src/keycloak-v9-common/java</source>
<source>${basedir}/src/keycloak-v10-common/java</source>
<source>${basedir}/src/keycloak-v11-common/java</source>
<source>${basedir}/src/keycloak-v12-common/java</source>
<source>${basedir}/src/keycloak-v12/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>keycloak-v13</id>
<properties>
<keycloak.majorVersion>13</keycloak.majorVersion>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source-keycloak-v13</id>
<goals><goal>add-source</goal></goals>
<configuration>
<sources>
<source>${basedir}/src/keycloak-v6-common/java</source>
<source>${basedir}/src/keycloak-v8-common/java</source>
<source>${basedir}/src/keycloak-v9-common/java</source>
<source>${basedir}/src/keycloak-v10-common/java</source>
<source>${basedir}/src/keycloak-v11-common/java</source>
<source>${basedir}/src/keycloak-v12-common/java</source>
<source>${basedir}/src/keycloak-v13-common/java</source>
<source>${basedir}/src/keycloak-v13/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>keycloak-v14</id>
<properties>
<keycloak.majorVersion>14</keycloak.majorVersion>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source-keycloak-v14</id>
<goals><goal>add-source</goal></goals>
<configuration>
<sources>
<source>${basedir}/src/keycloak-v6-common/java</source>
<source>${basedir}/src/keycloak-v8-common/java</source>
<source>${basedir}/src/keycloak-v9-common/java</source>
<source>${basedir}/src/keycloak-v10-common/java</source>
<source>${basedir}/src/keycloak-v11-common/java</source>
<source>${basedir}/src/keycloak-v12-common/java</source>
<source>${basedir}/src/keycloak-v13-common/java</source>
<source>${basedir}/src/keycloak-v14-common/java</source>
<source>${basedir}/src/keycloak-v14/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>keycloak-v15</id>
<properties>
<keycloak.majorVersion>15</keycloak.majorVersion>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source-keycloak-v15</id>
<goals><goal>add-source</goal></goals>
<configuration>
<sources>
<source>${basedir}/src/keycloak-v6-common/java</source>
<source>${basedir}/src/keycloak-v8-common/java</source>
<source>${basedir}/src/keycloak-v9-common/java</source>
<source>${basedir}/src/keycloak-v10-common/java</source>
<source>${basedir}/src/keycloak-v11-common/java</source>
<source>${basedir}/src/keycloak-v12-common/java</source>
<source>${basedir}/src/keycloak-v13-common/java</source>
<source>${basedir}/src/keycloak-v14-common/java</source>
<source>${basedir}/src/keycloak-v15-common/java</source>
<source>${basedir}/src/keycloak-v15/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -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.");
}
}

View File

@ -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;
}
}

View File

@ -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.");
}
}

View File

@ -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;
}
}

View File

@ -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<RequiredCredentialModel> getRequiredCredentialsStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<GroupModel> getDefaultGroupsStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<ClientModel> getClientsStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<ClientModel> getClientsStream(Integer firstResult, Integer maxResults) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<ClientModel> getAlwaysDisplayInConsoleClientsStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<ClientModel> searchClientByClientIdStream(String clientId, Integer firstResult, Integer maxResults) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<AuthenticationFlowModel> getAuthenticationFlowsStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<AuthenticationExecutionModel> getAuthenticationExecutionsStream(String flowId) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<AuthenticatorConfigModel> getAuthenticatorConfigsStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<RequiredActionProviderModel> getRequiredActionProvidersStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<IdentityProviderModel> getIdentityProvidersStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<IdentityProviderMapperModel> getIdentityProviderMappersStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<IdentityProviderMapperModel> getIdentityProviderMappersByAliasStream(String brokerAlias) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<ComponentModel> getComponentsStream(String parentId, String providerType) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<ComponentModel> getComponentsStream(String parentId) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<ComponentModel> getComponentsStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<String> getEventsListenersStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<String> getEnabledEventTypesStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<String> getSupportedLocalesStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<GroupModel> getGroupsStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<GroupModel> getTopLevelGroupsStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<GroupModel> getTopLevelGroupsStream(Integer first, Integer max) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<GroupModel> searchForGroupByNameStream(String search, Integer first, Integer max) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<ClientScopeModel> getClientScopesStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void patchRealmLocalizationTexts(String locale, Map<String, String> localizationTexts) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public boolean removeRealmLocalizationTexts(String locale) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Map<String, Map<String, String>> getRealmLocalizationTexts() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Map<String, String> getRealmLocalizationTextsByLocale(String locale) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<ClientScopeModel> getDefaultClientScopesStream(boolean defaultScope) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<RoleModel> getRolesStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<RoleModel> getRolesStream(Integer firstResult, Integer maxResults) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<RoleModel> searchForRolesStream(String search, Integer first, Integer max) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<String> getDefaultRolesStream() {
throw new UnsupportedOperationException("Not supported yet.");
}
}

View File

@ -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<String> 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<String> policyDefinitions = new LinkedList<>();
// Iterate groups and collect policy strings
user.getGroupsStream().forEach(new Consumer<GroupModel>() {
@Override
public void accept(GroupModel group) {
logger.debugf("group: %s", group.getName());
group.getAttributeStream(groupAttribute).forEach(new Consumer<String>() {
@Override
public void accept(String policyString) {
logger.infof("adding group password policy: %s", policyString);
policyDefinitions.add(policyString);
}
});
}
});
return policyDefinitions;
}
}

View File

@ -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;
}
}

View File

@ -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<ClientInitialAccessModel> getClientInitialAccesses() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void decreaseRemainingCount(ClientInitialAccessModel clientInitialAccess) {
throw new UnsupportedOperationException("Not supported yet.");
}
}

View File

@ -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;
}
}

View File

@ -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<ClientModel> searchClientByAttributes(Map<String, String> attributes, Integer firstResult, Integer maxResults) {
throw new UnsupportedOperationException("Not supported yet.");
}
}

View File

@ -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;
}
}

View File

@ -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.");
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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<String> 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<String> 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;
}
}

View File

@ -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;
}
}

View File

@ -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.");
}
}

View File

@ -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;
}
}

View File

@ -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<ClientModel> getClients(Integer firstResult, Integer maxResults) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Long getClientsCount() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public List<ClientModel> getAlwaysDisplayInConsoleClients() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public List<ClientModel> 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<RoleModel> getRoles(Integer firstResult, Integer maxResults) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Set<RoleModel> searchForRoles(String search, Integer first, Integer max) {
throw new UnsupportedOperationException("Not supported yet.");
}
}

View File

@ -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;
}
}

View File

@ -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()");
}
}

View File

@ -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<String> 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() {
}
}

View File

@ -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<String> 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<String> 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<String> 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() {
}
}

View File

@ -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()");
}
}

View File

@ -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<String> findPolicies(RealmModel realm, UserModel user);
protected abstract List<String> 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<PasswordPolicyProvider> list = new LinkedList<>();
PasswordPolicy parsedPolicy = PasswordPolicy.parse(session, policy);
return parsedPolicy;
}

View File

@ -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);
}
}