keycloak-memoizeit
Changes
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java 6(+6 -0)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccessBean.java 85(+85 -0)
forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties 35(+35 -0)
forms/common-themes/src/main/resources/theme/base/account/messages/messages_it.properties 103(+103 -0)
forms/common-themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties 1(+1 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html 7(+3 -4)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/pagenotfound.html 7(+7 -0)
forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties 175(+175 -0)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java 7(+5 -2)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java 12(+5 -7)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java 12(+12 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java 12(+12 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java 33(+33 -0)
model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentProtocolMapperEntity.java 115(+115 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java 11(+11 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java 13(+12 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java 30(+29 -1)
Details
diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml
index 2a066d0..fbea205 100755
--- a/connections/jpa/src/main/resources/META-INF/persistence.xml
+++ b/connections/jpa/src/main/resources/META-INF/persistence.xml
@@ -20,6 +20,9 @@
<class>org.keycloak.models.jpa.entities.IdentityProviderMapperEntity</class>
<class>org.keycloak.models.jpa.entities.ClientIdentityProviderMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ProtocolMapperEntity</class>
+ <class>org.keycloak.models.jpa.entities.GrantedConsentEntity</class>
+ <class>org.keycloak.models.jpa.entities.GrantedConsentRoleEntity</class>
+ <class>org.keycloak.models.jpa.entities.GrantedConsentProtocolMapperEntity</class>
<!-- JpaUserSessionProvider -->
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionEntity</class>
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml
index aa78acb..a1698dc 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml
@@ -35,10 +35,43 @@
<addColumn tableName="CREDENTIAL">
<column name="CREATED_DATE" type="BIGINT"/>
</addColumn>
+ <createTable tableName="GRANTED_CONSENT">
+ <column name="ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="CLIENT_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="USER_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+ <createTable tableName="GRANTED_CONSENT_ROLE">
+ <column name="GRANTED_CONSENT_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="ROLE_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+ <createTable tableName="GRANTED_CONSENT_PROT_MAPPER">
+ <column name="GRANTED_CONSENT_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="PROTOCOL_MAPPER_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_IDPM" tableName="IDENTITY_PROVIDER_MAPPER"/>
<addPrimaryKey columnNames="IDP_MAPPER_ID, NAME" constraintName="CONSTRAINT_IDPMConfig" tableName="IDP_MAPPER_CONFIG"/>
+ <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_GRNTCSNT_PM" tableName="GRANTED_CONSENT"/>
+ <addPrimaryKey columnNames="GRANTED_CONSENT_ID, ROLE_ID" constraintName="CONSTRAINT_GRNTCSNT_ROLE_PM" tableName="GRANTED_CONSENT_ROLE"/>
+ <addPrimaryKey columnNames="GRANTED_CONSENT_ID, PROTOCOL_MAPPER_ID" constraintName="CONSTRAINT_GRNTCSNT_PRM_PM" tableName="GRANTED_CONSENT_PROT_MAPPER"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="IDENTITY_PROVIDER_MAPPER" constraintName="FK_IDPM_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
<addForeignKeyConstraint baseColumnNames="IDP_MAPPER_ID" baseTableName="IDP_MAPPER_CONFIG" constraintName="FK_IDPMConfig" referencedColumnNames="ID" referencedTableName="IDENTITY_PROVIDER_MAPPER"/>
+ <addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="GRANTED_CONSENT" constraintName="FK_GRNTCSNT_USER" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
+ <addForeignKeyConstraint baseColumnNames="GRANTED_CONSENT_ID" baseTableName="GRANTED_CONSENT_ROLE" constraintName="FK_GRNTCSNT_ROLE_GR" referencedColumnNames="ID" referencedTableName="GRANTED_CONSENT"/>
+ <addForeignKeyConstraint baseColumnNames="GRANTED_CONSENT_ID" baseTableName="GRANTED_CONSENT_PROT_MAPPER" constraintName="FK_GRNTCSNT_PRM_GR" referencedColumnNames="ID" referencedTableName="GRANTED_CONSENT"/>
<addColumn tableName="CLIENT">
<column name="CONSENT_REQUIRED" type="BOOLEAN" defaultValueBoolean="false">
@@ -68,6 +101,7 @@
<dropUniqueConstraint tableName="KEYCLOAK_ROLE" constraintName="UK_J3RWUVD56ONTGSUHOGM184WW2"/>
<addUniqueConstraint columnNames="NAME,CLIENT_REALM_CONSTRAINT" constraintName="UK_J3RWUVD56ONTGSUHOGM184WW2-2" tableName="KEYCLOAK_ROLE"/>
+ <addUniqueConstraint columnNames="CLIENT_ID, USER_ID" constraintName="UK_JKUWUVD56ONTGSUHOGM8UEWRT" tableName="GRANTED_CONSENT"/>
</changeSet>
</databaseChangeLog>
diff --git a/core/src/main/java/org/keycloak/util/UriUtils.java b/core/src/main/java/org/keycloak/util/UriUtils.java
index f9687a5..775729a 100755
--- a/core/src/main/java/org/keycloak/util/UriUtils.java
+++ b/core/src/main/java/org/keycloak/util/UriUtils.java
@@ -18,7 +18,8 @@ public class UriUtils {
public static String getOrigin(String uri) {
String u = uri.toString();
- return u.substring(0, u.indexOf('/', 8));
+ int e = u.indexOf('/', 8);
+ return e != -1 ? u.substring(0, u.indexOf('/', 8)) : u;
}
public static boolean isOrigin(String url) {
diff --git a/docbook/reference/en/en-US/modules/direct-access.xml b/docbook/reference/en/en-US/modules/direct-access.xml
index 39b0e5c..ee67ced 100755
--- a/docbook/reference/en/en-US/modules/direct-access.xml
+++ b/docbook/reference/en/en-US/modules/direct-access.xml
@@ -80,14 +80,14 @@ HttpClient client = new HttpClientBuilder()
try {
HttpPost post = new HttpPost(
KeycloakUriBuilder.fromUri("http://localhost:8080/auth")
- .path(ServiceUrlConstants.TOKEN_SERVICE_DIRECT_GRANT_PATH).build("demo"));
+ .path(ServiceUrlConstants.TOKEN_PATH).build("demo"));
List <NameValuePair> formparams = new ArrayList <NameValuePair>();
+ formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "password"));
formparams.add(new BasicNameValuePair("username", "bburke"));
formparams.add(new BasicNameValuePair("password", "password"));
if (isPublic()) { // if client is public access type
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "customer-portal"));
- formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "password"));
} else {
String authorization = BasicAuthHelper.createHeader("customer-portal", "secret-secret-secret");
post.setHeader("Authorization", authorization);
diff --git a/docbook/reference/en/en-US/modules/kerberos.xml b/docbook/reference/en/en-US/modules/kerberos.xml
index 31c8d10..c4ff1be 100644
--- a/docbook/reference/en/en-US/modules/kerberos.xml
+++ b/docbook/reference/en/en-US/modules/kerberos.xml
@@ -215,10 +215,11 @@ ktadd -k /tmp/http.keytab HTTP/www.mydomain.org@MYDOMAIN.ORG
The scenario is supported by Keycloak, but there is tricky thing that SPNEGO authentication is done by Keycloak server but
GSS credential will need to be used by your application. So you need to enable built-in <literal>gss delegation credential</literal> protocol mapper
in admin console for your application. This will cause that Keycloak will deserialize GSS credential and transmit it to the application
- in access token. Application will need to deserialize it and use it for further GSS calls against other services.
+ in access token. Application will need to deserialize it and use it for further GSS calls against other services. We have an example, which is showing it in details. It's in <literal>examples/kerberos</literal>
+ in the Keycloak appliance distribution or WAR distribution download. You can also check the example sources directly <ulink url="https://github.com/keycloak/keycloak/blob/master/examples/kerberos">here</ulink> .
</para>
<para>
- GSSContext will need to
+ Once you deserialize the credential from the access token to the GSSCredential object, then GSSContext will need to
be created with this credential passed to the method <literal>GSSManager.createContext</literal> for example like this:
<programlisting><![CDATA[
GSSContext context = gssManager.createContext(serviceName, krb5Oid,
@@ -227,7 +228,7 @@ GSSContext context = gssManager.createContext(serviceName, krb5Oid,
</para>
<para>
Note that you also need to configure <literal>forwardable</literal> kerberos tickets in <literal>krb5.conf</literal> file
- and add support for delegated credentials to your browser. See the kerberos example from Keycloak example set for details.
+ and add support for delegated credentials to your browser. For details, see the kerberos example from Keycloak examples set as mentioned above.
</para>
<warning>
<para>
diff --git a/docbook/reference/en/en-US/modules/security-vulnerabilities.xml b/docbook/reference/en/en-US/modules/security-vulnerabilities.xml
index 0756ebd..0c4bee3 100755
--- a/docbook/reference/en/en-US/modules/security-vulnerabilities.xml
+++ b/docbook/reference/en/en-US/modules/security-vulnerabilities.xml
@@ -128,9 +128,10 @@
<para>
In the admin console, per realm, you can set up a password policy to enforce that users pick hard to guess passwords.
A password has to match all policies. The password policies that can be configured are hash iterations, length, digits,
- lowercase, uppercase, special characters, not username, regex patterns and expired passwords. Expired Passwords policy
+ lowercase, uppercase, special characters, not username, regex patterns, password history and force expired password update.
+ Force expired password update policy forces or requires password updates after specified span of time. Password history policy
restricts a user from resetting his password to N old expired passwords. Multiple regex patterns, separated by comma,
- can be specified. If there's more than one regex added, password has to match all fully.
+ can be specified in regex pattern policy. If there's more than one regex added, password has to match all fully.
Increasing number of Hash Iterations (n) does not worsen anything (and certainly not the cipher) and it greatly increases the
resistance to dictionary attacks. However the drawback to increasing n is that it has some cost (CPU usage, energy, delay) for
the legitimate parties. Increasing n also slightly increases the odds that a random password gives the same result as the right
diff --git a/docbook/reference/en/en-US/modules/server-installation.xml b/docbook/reference/en/en-US/modules/server-installation.xml
index 9667a7b..223c330 100755
--- a/docbook/reference/en/en-US/modules/server-installation.xml
+++ b/docbook/reference/en/en-US/modules/server-installation.xml
@@ -826,4 +826,36 @@ All configuration options are optional. Default value for directory is <literal>
<link linkend='themes'>Themes</link> sections for more information on how to do this.
</para>
</section>
+
+ <section>
+ <title>Installing Keycloak Server as Root Context</title>
+ <para>
+ The Keycloak server can be installed as the default web application. This way, instead of referencing
+ the server as <literal>http://mydomain.com/auth</literal>, it would be
+ <literal>http://mydomain.com/</literal>.
+ </para>
+ <para>
+ To do this, you need to add a <literal>default-web-module</literal> attribute in the Undertow subystem in standalone.xml.
+ <programlisting><![CDATA[
+<subsystem xmlns="urn:jboss:domain:undertow:1.2">
+ <server name="default-server">
+ <host name="default-host" alias="localhost" default-web-module="main-auth-server.war">
+ <location name="/" handler="welcome-content"/>
+ </host>
+]]></programlisting>
+ </para>
+ <para>
+ <literal>main-auth-server</literal> is the name of the Keycloak server as defined in the Keycloak subsystem.
+ </para>
+ <para>
+ <note>
+ If you have already run your server before changing to the root context then your database
+ will contain references to the old /auth context. And, your clients may also have incorrect
+ references. To fix this on the server side, you will need to <link linkend="export-import">export
+ your database to json, make corrections, and then import.</link> Client-side keycloak.json
+ files will need to be updated manually as well.
+ </note>
+ </para>
+
+ </section>
</chapter>
diff --git a/events/api/src/main/java/org/keycloak/events/Details.java b/events/api/src/main/java/org/keycloak/events/Details.java
index 17a24cf..cee7475 100755
--- a/events/api/src/main/java/org/keycloak/events/Details.java
+++ b/events/api/src/main/java/org/keycloak/events/Details.java
@@ -23,5 +23,6 @@ public interface Details {
String UPDATED_REFRESH_TOKEN_ID = "updated_refresh_token_id";
String NODE_HOST = "node_host";
String REASON = "reason";
+ String REVOKED_CLIENT = "revoked_client";
}
diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java
index 680a06e..dc40379 100755
--- a/events/api/src/main/java/org/keycloak/events/EventType.java
+++ b/events/api/src/main/java/org/keycloak/events/EventType.java
@@ -39,6 +39,8 @@ public enum EventType {
REMOVE_TOTP(true),
REMOVE_TOTP_ERROR(true),
+ REVOKE_GRANT(true),
+
SEND_VERIFY_EMAIL(true),
SEND_VERIFY_EMAIL_ERROR(true),
SEND_RESET_PASSWORD(true),
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPIdentityStoreRegistry.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPIdentityStoreRegistry.java
index 22aa55a..b818ace 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPIdentityStoreRegistry.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPIdentityStoreRegistry.java
@@ -92,6 +92,9 @@ public class LDAPIdentityStoreRegistry {
case LDAPConstants.VENDOR_TIVOLI:
uniqueIdentifierAttributeName = "uniqueidentifier";
break;
+ case LDAPConstants.VENDOR_NOVELL_EDIRECTORY:
+ uniqueIdentifierAttributeName = "guid";
+ break;
case LDAPConstants.VENDOR_ACTIVE_DIRECTORY:
uniqueIdentifierAttributeName = LDAPConstants.OBJECT_GUID;
}
diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java
index d316574..3e236dd 100644
--- a/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java
+++ b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java
@@ -5,6 +5,6 @@ package org.keycloak.account;
*/
public enum AccountPages {
- ACCOUNT, PASSWORD, TOTP, FEDERATED_IDENTITY, LOG, SESSIONS;
+ ACCOUNT, PASSWORD, TOTP, FEDERATED_IDENTITY, LOG, SESSIONS, ACCESS;
}
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
index fe38f2c..ba95348 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
@@ -20,6 +20,7 @@ import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.keycloak.account.AccountPages;
import org.keycloak.account.AccountProvider;
+import org.keycloak.account.freemarker.model.AccessBean;
import org.keycloak.account.freemarker.model.AccountBean;
import org.keycloak.account.freemarker.model.AccountFederatedIdentityBean;
import org.keycloak.account.freemarker.model.FeaturesBean;
@@ -37,6 +38,7 @@ import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.LocaleHelper;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
+import org.keycloak.freemarker.beans.AdvancedMessageFormatterMethod;
import org.keycloak.freemarker.beans.LocaleBean;
import org.keycloak.freemarker.beans.MessageBean;
import org.keycloak.freemarker.beans.MessageFormatterMethod;
@@ -183,6 +185,10 @@ public class FreeMarkerAccountProvider implements AccountProvider {
case SESSIONS:
attributes.put("sessions", new SessionsBean(realm, sessions));
break;
+ case ACCESS:
+ attributes.put("access", new AccessBean(realm, user, uriInfo.getBaseUri(), stateChecker));
+ attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
+ break;
case PASSWORD:
attributes.put("password", new PasswordBean(passwordSet));
}
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccessBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccessBean.java
new file mode 100644
index 0000000..58cea2f
--- /dev/null
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccessBean.java
@@ -0,0 +1,85 @@
+package org.keycloak.account.freemarker.model;
+
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.GrantedConsentModel;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.util.MultivaluedHashMap;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AccessBean {
+
+ private List<ClientGrantBean> clientGrants = new LinkedList<ClientGrantBean>();
+
+ public AccessBean(RealmModel realm, UserModel user, URI baseUri, String stateChecker) {
+ List<GrantedConsentModel> grantedConsents = user.getGrantedConsents();
+ for (GrantedConsentModel consent : grantedConsents) {
+ ClientModel client = realm.getClientById(consent.getClientId());
+
+ List<RoleModel> realmRolesGranted = new LinkedList<RoleModel>();
+ MultivaluedHashMap<String, RoleModel> resourceRolesGranted = new MultivaluedHashMap<String, RoleModel>();
+ for (String roleId : consent.getGrantedRoles()) {
+ RoleModel role = realm.getRoleById(roleId);
+ if (role.getContainer() instanceof RealmModel) {
+ realmRolesGranted.add(role);
+ } else {
+ resourceRolesGranted.add(((ClientModel) role.getContainer()).getClientId(), role);
+ }
+ }
+
+ List<String> claimsGranted = new LinkedList<String>();
+ for (String protocolMapperId : consent.getGrantedProtocolMappers()) {
+ ProtocolMapperModel protocolMapper = client.getProtocolMapperById(protocolMapperId);
+ claimsGranted.add(protocolMapper.getConsentText());
+ }
+
+ ClientGrantBean clientGrant = new ClientGrantBean(realmRolesGranted, resourceRolesGranted, client, claimsGranted);
+ clientGrants.add(clientGrant);
+ }
+ }
+
+ public List<ClientGrantBean> getClientGrants() {
+ return clientGrants;
+ }
+
+ public static class ClientGrantBean {
+
+ private final List<RoleModel> realmRolesGranted;
+ private final MultivaluedHashMap<String, RoleModel> resourceRolesGranted;
+ private final ClientModel client;
+ private final List<String> claimsGranted;
+
+ public ClientGrantBean(List<RoleModel> realmRolesGranted, MultivaluedHashMap<String, RoleModel> resourceRolesGranted,
+ ClientModel client, List<String> claimsGranted) {
+ this.realmRolesGranted = realmRolesGranted;
+ this.resourceRolesGranted = resourceRolesGranted;
+ this.client = client;
+ this.claimsGranted = claimsGranted;
+ }
+
+ public List<RoleModel> getRealmRolesGranted() {
+ return realmRolesGranted;
+ }
+
+ public MultivaluedHashMap<String, RoleModel> getResourceRolesGranted() {
+ return resourceRolesGranted;
+ }
+
+ public ClientModel getClient() {
+ return client;
+ }
+
+ public List<String> getClaimsGranted() {
+ return claimsGranted;
+ }
+
+ }
+}
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java
index 001ffde..40ea59f 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java
@@ -59,6 +59,10 @@ public class UrlBean {
return Urls.accountSessionsLogoutPage(baseQueryURI, realm, stateChecker).toString();
}
+ public String getRevokeClientUrl() {
+ return Urls.accountRevokeClientPage(baseQueryURI, realm).toString();
+ }
+
public String getTotpRemoveUrl() {
return Urls.accountTotpRemove(baseQueryURI, realm, stateChecker).toString();
}
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java
index 1d33f8a..847a3e6 100644
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java
@@ -21,6 +21,8 @@ public class Templates {
return "log.ftl";
case SESSIONS:
return "sessions.ftl";
+ case ACCESS:
+ return "access.ftl";
default:
throw new IllegalArgumentException();
}
diff --git a/forms/common-themes/src/main/resources/theme/base/account/access.ftl b/forms/common-themes/src/main/resources/theme/base/account/access.ftl
new file mode 100755
index 0000000..dc3cc41
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/account/access.ftl
@@ -0,0 +1,55 @@
+<#import "template.ftl" as layout>
+<@layout.mainLayout active='access' bodyClass='access'; section>
+
+ <div class="row">
+ <div class="col-md-10">
+ <h2>${msg("accessHtmlTitle")}</h2>
+ </div>
+ </div>
+
+ <form action="${url.revokeClientUrl}" method="post">
+ <input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
+
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <td>${msg("client")}</td>
+ <td>${msg("grantedPersonalInfo")}</td>
+ <td>${msg("grantedPermissions")}</td>
+ <td>${msg("action")}</td>
+ </tr>
+ </thead>
+
+ <tbody>
+ <#list access.clientGrants as clientGrant>
+ <tr>
+ <td><#if clientGrant.client.baseUrl??><a href="${clientGrant.client.baseUrl}">${clientGrant.client.clientId}</a><#else>${clientGrant.client.clientId}</#if></td>
+ <td>
+ <#list clientGrant.claimsGranted as claim>
+ ${advancedMsg(claim)}<#if claim_has_next>, </#if>
+ </#list>
+ </td>
+ <td>
+ <#list clientGrant.realmRolesGranted as role>
+ <#if role.description??>${advancedMsg(role.description)}<#else>${advancedMsg(role.name)}</#if>
+ <#if role_has_next>, </#if>
+ </#list>
+ <#list clientGrant.resourceRolesGranted?keys as resource>
+ <#if clientGrant.realmRolesGranted?has_content>, </#if>
+ <#list clientGrant.resourceRolesGranted[resource] as role>
+ <#if role.description??>${advancedMsg(role.description)}<#else>${advancedMsg(role.name)}</#if>
+ ${msg("inResource", resource)}
+ <#if role_has_next>, </#if>
+ </#list>
+ </#list>
+ </td>
+ <td>
+ <button type='submit' class='btn btn-primary' id='revoke-${clientGrant.client.clientId}' name='clientId' value="${clientGrant.client.id}">${msg("revoke")}</button>
+ </td>
+ </tr>
+ </#list>
+ </tbody>
+ </table>
+ </form>
+
+</@layout.mainLayout>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_de.properties
index 27902dd..a846d54 100644
--- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_de.properties
+++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_de.properties
@@ -99,5 +99,6 @@ invalidPasswordRegexPatternMessage=Ung\u00FCltiges Passwort\: nicht Regex-Muster
invalidPasswordHistoryMessage=Ung\u00FCltiges Passwort: darf nicht gleich einem der letzten {0} Passwortgeschichte.
locale_de=Deutsch
-locale_en=Englisch
+locale_en=English
+locale_it=Italian
locale_pt-BR=Portugu\u00EAs (Brasil)
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
index 713904b..9749f25 100755
--- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -12,20 +12,45 @@ changePasswordHtmlTitle=Change Password
sessionsHtmlTitle=Sessions
accountManagementTitle=Keycloak Account Management
authenticatorTitle=Authenticator
+accessHtmlTitle=Manage Granted Permissions
authenticatorCode=One-time code
email=Email
firstName=First name
+givenName=Given name
+fullName=Full name
lastName=Last name
+familyName=Family name
password=Password
passwordConfirm=Confirmation
passwordNew=New Password
username=Username
+address=Address
street=Street
locality=City or Locality
region=State, Province, or Region
postal_code=Zip or Postal code
country=Country
+emailVerified=Email verified
+gssDelegationCredential=gss delegation credential
+
+role_admin=Admin
+role_realm-admin=Realm Admin
+role_create-realm=Create realm
+role_view-realm=View realm
+role_view-users=View users
+role_view-applications=View applications
+role_view-clients=View clients
+role_view-events=View events
+role_view-identity-providers=View identity providers
+role_manage-realm=Manage realm
+role_manage-users=Manage users
+role_manage-applications=Manage applications
+role_manage-identity-providers=Manage identity providers
+role_manage-clients=Manage clients
+role_manage-events=Manage events
+role_view-profile=View profile
+
requiredFields=Required fields
allFieldsRequired=All fields required
@@ -49,6 +74,13 @@ federatedIdentity=Federated Identity
authenticator=Authenticator
sessions=Sessions
log=Log
+access=Access
+
+grantedPersonalInfo=Granted Personal Info
+grantedPermissions=Granted Permissions
+action=Action
+inResource=in <strong>{0}</strong>
+revoke=Revoke Access
configureAuthenticators=Configured Authenticators
mobile=Mobile
@@ -74,6 +106,8 @@ readOnlyPasswordMessage=You can''t update your password as your account is read
successTotpMessage=Mobile authenticator configured.
successTotpRemovedMessage=Mobile authenticator removed.
+successGrantRevokedMessage=Access revoked successfully.
+
accountUpdatedMessage=Your account has been updated.
accountPasswordUpdatedMessage=Your password has been updated.
@@ -99,4 +133,5 @@ invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last
locale_de=German
locale_en=English
+locale_it=Italian
locale_pt-BR=Portugu\u00EAs (Brasil)
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_it.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_it.properties
new file mode 100755
index 0000000..d22b935
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_it.properties
@@ -0,0 +1,103 @@
+doSave=Salva
+doCancel=Annulla
+doLogOutAllSessions=Effettua Log out in tutte le sessioni
+doRemove=Elimina
+doAdd=Aggiungi
+doSignOut=Sign Out
+
+editAccountHtmlTtile=Gestisci Account
+federatedIdentitiesHtmlTitle=Federated Identities
+accountLogHtmlTitle=Account Log
+changePasswordHtmlTitle=Cambia Password
+sessionsHtmlTitle=Sessioni
+accountManagementTitle=Keycloak Account Management
+authenticatorTitle=Authenticator
+
+authenticatorCode=Codice One-time
+email=Email
+firstName=Nome
+lastName=Cognome
+password=Password
+passwordConfirm=Conferma Password
+passwordNew=Nuova Password
+username=Username
+street=Via
+locality=Citta'' o Localita''
+region=Stato, Provincia, o Regione
+postal_code=Cap
+country=Paese
+
+requiredFields=Campi obbligatori
+allFieldsRequired=Tutti campi obbligatori
+
+backToApplication=« Torna all''applicazione
+backTo=Torna a {0}
+
+date=Data
+event=Evento
+ip=IP
+client=Client
+clients=Client
+details=Dettagli
+started=Iniziato
+lastAccess=Ultimo Accesso
+expires=Scade
+applications=Applicazioni
+
+account=Account
+federatedIdentity=Federated Identity
+authenticator=Authenticator
+sessions=Sessioni
+log=Log
+
+configureAuthenticators=Authenticator configurati
+mobile=Mobile
+totpStep1=Installa <a href="https://fedorahosted.org/freeotp/" target="_blank">FreeOTP</a> o <a href="http://code.google.com/p/google-authenticator/" target="_blank">Google Authenticator</a> sul tuo dispositivo mobile.
+totpStep2=Apri l''applicazione e scansiona il barcode o scrivi la chiave.
+totpStep3=Scrivi il codice one-time fornito dall''applicazione e clicca Salva per completare il setup.
+
+missingFirstNameMessage=Inserisci il nome.
+invalidEmailMessage=Invalid email address.
+missingLastNameMessage=Inserisci il cognome.
+missingEmailMessage=Inserisci l''indirizzo email.
+missingPasswordMessage=Inserisci la password.
+notMatchPasswordMessage=La password non coincide.
+
+missingTotpMessage=Inserisci il codice di autenticazione.
+invalidPasswordExistingMessage=Password esistente non valida.
+invalidPasswordConfirmMessage=La password di conferma non coincide.
+invalidTotpMessage=Codice di autenticazione non valido.
+
+readOnlyUserMessage=Non puoi aggiornare il tuo account dal momento che e'' in sola lettura.
+readOnlyPasswordMessage=Non puoi aggiornare la tua password dal momento che e'' in sola lettura.
+
+successTotpMessage=Mobile authenticator configurato.
+successTotpRemovedMessage=Mobile authenticator rliminato.
+
+accountUpdatedMessage=Il tuo account e'' stato aggiornato.
+accountPasswordUpdatedMessage=La tua password e'' stata aggiornata.
+
+missingIdentityProviderMessage=Identity provider non specificata.
+invalidFederatedIdentityActionMessage=Azione non valida o mancante.
+identityProviderNotFoundMessage=L''identity provider specificato non e'' stato trovato.
+federatedIdentityLinkNotActiveMessage=Questo identity non e'' piu'' attivo.
+federatedIdentityRemovingLastProviderMessage=Non puoi rimuovere l''ultimo federated identity dal momento che non hai piu'' la password.
+identityProviderRedirectErrorMessage=Fallito il redirect all''identity provider.
+identityProviderRemovedMessage=Identity provider eliminato con successo.
+
+accountDisabledMessage=Account disabilitato, contatta l''amministratore.
+
+accountTemporarilyDisabledMessage=L''account e'' temporaneamente disabilitato, contatta l''admin o riprova piu'' tardi.
+invalidPasswordMinLengthMessage=Password non valida: lunghezza minima {0}.
+invalidPasswordMinLowerCaseCharsMessage=Password non valida: deve contenere almeno {0} caratteri minuscoli.
+invalidPasswordMinDigitsMessage=Password non valida: deve contenere almeno {0} numeri.
+invalidPasswordMinUpperCaseCharsMessage=Password non valida: deve contenere almeno {0} caratteri maiuscoli.
+invalidPasswordMinSpecialCharsMessage=Password non valida: deve contenere almeno {0} caratteri speciali.
+invalidPasswordNotUsernameMessage=Password non valida: non deve essere uguale allo username.
+invalidPasswordRegexPatternMessage=Password non valida: fallito il match con una o piu'' espressioni regolari.
+invalidPasswordHistoryMessage=Password non valida: non deve ssere uguale ad una delle ultime {0} password.
+
+locale_de=German
+locale_en=English
+locale_it=Italian
+locale_pt-BR=Portugu\u00EAs (Brasil)
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties
index 52e0f4d..367be5f 100644
--- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties
+++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties
@@ -100,4 +100,5 @@ invalidPasswordHistoryMessage=Senha inv\u00E1lida\: n\u00E3o deve ser igual a qu
locale_de=Deutsch
locale_en=English
+locale_it=Italian
locale_pt-BR=Portugu\u00EAs (BR)
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/account/template.ftl b/forms/common-themes/src/main/resources/theme/base/account/template.ftl
index d461118..1319b6f 100644
--- a/forms/common-themes/src/main/resources/theme/base/account/template.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/account/template.ftl
@@ -56,6 +56,7 @@
<li class="<#if active=='totp'>active</#if>"><a href="${url.totpUrl}">${msg("authenticator")}</a></li>
<#if features.identityFederation><li class="<#if active=='social'>active</#if>"><a href="${url.socialUrl}">${msg("federatedIdentity")}</a></li></#if>
<li class="<#if active=='sessions'>active</#if>"><a href="${url.sessionsUrl}">${msg("sessions")}</a></li>
+ <li class="<#if active=='access'>active</#if>"><a href="${url.accessUrl}">${msg("access")}</a></li>
<#if features.log><li class="<#if active=='log'>active</#if>"><a href="${url.logUrl}">${msg("log")}</a></li></#if>
</ul>
</div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/index.ftl b/forms/common-themes/src/main/resources/theme/base/admin/index.ftl
index 20e21b7..3883361 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/index.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/admin/index.ftl
@@ -64,7 +64,7 @@
<div data-ng-include data-src="resourceUrl + '/partials/menu.html'"></div>
</header>
-<div class="container" data-ng-show="auth.hasAnyAccess">
+<div class="container">
<div data-ng-view id="view"></div>
<div id="loading" class="loading-backdrop">
<div class="loading">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index cffa8e0..e696714 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -960,8 +960,6 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'ProtocolListCtrl'
})
-
-
.when('/server-info', {
templateUrl : resourceUrl + '/partials/server-info.html'
})
@@ -969,8 +967,14 @@ module.config([ '$routeProvider', function($routeProvider) {
templateUrl : resourceUrl + '/partials/home.html',
controller : 'LogoutCtrl'
})
- .otherwise({
+ .when('/notfound', {
templateUrl : resourceUrl + '/partials/notfound.html'
+ })
+ .when('/forbidden', {
+ templateUrl : resourceUrl + '/partials/forbidden.html'
+ })
+ .otherwise({
+ templateUrl : resourceUrl + '/partials/pagenotfound.html'
});
} ]);
@@ -994,29 +998,6 @@ module.config(function($httpProvider) {
});
-module.factory('errorInterceptor', function($q, $window, $rootScope, $location, Notifications, Auth) {
- return function(promise) {
- return promise.then(function(response) {
- return response;
- }, function(response) {
- if (response.status == 401) {
- Auth.authz.logout();
- } else if (response.status == 403) {
- Notifications.error("Forbidden");
- } else if (response.status == 404) {
- Notifications.error("Not found");
- } else if (response.status) {
- if (response.data && response.data.errorMessage) {
- Notifications.error(response.data.errorMessage);
- } else {
- Notifications.error("An unexpected server error has occurred");
- }
- }
- return $q.reject(response);
- });
- };
-});
-
module.factory('spinnerInterceptor', function($q, $window, $rootScope, $location) {
return function(promise) {
return promise.then(function(response) {
@@ -1044,6 +1025,29 @@ module.factory('spinnerInterceptor', function($q, $window, $rootScope, $location
};
});
+module.factory('errorInterceptor', function($q, $window, $rootScope, $location, Notifications, Auth) {
+ return function(promise) {
+ return promise.then(function(response) {
+ return response;
+ }, function(response) {
+ if (response.status == 401) {
+ Auth.authz.logout();
+ } else if (response.status == 403) {
+ $location.path('/forbidden');
+ } else if (response.status == 404) {
+ $location.path('/notfound');
+ } else if (response.status) {
+ if (response.data && response.data.errorMessage) {
+ Notifications.error(response.data.errorMessage);
+ } else {
+ Notifications.error("An unexpected server error has occurred");
+ }
+ }
+ return $q.reject(response);
+ });
+ };
+});
+
// collapsable form fieldsets
module.directive('collapsable', function() {
return function(scope, element, attrs) {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index c2599a7..c5479e0 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -121,12 +121,8 @@ module.controller('RealmDropdownCtrl', function($scope, Realm, Current, Auth, $l
$scope.changeRealm = function(selectedRealm) {
$location.url("/realms/" + selectedRealm);
- };
-
- $scope.showNav = function() {
- var show = Current.realms.length > 0;
- return Auth.loggedIn && show;
}
+
$scope.refresh = function() {
Current.refresh();
}
@@ -399,7 +395,7 @@ module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serv
$scope.supportedLocalesOptions = {
'multiple' : true,
'simple_tags' : true,
- 'tags' : ['en', 'de', 'pt-BR']
+ 'tags' : ['en', 'de', 'pt-BR', 'it']
};
$scope.$watch('realm.supportedLocales', function(oldVal, newVal) {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index 30003bd..17fbb16 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -547,6 +547,7 @@ module.controller('LDAPCtrl', function($scope, $location, Notifications, Dialog,
{ "id": "ad", "name": "Active Directory" },
{ "id": "rhds", "name": "Red Hat Directory Server" },
{ "id": "tivoli", "name": "Tivoli" },
+ { "id": "edirectory", "name": "Novell eDirectory" },
{ "id": "other", "name": "Other" }
];
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
index 2bb4936..6703973 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -908,7 +908,8 @@ module.factory('PasswordPolicy', function() {
specialChars: "Minimal number (integer type) of special characters in password. Default value is 1.",
notUsername: "Block passwords that are equal to the username",
regexPatterns: "Block passwords that do not match all of the regex patterns (string type).",
- passwordHistory: "Block passwords that are equal to previous passwords. Default value is 3."
+ passwordHistory: "Block passwords that are equal to previous passwords. Default value is 3.",
+ forceExpiredPasswordChange: "Force password change when password credential is expired. Default value is 365 days."
}
p.allPolicies = [
@@ -920,7 +921,8 @@ module.factory('PasswordPolicy', function() {
{ name: 'specialChars', value: 1 },
{ name: 'notUsername', value: 1 },
{ name: 'regexPatterns', value: ''},
- { name: 'passwordHistory', value: 3 }
+ { name: 'passwordHistory', value: 3 },
+ { name: 'forceExpiredPasswordChange', value: 365 }
];
p.parse = function(policyString) {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index aa3d9ba..ebfca94 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -178,12 +178,11 @@
<span tooltip-placement="right" tooltip="Valid URI pattern a browser can redirect to after a successful login or logout. Simple wildcards are allowed i.e. 'http://example.com/*'. Relative path can be specified too i.e. /my/relative/path/*. Relative paths will generate a redirect URI using the request's host and port. For SAML, you must set valid URI patterns if you are relying on the consumer service URL embedded with the login request." class="fa fa-info-circle"></span>
</div>
<div class="form-group" data-ng-show="!client.bearerOnly && !create">
- <label class="col-sm-2 control-label" for="baseUrl">Default Redirect URL</label>
+ <label class="col-sm-2 control-label" for="baseUrl">Base URL</label>
<div class="col-sm-6">
- <input class="form-control" type="text" name="baseUrl" id="baseUrl"
- data-ng-model="client.baseUrl">
+ <input class="form-control" type="text" name="baseUrl" id="baseUrl" data-ng-model="client.baseUrl">
</div>
- <span tooltip-placement="right" tooltip="Default URL to use when the auth server needs to redirect back to the client. This URL will also be used when the auth server needs to link to the client for any reason." class="fa fa-info-circle"></span>
+ <span tooltip-placement="right" tooltip="Default URL to use when the auth server needs to redirect or link back to the client." class="fa fa-info-circle"></span>
</div>
<div class="form-group" data-ng-hide="create || protocol == 'saml'">
<label class="col-sm-2 control-label" for="adminUrl">Admin URL</label>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/forbidden.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/forbidden.html
new file mode 100755
index 0000000..6a0d5bf
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/forbidden.html
@@ -0,0 +1,7 @@
+<div id="content-area" class="col-sm-12" role="main">
+ <div class="error-container">
+ <h2>Forbidden</h2>
+ <p class="instruction">You don't have access to the requested resource.</p>
+ <a href="#" class="link-right">Go to the home page »</a>
+ </div>
+</div>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/menu.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/menu.html
index c1062df..66f58c9 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/menu.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/menu.html
@@ -26,10 +26,10 @@
</li>
</ul>
<ul class="nav navbar-nav navbar-primary persistent-secondary" data-ng-controller="RealmDropdownCtrl">
- <li class="dropdown context" data-ng-show="showNav()">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+ <li class="dropdown context" data-ng-show="current.realm.realm">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">
{{current.realm.realm}}
- <b class="caret"></b>
+ <b class="caret" data-ng-show="current.realms.length > 1"></b>
</a>
<ul class="dropdown-menu" data-ng-show="current.realms.length > 1">
<li data-ng-repeat="realm in current.realms" data-ng-if="realm.realm != current.realm.realm">
@@ -37,6 +37,17 @@
</li>
</ul>
</li>
+ <li class="dropdown context" data-ng-show="!current.realm.realm">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+ Select realm...
+ <b class="caret"></b>
+ </a>
+ <ul class="dropdown-menu">
+ <li data-ng-repeat="realm in current.realms">
+ <a href="" ng-click="changeRealm(realm.realm)">{{realm.realm}}</a>
+ </li>
+ </ul>
+ </li>
<li class="active pull-right" data-ng-show="auth.user && access.createRealm">
<a class="button primary" href="#/create/realm" data-ng-class="path[0] == 'create' && path[1] == 'realm' && 'active'"
data-ng-show="auth.user">Add Realm</a>
@@ -45,6 +56,7 @@
</div>
</div>
</nav>
+
<!-- TODO remove once this page is properly styled -->
<style type="text/css">
.icon-spinner6 {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/notfound.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/notfound.html
index bdbb997..05652f0 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/notfound.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/notfound.html
@@ -1,14 +1,7 @@
-<div id="wrapper" class="container">
- <div class="row">
- <div class="bs-sidebar col-md-3 clearfix"></div>
- <div id="content-area" class="col-md-9" role="main">
- <div class="error-container">
- <h2>Page <strong>not found</strong>...</h2>
- <p class="instruction">We could not find the page you are looking for. Please make sure the URL you entered is correct.</p>
- <a href="#" class="link-right">Go to the home page »</a>
- <!-- <a href="#" class="link-right">Go to the realm page »</a> -->
- </div>
- </div>
- <div id="container-right-bg"></div>
+<div id="content-area" class="col-sm-12" role="main">
+ <div class="error-container">
+ <h2>Resource <strong>not found</strong>...</h2>
+ <p class="instruction">We could not find the resource you are looking for. Please make sure the URL you entered is correct.</p>
+ <a href="#" class="link-right">Go to the home page »</a>
</div>
</div>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/pagenotfound.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/pagenotfound.html
new file mode 100755
index 0000000..769d71e
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/pagenotfound.html
@@ -0,0 +1,7 @@
+<div id="content-area" class="col-sm-12" role="main">
+ <div class="error-container">
+ <h2>Page <strong>not found</strong>...</h2>
+ <p class="instruction">We could not find the page you are looking for. Please make sure the URL you entered is correct.</p>
+ <a href="#" class="link-right">Go to the home page »</a>
+ </div>
+</div>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/login/error.ftl b/forms/common-themes/src/main/resources/theme/base/login/error.ftl
index 2c8c153..95de521 100755
--- a/forms/common-themes/src/main/resources/theme/base/login/error.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/login/error.ftl
@@ -7,6 +7,9 @@
<#elseif section = "form">
<div id="kc-error-message">
<p class="instruction">${message.summary}</p>
+ <#if client?? && client.baseUrl?has_content>
+ <p><a href="${client.baseUrl}">${msg("backToApplication")}</a></p>
+ </#if>
</div>
</#if>
</@layout.registrationLayout>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-oauth-grant.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-oauth-grant.ftl
index 9b0637b..6136e8b 100755
--- a/forms/common-themes/src/main/resources/theme/base/login/login-oauth-grant.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/login/login-oauth-grant.ftl
@@ -1,4 +1,3 @@
-<#-- TODO: Only a placeholder, implementation needed -->
<#import "template.ftl" as layout>
<@layout.registrationLayout bodyClass="oauth"; section>
<#if section = "title">
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties
index 9de1c21..ca45f53 100644
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties
@@ -174,5 +174,6 @@ clientNotFoundMessage=Client not found.
emailVerifiedMessage=Ihr E-Mail Adresse wurde erfolgreich verifiziert.
locale_de=Deutsch
-locale_en=Englisch
+locale_en=English
+locale_it=Italian
locale_pt-BR=Portugu\u00EAs (Brasil)
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 6d34f4e..6122533 100755
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -157,8 +157,7 @@ couldNotSendAuthenticationRequestMessage=Could not send authentication request t
unexpectedErrorHandlingRequestMessage=Unexpected error when handling authentication request to identity provider [{0}].
invalidAccessCodeMessage=Invalid access code.
sessionNotActiveMessage=Session not active.
-unknownCodeMessage=Unknown code, please login again through your application.
-invalidCodeMessage=Invalid code, please login again through your application.
+invalidCodeMessage=An error occurred, please login again through your application.
identityProviderUnexpectedErrorMessage=Unexpected error when authenticating with identity provider
identityProviderNotFoundMessage=Could not find an identity provider with the identifier [{0}].
realmSupportsNoCredentialsMessage=Realm [{0}] does not support any credential type.
@@ -167,6 +166,7 @@ emailVerifiedMessage=Your email address has been verified.
locale_de=German
locale_en=English
+locale_it=Italian
locale_pt-BR=Portugu\u00EAs (Brasil)
backToApplication=« Back to Application
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties
new file mode 100755
index 0000000..f7296b9
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties
@@ -0,0 +1,175 @@
+doLogIn=Accedi
+doRegister=Registrati
+doCancel=Annulla
+doSubmit=Invia
+doYes=Si
+doNo=No
+doForgotPassword=Password Dimenticata?
+doClickHere=Clicca qui
+
+registerWithTitle=Registrati come {0}
+registerWithTitleHtml=Registrati come <strong>{0}</strong>
+loginTitle=Accedi a {0}
+loginTitleHtml=Accedi a <strong>{0}</strong>
+loginTotpTitle=Configura Autenticazione Mobile
+loginProfileTitle=Aggiorna Profilo
+oauthGrantTitle=OAuth Grant
+oauthGrantTitleHtml=Accesso temporaneo per <strong>{0}</strong> richiesto da <strong>{1}</strong>.
+errorTitle=Siamo spiacenti...
+errorTitleHtml=Siamo <strong>spiacenti</strong> ...
+emailVerifyTitle=Verifica Email
+emailForgotTitle=Password Dimenticata?
+updatePasswordTitle=Modifica Password
+codeSuccessTitle=Codice di Successo
+codeErrorTitle=Codice di Errore\: {0}
+
+noAccount=Nuovo Utente?
+username=Username
+usernameOrEmail=Username o email
+firstName=Nome
+givenName=Nome
+fullName=Nome Completo
+lastName=Cognome
+familyName=Cognome
+email=Email
+password=Password
+passwordConfirm=Conferma password
+passwordNew=Nuova Password
+passwordNewConfirm=Conferma nuova password
+rememberMe=Ricordami
+authenticatorCode=Codice One-time
+address=Indirizzo
+street=Via
+locality=Citta'' o Localita''
+region=Stato, Provincia, o Regione
+postal_code=Cap
+country=Paese
+emailVerified=Email verificata
+gssDelegationCredential=credenziali gss delegation
+
+loginTotpStep1=Installa <a href="https://fedorahosted.org/freeotp/" target="_blank">FreeOTP</a> or <a href="http://code.google.com/p/google-authenticator/" target="_blank">Google Authenticator</a> sul tuo dispositivo mobile
+loginTotpStep2=Apri l''applicazione e scansione il barcode o scrivi la chiave
+loginTotpStep3=Scrivi il codice one-time fornito dall''applicazione e premi Invia per finire il setup
+loginTotpOneTime=Codice one-time
+
+oauthGrantRequest=Vuoi assegnare questi privilegi di accesso?
+inResource=per <strong>{0}</strong>
+
+emailVerifyInstruction1=Ti e'' stata inviata una email con le istruzioni per la verifica della tua email.
+emailVerifyInstruction2=Non hai ricevuto un codice di verifica nella tua email?
+emailVerifyInstruction3=per reinviare la mail.
+
+backToLogin=« Torna al Login
+
+emailInstruction=Scrivi il tuo username o indirizzo email e noi ti invieremo le istruzioni per creare una nuova password.
+
+copyCodeInstruction=Copiaquesto codice e incollalo nella tua applicazione:
+
+personalInfo=Informazioni personali:
+
+role_admin=Admin
+role_realm-admin=Realm Admin
+role_create-realm=Crea realm
+role_view-realm=Visualizza realm
+role_view-users=Visualizza utenti
+role_view-applications=Visualizza applicazioni
+role_view-clients=Visualizza client
+role_view-events=Visualizza eventi
+role_view-identity-providers=Visualizza identity provider
+role_manage-realm=Gestisci realm
+role_manage-users=Gestisci utenti
+role_manage-applications=Gestisci applicazioni
+role_manage-identity-providers=Gestisci identity provider
+role_manage-clients=Gestisci client
+role_manage-events=Gestisci eventi
+role_view-profile=Visualizza profilo
+role_manage-account=Gestisci account
+
+invalidUserMessage=Username o password non valida.
+invalidEmailMessage=Indirizzo email non valido.
+accountDisabledMessage=Account disabilitato, contatta l''admin.
+accountTemporarilyDisabledMessage=Account temporaneamente disabilitato, contatta l''admin o riprova piu'' tardi.
+expiredCodeMessage=Login timeout. Effettua login di nuovo.
+
+missingFirstNameMessage=Inserisci il nome.
+missingLastNameMessage=Inserisci il cognome.
+missingEmailMessage=Inserisci l''indirizzo email.
+missingUsernameMessage=Inserisci lo username.
+missingPasswordMessage=Inserisci la password.
+missingTotpMessage=Inserisci il codice di autenticazione.
+notMatchPasswordMessage=Le passwords non coincidono.
+
+invalidPasswordExistingMessage=Password esistente non valida.
+invalidPasswordConfirmMessage=La password di conferma non coincide.
+invalidTotpMessage=Codice di autenticazione non valido.
+
+usernameExistsMessage=Username gia'' esistente.
+emailExistsMessage=Email gia'' esistente.
+
+federatedIdentityEmailExistsMessage=Utente con email gia'' esistente. Effettua il login nella gestione account per associare l''account.
+federatedIdentityUsernameExistsMessage=Utente con username gia'' esistente.Effettua il login nella gestione account per associare l''account.
+
+configureTotpMessage=Devi impostare un Mobile Authenticator per attivare il tuo account.
+updateProfileMessage=Devi aggiornare il tuo profilo utente per attivare il tuo account.
+updatePasswordMessage=Devi cambiare la password per attivare il tuo account.
+verifyEmailMessage=Devi verificare il tuo indirizzo email per attivare il tuo account.
+
+emailSentMessage=Riceverai a breve una email con maggiori istruzioni.
+emailSendErrorMessage=Invio email fallito, riptoav piu'' tardi.
+
+accountUpdatedMessage=Il tuo account e'' stato aggiornato.
+accountPasswordUpdatedMessage=La tua password e'' stata aggiornata.
+
+noAccessMessage=Nessun accesso
+
+invalidPasswordMinLengthMessage=Password non valida: lunghezza minima {0}.
+invalidPasswordMinDigitsMessage=Password non valida: deve contenere almeno {0} numeri.
+invalidPasswordMinLowerCaseCharsMessage=Password non valida: deve contenere almeno {0} caratteri minuscoli.
+invalidPasswordMinUpperCaseCharsMessage=Password non valida: deve contenere almeno {0} caratteri maiuscoli.
+invalidPasswordMinSpecialCharsMessage=Password non valida: deve contenere almeno {0} caratteri speciali.
+invalidPasswordNotUsernameMessage=Password non valida: non deve essere uguale allo username.
+invalidPasswordRegexPatternMessage=Password non valida: fallito il match con una o piu'' espressioni regolari.
+invalidPasswordHistoryMessage=Password non valida: non deve ssere uguale ad una delle ultime {0} password.
+
+failedToProcessResponseMessage=Fallimento nell''elaborazione della risposta
+httpsRequiredMessage=HTTPS richiesto
+realmNotEnabledMessage=Realm non abilitato
+invalidRequestMessage=Richiesta non valida
+unknownLoginRequesterMessage=Richiedente di Login non riconosciuto
+loginRequesterNotEnabledMessage=Richiedente di Login non abilitato
+bearerOnlyMessage=Alle applicazioni di tipo Bearer-only non e'' consentito di effettuare il login tramite browser
+directGrantsOnlyMessage=Ai client di tipo Direct-grants-only non e'' consentito di effettuare il login tramite browser
+invalidRedirectUriMessage=Redirect uri non valido
+unsupportedNameIdFormatMessage=NameIDFormat non supportato
+invlidRequesterMessage=Richiedente non valido
+registrationNotAllowedMessage=Registrazione non permessa
+
+permissionNotApprovedMessage=Permesso non approvato.
+noRelayStateInResponseMessage=Nessun relay state in risposta dall''identity provider [{0}].
+identityProviderAlreadyLinkedMessage=L''identita'' restituita dall''identity provider [{0}] e'' gia'' associata ad un altro utente.
+insufficientPermissionMessage=Permessi insufficienti per associare le identita''.
+couldNotProceedWithAuthenticationRequestMessage=Non posso procedere con la richiesta di autenticazione all''identity provider.
+couldNotObtainTokenMessage=Non posso ottenere un token dall''identity provider [{0}].
+unexpectedErrorRetrievingTokenMessage=Errore inaspettato nella gestione del token dall''identity provider [{0}].
+unexpectedErrorHandlingResponseMessage=Errore inaspettato nella gestione della risposta dall''identity provider [{0}].
+identityProviderAuthenticationFailedMessage=Autenticazione fallita. Non posso effettuare l''autenticazione con l''identity provider [{0}].
+couldNotSendAuthenticationRequestMessage=Non posso inviare la richiesta di autenticazione all''identity provider [{0}].
+unexpectedErrorHandlingRequestMessage=Errore imprevisto durante l''autenticazione con identity provider [{0}].
+invalidAccessCodeMessage=Codice di accesso non valido.
+sessionNotActiveMessage=Sessione non attiva.
+invalidCodeMessage=Si e'' verificato un errore, per piacere effettua di nuovo il login nella tua applicazione.
+identityProviderUnexpectedErrorMessage=Errore imprevisto durante l''autenticazione con identity provider
+identityProviderNotFoundMessage=Non posso trovare un identity provider con l''identificativo [{0}].
+realmSupportsNoCredentialsMessage=Il Realm [{0}] non supporta nessun tipo di credenziali.
+identityProviderNotUniqueMessage=Il Realm [{0}] supporta piu'' di un identity provider. Non posso determinare quale identity provider con il quale autenticarti.
+emailVerifiedMessage=Il tuo indirizzo email e'' stato verificato.
+
+locale_de=German
+locale_en=English
+locale_it=Italian
+locale_pt-BR=Portugu\u00EAs (Brasil)
+
+backToApplication=« Torna all''Applicazione
+missingParameterMessage=Parametri Mancanti\: {0}
+clientNotFoundMessage=Client non trovato.
+invalidParameterMessage=Parametro non valido\: {0}
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties
index e33f5aa..896f83f 100644
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties
@@ -167,6 +167,7 @@ emailVerifiedMessage=O seu endere\u00E7o de e-mail foi confirmado.
locale_de=Deutsch
locale_en=English
+locale_it=Italian
locale_pt-BR=Portugu\u00EAs (BR)
backToApplication=« Voltar para a aplica\u00E7\u00E3o
diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
index 8e5ef24..adfd3f8 100755
--- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
+++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
@@ -10,6 +10,7 @@ import javax.ws.rs.core.UriInfo;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
@@ -41,7 +42,7 @@ public interface LoginFormsProvider extends Provider {
public LoginFormsProvider setClientSessionCode(String accessCode);
- public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested);
+ public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested, List<ProtocolMapperModel> protocolMappers);
public LoginFormsProvider setAccessRequest(String message);
/**
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
index 7e5cb78..3c0f56f 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -32,6 +32,7 @@ import org.keycloak.login.freemarker.model.UrlBean;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
@@ -66,6 +67,7 @@ import java.util.concurrent.TimeUnit;
private Response.Status status;
private List<RoleModel> realmRolesRequested;
private MultivaluedMap<String, RoleModel> resourceRolesRequested;
+ private List<ProtocolMapperModel> protocolMappersRequested;
private MultivaluedMap<String, String> queryParams;
private Map<String, String> httpResponseHeaders = new HashMap<String, String>();
private String accessRequestMessage;
@@ -243,7 +245,7 @@ import java.util.concurrent.TimeUnit;
attributes.put("register", new RegisterBean(formData));
break;
case OAUTH_GRANT:
- attributes.put("oauth", new OAuthGrantBean(accessCode, clientSession, client, realmRolesRequested, resourceRolesRequested, this.accessRequestMessage));
+ attributes.put("oauth", new OAuthGrantBean(accessCode, clientSession, client, realmRolesRequested, resourceRolesRequested, protocolMappersRequested, this.accessRequestMessage));
attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
break;
case CODE:
@@ -366,9 +368,10 @@ import java.util.concurrent.TimeUnit;
}
@Override
- public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested) {
+ public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested, List<ProtocolMapperModel> protocolMappersRequested) {
this.realmRolesRequested = realmRolesRequested;
this.resourceRolesRequested = resourceRolesRequested;
+ this.protocolMappersRequested = protocolMappersRequested;
return this;
}
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java
index 431a606..98cd164 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java
@@ -42,20 +42,18 @@ public class OAuthGrantBean {
private ClientModel client;
private List<String> claimsRequested;
- public OAuthGrantBean(String code, ClientSessionModel clientSession, ClientModel client, List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested, String accessRequestMessage) {
+ public OAuthGrantBean(String code, ClientSessionModel clientSession, ClientModel client, List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested,
+ List<ProtocolMapperModel> protocolMappersRequested, String accessRequestMessage) {
this.code = code;
this.client = client;
this.realmRolesRequested = realmRolesRequested;
this.resourceRolesRequested = resourceRolesRequested;
this.accessRequestMessage = accessRequestMessage;
- // todo support locale
List<String> claims = new LinkedList<String>();
- if (clientSession != null) {
- for (ProtocolMapperModel model : client.getProtocolMappers()) {
- if (model.isConsentRequired() && model.getProtocol().equals(clientSession.getAuthMethod()) && model.getConsentText() != null) {
- claims.add(model.getConsentText());
- }
+ if (protocolMappersRequested != null) {
+ for (ProtocolMapperModel model : protocolMappersRequested) {
+ claims.add(model.getConsentText());
}
}
if (claims.size() > 0) this.claimsRequested = claims;
diff --git a/model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java b/model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java
index 5493fe2..08f8f90 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java
@@ -11,7 +11,7 @@ public class CredentialEntity {
private String device;
private byte[] salt;
private int hashIterations;
- private long createdDate;
+ private Long createdDate;
private UserEntity user;
@@ -63,11 +63,11 @@ public class CredentialEntity {
this.hashIterations = hashIterations;
}
- public long getCreatedDate() {
+ public Long getCreatedDate() {
return createdDate;
}
- public void setCreatedDate(long createdDate) {
+ public void setCreatedDate(Long createdDate) {
this.createdDate = createdDate;
}
diff --git a/model/api/src/main/java/org/keycloak/models/GrantedConsentModel.java b/model/api/src/main/java/org/keycloak/models/GrantedConsentModel.java
new file mode 100644
index 0000000..e036655
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/GrantedConsentModel.java
@@ -0,0 +1,47 @@
+package org.keycloak.models;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GrantedConsentModel {
+
+ private final String clientId;
+ private Set<String> protocolMapperIds = new HashSet<String>();
+ private Set<String> roleIds = new HashSet<String>();
+
+ public GrantedConsentModel(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public void addGrantedRole(String roleId) {
+ roleIds.add(roleId);
+ }
+
+ public Set<String> getGrantedRoles() {
+ return roleIds;
+ }
+
+ public boolean isRoleGranted(String roleId) {
+ return roleIds.contains(roleId);
+ }
+
+ public void addGrantedProtocolMapper(String protocolMapperId) {
+ protocolMapperIds.add(protocolMapperId);
+ }
+
+ public Set<String> getGrantedProtocolMappers() {
+ return protocolMapperIds;
+ }
+
+ public boolean isProtocolMapperGranted(String protocolMapperId) {
+ return protocolMapperIds.contains(protocolMapperId);
+ }
+
+}
diff --git a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java
index acab3e0..14aa55d 100644
--- a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java
+++ b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java
@@ -10,6 +10,7 @@ public class LDAPConstants {
public static final String VENDOR_ACTIVE_DIRECTORY = "ad";
public static final String VENDOR_OTHER = "other";
public static final String VENDOR_TIVOLI = "tivoli";
+ public static final String VENDOR_NOVELL_EDIRECTORY="edirectory" ;
public static final String USERNAME_LDAP_ATTRIBUTE = "usernameLDAPAttribute";
public static final String USER_OBJECT_CLASSES = "userObjectClasses";
diff --git a/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java b/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
index 59d2877..ed2282c 100755
--- a/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
+++ b/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
@@ -78,6 +78,8 @@ public class PasswordPolicy {
list.add(new RegexPatterns(args));
} else if (name.equals(PasswordHistory.NAME)) {
list.add(new PasswordHistory(args));
+ } else if (name.equals(ForceExpiredPasswordChange.NAME)) {
+ list.add(new ForceExpiredPasswordChange(args));
}
}
return list;
@@ -114,6 +116,22 @@ public class PasswordPolicy {
}
return -1;
}
+
+ /**
+ *
+ * @return -1 if no force expired password change setting
+ */
+ public int getDaysToExpirePassword() {
+ if (policies == null)
+ return -1;
+ for (Policy p : policies) {
+ if (p instanceof ForceExpiredPasswordChange) {
+ return ((ForceExpiredPasswordChange) p).daysToExpirePassword;
+ }
+
+ }
+ return -1;
+ }
public Error validate(UserModel user, String password) {
for (Policy p : policies) {
@@ -418,6 +436,25 @@ public class PasswordPolicy {
}
}
+ private static class ForceExpiredPasswordChange implements Policy {
+ private static final String NAME = "forceExpiredPasswordChange";
+ private int daysToExpirePassword;
+
+ public ForceExpiredPasswordChange(String[] args) {
+ daysToExpirePassword = intArg(NAME, 365, args);
+ }
+
+ @Override
+ public Error validate(String username, String password) {
+ return null;
+ }
+
+ @Override
+ public Error validate(UserModel user, String password) {
+ return null;
+ }
+ }
+
private static int intArg(String policy, int defaultValue, String... args) {
if (args == null || args.length == 0) {
return defaultValue;
diff --git a/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java b/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java
index 663eaec..4654ba5 100755
--- a/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java
@@ -12,7 +12,7 @@ public class UserCredentialValueModel {
private String device;
private byte[] salt;
private int hashIterations;
- private long createdDate;
+ private Long createdDate;
public String getType() {
return type;
@@ -54,11 +54,11 @@ public class UserCredentialValueModel {
this.hashIterations = iterations;
}
- public long getCreatedDate() {
+ public Long getCreatedDate() {
return createdDate;
}
- public void setCreatedDate(long createdDate) {
+ public void setCreatedDate(Long createdDate) {
this.createdDate = createdDate;
}
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
index 94d0226..b111433 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -320,6 +320,16 @@ public class UserFederationManager implements UserProvider {
session.userStorage().preRemove(realm, role);
}
+ @Override
+ public void preRemove(RealmModel realm, ClientModel client) {
+ session.userStorage().preRemove(realm, client);
+ }
+
+ @Override
+ public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+ session.userStorage().preRemove(client, protocolMapper);
+ }
+
public void updateCredential(RealmModel realm, UserModel user, UserCredentialModel credential) {
if (credential.getType().equals(UserCredentialModel.PASSWORD)) {
if (realm.getPasswordPolicy() != null) {
diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java
index 770cf03..6ad716c 100755
--- a/model/api/src/main/java/org/keycloak/models/UserModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserModel.java
@@ -75,9 +75,11 @@ public interface UserModel {
String getFederationLink();
void setFederationLink(String link);
-
-
-
+ GrantedConsentModel addGrantedConsent(GrantedConsentModel consent);
+ GrantedConsentModel getGrantedConsentByClient(String clientId);
+ List<GrantedConsentModel> getGrantedConsents();
+ void updateGrantedConsent(GrantedConsentModel consent);
+ boolean revokeGrantedConsentForClient(String clientId);
public static enum RequiredAction {
VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD
diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java
index b508638..5e78cd1 100755
--- a/model/api/src/main/java/org/keycloak/models/UserProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java
@@ -41,6 +41,9 @@ public interface UserProvider extends Provider {
void preRemove(RealmModel realm, RoleModel role);
+ void preRemove(RealmModel realm, ClientModel client);
+ void preRemove(ClientModel realm, ProtocolMapperModel protocolMapper);
+
boolean validCredentials(RealmModel realm, UserModel user, List<UserCredentialModel> input);
boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input);
CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input);
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 00e86f0..0af1442 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -36,6 +36,7 @@ import org.keycloak.representations.idm.ScopeMappingRepresentation;
import org.keycloak.representations.idm.SocialLinkRepresentation;
import org.keycloak.representations.idm.UserFederationProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.util.UriUtils;
import java.io.IOException;
import java.net.URI;
@@ -526,7 +527,7 @@ public class RepresentationToModel {
client.setManagementUrl(resourceRep.getAdminUrl());
if (resourceRep.isSurrogateAuthRequired() != null)
client.setSurrogateAuthRequired(resourceRep.isSurrogateAuthRequired());
- client.setBaseUrl(resourceRep.getBaseUrl());
+ if (resourceRep.getBaseUrl() != null) client.setBaseUrl(resourceRep.getBaseUrl());
if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly());
if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired());
if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient());
@@ -576,12 +577,8 @@ public class RepresentationToModel {
Set<String> origins = new HashSet<String>();
for (String redirectUri : resourceRep.getRedirectUris()) {
logger.debugv("add redirect-uri to origin: {0}", redirectUri);
- if (redirectUri.startsWith("http:")) {
- URI uri = URI.create(redirectUri);
- String origin = uri.getScheme() + "://" + uri.getHost();
- if (uri.getPort() != -1) {
- origin += ":" + uri.getPort();
- }
+ if (redirectUri.startsWith("http")) {
+ String origin = UriUtils.getOrigin(redirectUri);
logger.debugv("adding default client origin: {0}" , origin);
origins.add(origin);
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
index 0e84ca9..83b6add 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
@@ -1,6 +1,7 @@
package org.keycloak.models.utils;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.GrantedConsentModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
@@ -185,4 +186,29 @@ public class UserModelDelegate implements UserModel {
public void setFederationLink(String link) {
delegate.setFederationLink(link);
}
+
+ @Override
+ public GrantedConsentModel addGrantedConsent(GrantedConsentModel consent) {
+ return delegate.addGrantedConsent(consent);
+ }
+
+ @Override
+ public GrantedConsentModel getGrantedConsentByClient(String clientId) {
+ return delegate.getGrantedConsentByClient(clientId);
+ }
+
+ @Override
+ public List<GrantedConsentModel> getGrantedConsents() {
+ return delegate.getGrantedConsents();
+ }
+
+ @Override
+ public void updateGrantedConsent(GrantedConsentModel consent) {
+ delegate.updateGrantedConsent(consent);
+ }
+
+ @Override
+ public boolean revokeGrantedConsentForClient(String clientId) {
+ return delegate.revokeGrantedConsentForClient(clientId);
+ }
}
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
index 236f95c..3033eea 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
@@ -16,9 +16,11 @@
*/
package org.keycloak.models.file.adapter;
-import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientModel;
+
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
+import org.keycloak.models.GrantedConsentModel;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
@@ -31,7 +33,6 @@ import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
-import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -43,6 +44,7 @@ import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.entities.FederatedIdentityEntity;
import org.keycloak.models.entities.RoleEntity;
import org.keycloak.models.entities.UserEntity;
+import org.keycloak.util.Time;
/**
* UserModel for JSON persistence.
@@ -271,7 +273,6 @@ public class UserAdapter implements UserModel, Comparable {
private CredentialEntity setCredentials(UserEntity user, UserCredentialModel cred) {
CredentialEntity credentialEntity = new CredentialEntity();
credentialEntity.setType(cred.getType());
- credentialEntity.setCreatedDate(new Date().getTime());
credentialEntity.setDevice(cred.getDevice());
return credentialEntity;
}
@@ -285,6 +286,7 @@ public class UserAdapter implements UserModel, Comparable {
if (hashIterations == -1)
hashIterations = 1;
}
+ credentialEntity.setCreatedDate(Time.toMillis(Time.currentTime()));
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
@@ -430,6 +432,35 @@ public class UserAdapter implements UserModel, Comparable {
}
@Override
+ public GrantedConsentModel addGrantedConsent(GrantedConsentModel consent) {
+ // TODO
+ return null;
+ }
+
+ @Override
+ public GrantedConsentModel getGrantedConsentByClient(String clientId) {
+ // TODO
+ return null;
+ }
+
+ @Override
+ public List<GrantedConsentModel> getGrantedConsents() {
+ // TODO
+ return null;
+ }
+
+ @Override
+ public void updateGrantedConsent(GrantedConsentModel consent) {
+ // TODO
+ }
+
+ @Override
+ public boolean revokeGrantedConsentForClient(String clientId) {
+ // TODO
+ return false;
+ }
+
+ @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof UserModel)) return false;
diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
index 850d1a7..5c50a59 100644
--- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
+++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
@@ -19,6 +19,8 @@ package org.keycloak.models.file;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
+
+import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.file.adapter.UserAdapter;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
@@ -385,6 +387,16 @@ public class FileUserProvider implements UserProvider {
}
@Override
+ public void preRemove(RealmModel realm, ClientModel client) {
+ // TODO
+ }
+
+ @Override
+ public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+ // TODO
+ }
+
+ @Override
public boolean validCredentials(RealmModel realm, UserModel user, List<UserCredentialModel> input) {
return CredentialValidation.validCredentials(realm, user, input);
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
index 97f3c6c..16c77d1 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
@@ -1,8 +1,10 @@
package org.keycloak.models.cache;
+import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.FederatedIdentityModel;
@@ -310,4 +312,14 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, link);
}
+
+ @Override
+ public void preRemove(RealmModel realm, ClientModel client) {
+ getDelegate().preRemove(realm, client);
+ }
+
+ @Override
+ public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+ getDelegate().preRemove(client, protocolMapper);
+ }
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java
index 857b3e8..b0b7069 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java
@@ -1,7 +1,9 @@
package org.keycloak.models.cache;
+import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.FederatedIdentityModel;
@@ -175,4 +177,14 @@ public class NoCacheUserProvider implements CacheUserProvider {
public void preRemove(RealmModel realm, RoleModel role) {
getDelegate().preRemove(realm, role);
}
+
+ @Override
+ public void preRemove(RealmModel realm, ClientModel client) {
+ getDelegate().preRemove(realm, client);
+ }
+
+ @Override
+ public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+ getDelegate().preRemove(client, protocolMapper);
+ }
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
index 2087fc2..97b62fc 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
@@ -1,6 +1,7 @@
package org.keycloak.models.cache;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.GrantedConsentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
@@ -274,4 +275,36 @@ public class UserAdapter implements UserModel {
getDelegateForUpdate();
updated.deleteRoleMapping(role);
}
+
+ @Override
+ public GrantedConsentModel addGrantedConsent(GrantedConsentModel consent) {
+ getDelegateForUpdate();
+ return updated.addGrantedConsent(consent);
+ }
+
+ @Override
+ public GrantedConsentModel getGrantedConsentByClient(String clientId) {
+ // TODO: caching?
+ getDelegateForUpdate();
+ return updated.getGrantedConsentByClient(clientId);
+ }
+
+ @Override
+ public List<GrantedConsentModel> getGrantedConsents() {
+ // TODO: caching?
+ getDelegateForUpdate();
+ return updated.getGrantedConsents();
+ }
+
+ @Override
+ public void updateGrantedConsent(GrantedConsentModel consent) {
+ getDelegateForUpdate();
+ updated.updateGrantedConsent(consent);
+ }
+
+ @Override
+ public boolean revokeGrantedConsentForClient(String clientId) {
+ getDelegateForUpdate();
+ return updated.revokeGrantedConsentForClient(clientId);
+ }
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
index 7ebb242..498db9a 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
@@ -418,6 +418,8 @@ public class ClientAdapter implements ClientModel {
public void removeProtocolMapper(ProtocolMapperModel mapping) {
ProtocolMapperEntity toDelete = getProtocolMapperEntity(mapping.getId());
if (toDelete != null) {
+ session.users().preRemove(this, mapping);
+
this.entity.getProtocolMappers().remove(toDelete);
em.remove(toDelete);
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java
index bdbfe84..998c469 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java
@@ -38,7 +38,7 @@ public class CredentialEntity {
@Column(name="HASH_ITERATIONS")
protected int hashIterations;
@Column(name="CREATED_DATE")
- protected long createdDate;
+ protected Long createdDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="USER_ID")
@@ -100,11 +100,11 @@ public class CredentialEntity {
this.hashIterations = hashIterations;
}
- public long getCreatedDate() {
+ public Long getCreatedDate() {
return createdDate;
}
- public void setCreatedDate(long createdDate) {
+ public void setCreatedDate(Long createdDate) {
this.createdDate = createdDate;
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentEntity.java
new file mode 100644
index 0000000..262f37f
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentEntity.java
@@ -0,0 +1,96 @@
+package org.keycloak.models.jpa.entities;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.persistence.CascadeType;
+import javax.persistence.CollectionTable;
+import javax.persistence.Column;
+import javax.persistence.ElementCollection;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Entity
+@Table(name="GRANTED_CONSENT", uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"USER_ID", "CLIENT_ID"})
+})
+@NamedQueries({
+ @NamedQuery(name="grantedConsentByUserAndClient", query="select consent from GrantedConsentEntity consent where consent.user.id = :userId and consent.clientId = :clientId"),
+ @NamedQuery(name="grantedConsentsByUser", query="select consent from GrantedConsentEntity consent where consent.user.id = :userId"),
+ @NamedQuery(name="deleteGrantedConsentsByRealm", query="delete from GrantedConsentEntity consent where consent.user IN (select user from UserEntity user where user.realmId = :realmId)"),
+ @NamedQuery(name="deleteGrantedConsentsByUser", query="delete from GrantedConsentEntity consent where consent.user = :user"),
+ @NamedQuery(name="deleteGrantedConsentsByClient", query="delete from GrantedConsentEntity consent where consent.clientId = :clientId"),
+})
+public class GrantedConsentEntity {
+
+ @Id
+ @Column(name="ID", length = 36)
+ protected String id;
+
+ @ManyToOne(fetch= FetchType.LAZY)
+ @JoinColumn(name="USER_ID")
+ protected UserEntity user;
+
+ @Column(name="CLIENT_ID")
+ protected String clientId;
+
+ @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "grantedConsent")
+ Collection<GrantedConsentRoleEntity> grantedRoles = new ArrayList<GrantedConsentRoleEntity>();
+
+ @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "grantedConsent")
+ Collection<GrantedConsentProtocolMapperEntity> grantedProtocolMappers = new ArrayList<GrantedConsentProtocolMapperEntity>();
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public UserEntity getUser() {
+ return user;
+ }
+
+ public void setUser(UserEntity user) {
+ this.user = user;
+ }
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public Collection<GrantedConsentRoleEntity> getGrantedRoles() {
+ return grantedRoles;
+ }
+
+ public void setGrantedRoles(Collection<GrantedConsentRoleEntity> grantedRoles) {
+ this.grantedRoles = grantedRoles;
+ }
+
+ public Collection<GrantedConsentProtocolMapperEntity> getGrantedProtocolMappers() {
+ return grantedProtocolMappers;
+ }
+
+ public void setGrantedProtocolMappers(Collection<GrantedConsentProtocolMapperEntity> grantedProtocolMappers) {
+ this.grantedProtocolMappers = grantedProtocolMappers;
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentProtocolMapperEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentProtocolMapperEntity.java
new file mode 100644
index 0000000..7683dec
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentProtocolMapperEntity.java
@@ -0,0 +1,115 @@
+package org.keycloak.models.jpa.entities;
+
+import java.io.Serializable;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NamedQueries({
+ @NamedQuery(name="deleteGrantedConsentProtMappersByRealm", query=
+ "delete from GrantedConsentProtocolMapperEntity csm where csm.grantedConsent IN (select consent from GrantedConsentEntity consent where consent.user IN (select user from UserEntity user where user.realmId = :realmId))"),
+ @NamedQuery(name="deleteGrantedConsentProtMappersByUser", query="delete from GrantedConsentProtocolMapperEntity csm where csm.grantedConsent IN (select consent from GrantedConsentEntity consent where consent.user = :user)"),
+ @NamedQuery(name="deleteGrantedConsentProtMappersByProtocolMapper", query="delete from GrantedConsentProtocolMapperEntity csm where csm.protocolMapperId = :protocolMapperId)"),
+ @NamedQuery(name="deleteGrantedConsentProtMappersByClient", query="delete from GrantedConsentProtocolMapperEntity csm where csm.grantedConsent IN (select consent from GrantedConsentEntity consent where consent.clientId = :clientId))"),
+})
+@Entity
+@Table(name="GRANTED_CONSENT_PROT_MAPPER")
+@IdClass(GrantedConsentProtocolMapperEntity.Key.class)
+public class GrantedConsentProtocolMapperEntity {
+
+ @Id
+ @ManyToOne(fetch= FetchType.LAZY)
+ @JoinColumn(name = "GRANTED_CONSENT_ID")
+ protected GrantedConsentEntity grantedConsent;
+
+ @Id
+ @Column(name="PROTOCOL_MAPPER_ID")
+ protected String protocolMapperId;
+
+ public GrantedConsentEntity getGrantedConsent() {
+ return grantedConsent;
+ }
+
+ public void setGrantedConsent(GrantedConsentEntity grantedConsent) {
+ this.grantedConsent = grantedConsent;
+ }
+
+ public String getProtocolMapperId() {
+ return protocolMapperId;
+ }
+
+ public void setProtocolMapperId(String protocolMapperId) {
+ this.protocolMapperId = protocolMapperId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ GrantedConsentProtocolMapperEntity that = (GrantedConsentProtocolMapperEntity)o;
+ Key myKey = new Key(this.grantedConsent, this.protocolMapperId);
+ Key hisKey = new Key(that.grantedConsent, that.protocolMapperId);
+ return myKey.equals(hisKey);
+ }
+
+ @Override
+ public int hashCode() {
+ Key myKey = new Key(this.grantedConsent, this.protocolMapperId);
+ return myKey.hashCode();
+ }
+
+ public static class Key implements Serializable {
+
+ protected GrantedConsentEntity grantedConsent;
+
+ protected String protocolMapperId;
+
+ public Key() {
+ }
+
+ public Key(GrantedConsentEntity grantedConsent, String protocolMapperId) {
+ this.grantedConsent = grantedConsent;
+ this.protocolMapperId = protocolMapperId;
+ }
+
+ public GrantedConsentEntity getGrantedConsent() {
+ return grantedConsent;
+ }
+
+ public String getProtocolMapperId() {
+ return protocolMapperId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Key key = (Key) o;
+
+ if (grantedConsent != null ? !grantedConsent.getId().equals(key.grantedConsent != null ? key.grantedConsent.getId() : null) : key.grantedConsent != null) return false;
+ if (protocolMapperId != null ? !protocolMapperId.equals(key.protocolMapperId) : key.protocolMapperId != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = grantedConsent != null ? grantedConsent.getId().hashCode() : 0;
+ result = 31 * result + (protocolMapperId != null ? protocolMapperId.hashCode() : 0);
+ return result;
+ }
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentRoleEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentRoleEntity.java
new file mode 100644
index 0000000..ee2b3bb
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentRoleEntity.java
@@ -0,0 +1,115 @@
+package org.keycloak.models.jpa.entities;
+
+import java.io.Serializable;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NamedQueries({
+ @NamedQuery(name="deleteGrantedConsentRolesByRealm", query="delete from GrantedConsentRoleEntity grantedRole where grantedRole.grantedConsent IN (select consent from GrantedConsentEntity consent where consent.user IN (select user from UserEntity user where user.realmId = :realmId))"),
+ @NamedQuery(name="deleteGrantedConsentRolesByUser", query="delete from GrantedConsentRoleEntity grantedRole where grantedRole.grantedConsent IN (select consent from GrantedConsentEntity consent where consent.user = :user)"),
+ @NamedQuery(name="deleteGrantedConsentRolesByRole", query="delete from GrantedConsentRoleEntity grantedRole where grantedRole.roleId = :roleId)"),
+ @NamedQuery(name="deleteGrantedConsentRolesByClient", query="delete from GrantedConsentRoleEntity grantedRole where grantedRole.grantedConsent IN (select consent from GrantedConsentEntity consent where consent.clientId = :clientId)"),
+})
+@Entity
+@Table(name="GRANTED_CONSENT_ROLE")
+@IdClass(GrantedConsentRoleEntity.Key.class)
+public class GrantedConsentRoleEntity {
+
+ @Id
+ @ManyToOne(fetch= FetchType.LAZY)
+ @JoinColumn(name = "GRANTED_CONSENT_ID")
+ protected GrantedConsentEntity grantedConsent;
+
+ @Id
+ @Column(name="ROLE_ID")
+ protected String roleId;
+
+ public GrantedConsentEntity getGrantedConsent() {
+ return grantedConsent;
+ }
+
+ public void setGrantedConsent(GrantedConsentEntity grantedConsent) {
+ this.grantedConsent = grantedConsent;
+ }
+
+ public String getRoleId() {
+ return roleId;
+ }
+
+ public void setRoleId(String roleId) {
+ this.roleId = roleId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ GrantedConsentRoleEntity that = (GrantedConsentRoleEntity)o;
+ Key myKey = new Key(this.grantedConsent, this.roleId);
+ Key hisKey = new Key(that.grantedConsent, that.roleId);
+ return myKey.equals(hisKey);
+ }
+
+ @Override
+ public int hashCode() {
+ Key myKey = new Key(this.grantedConsent, this.roleId);
+ return myKey.hashCode();
+ }
+
+ public static class Key implements Serializable {
+
+ protected GrantedConsentEntity grantedConsent;
+
+ protected String roleId;
+
+ public Key() {
+ }
+
+ public Key(GrantedConsentEntity grantedConsent, String roleId) {
+ this.grantedConsent = grantedConsent;
+ this.roleId = roleId;
+ }
+
+ public GrantedConsentEntity getGrantedConsent() {
+ return grantedConsent;
+ }
+
+ public String getRoleId() {
+ return roleId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Key key = (Key) o;
+
+ if (grantedConsent != null ? !grantedConsent.getId().equals(key.grantedConsent != null ? key.grantedConsent.getId() : null) : key.grantedConsent != null) return false;
+ if (roleId != null ? !roleId.equals(key.roleId) : key.roleId != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = grantedConsent != null ? grantedConsent.getId().hashCode() : 0;
+ result = 31 * result + (roleId != null ? roleId.hashCode() : 0);
+ return result;
+ }
+ }
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index 07d3f6f..15300d0 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -4,6 +4,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
@@ -87,6 +88,9 @@ public class JpaUserProvider implements UserProvider {
private void removeUser(UserEntity user) {
em.createNamedQuery("deleteUserRoleMappingsByUser").setParameter("user", user).executeUpdate();
em.createNamedQuery("deleteFederatedIdentityByUser").setParameter("user", user).executeUpdate();
+ em.createNamedQuery("deleteGrantedConsentRolesByUser").setParameter("user", user).executeUpdate();
+ em.createNamedQuery("deleteGrantedConsentProtMappersByUser").setParameter("user", user).executeUpdate();
+ em.createNamedQuery("deleteGrantedConsentsByUser").setParameter("user", user).executeUpdate();
em.remove(user);
}
@@ -130,7 +134,13 @@ public class JpaUserProvider implements UserProvider {
@Override
public void preRemove(RealmModel realm) {
- int num = em.createNamedQuery("deleteUserRoleMappingsByRealm")
+ int num = em.createNamedQuery("deleteGrantedConsentRolesByRealm")
+ .setParameter("realmId", realm.getId()).executeUpdate();
+ num = em.createNamedQuery("deleteGrantedConsentProtMappersByRealm")
+ .setParameter("realmId", realm.getId()).executeUpdate();
+ num = em.createNamedQuery("deleteGrantedConsentsByRealm")
+ .setParameter("realmId", realm.getId()).executeUpdate();
+ num = em.createNamedQuery("deleteUserRoleMappingsByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUserRequiredActionsByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
@@ -174,10 +184,25 @@ public class JpaUserProvider implements UserProvider {
@Override
public void preRemove(RealmModel realm, RoleModel role) {
+ em.createNamedQuery("deleteGrantedConsentRolesByRole").setParameter("roleId", role.getId()).executeUpdate();
em.createNamedQuery("deleteUserRoleMappingsByRole").setParameter("roleId", role.getId()).executeUpdate();
}
@Override
+ public void preRemove(RealmModel realm, ClientModel client) {
+ em.createNamedQuery("deleteGrantedConsentProtMappersByClient").setParameter("clientId", client.getId()).executeUpdate();
+ em.createNamedQuery("deleteGrantedConsentRolesByClient").setParameter("clientId", client.getId()).executeUpdate();
+ em.createNamedQuery("deleteGrantedConsentsByClient").setParameter("clientId", client.getId()).executeUpdate();
+ }
+
+ @Override
+ public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+ em.createNamedQuery("deleteGrantedConsentProtMappersByProtocolMapper")
+ .setParameter("protocolMapperId", protocolMapper.getId())
+ .executeUpdate();
+ }
+
+ @Override
public UserModel getUserById(String id, RealmModel realm) {
TypedQuery<UserEntity> query = em.createNamedQuery("getRealmUserById", UserEntity.class);
query.setParameter("id", id);
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 2ab9ac7..4cc5ddf 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -658,6 +658,8 @@ public class RealmAdapter implements RealmModel {
ClientModel client = getClientById(id);
if (client == null) return false;
+ session.users().preRemove(this, client);
+
for (RoleModel role : client.getRoles()) {
client.removeRole(role);
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index 2216d22..f8d9352 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
@@ -1,6 +1,9 @@
package org.keycloak.models.jpa;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.GrantedConsentModel;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ModelException;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
@@ -9,12 +12,16 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.entities.CredentialEntity;
+import org.keycloak.models.jpa.entities.GrantedConsentEntity;
+import org.keycloak.models.jpa.entities.GrantedConsentProtocolMapperEntity;
+import org.keycloak.models.jpa.entities.GrantedConsentRoleEntity;
import org.keycloak.models.jpa.entities.UserAttributeEntity;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.jpa.entities.UserRequiredActionEntity;
import org.keycloak.models.jpa.entities.UserRoleMappingEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
+import org.keycloak.util.Time;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
@@ -23,6 +30,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
+import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -273,7 +281,6 @@ public class UserAdapter implements UserModel {
CredentialEntity credentialEntity = new CredentialEntity();
credentialEntity.setId(KeycloakModelUtils.generateId());
credentialEntity.setType(cred.getType());
- credentialEntity.setCreatedDate(new Date().getTime());
credentialEntity.setDevice(cred.getDevice());
credentialEntity.setUser(user);
return credentialEntity;
@@ -288,6 +295,7 @@ public class UserAdapter implements UserModel {
if (hashIterations == -1)
hashIterations = 1;
}
+ credentialEntity.setCreatedDate(Time.toMillis(Time.currentTime()));
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
@@ -472,6 +480,164 @@ public class UserAdapter implements UserModel {
}
@Override
+ public GrantedConsentModel addGrantedConsent(GrantedConsentModel consent) {
+ String clientId = consent.getClientId();
+ if (clientId == null) {
+ throw new ModelException("clientId needs to be filled for newly added consent!");
+ }
+
+ GrantedConsentEntity consentEntity = getGrantedConsentEntity(clientId);
+ if (consentEntity != null) {
+ throw new ModelDuplicateException("Consent already exists for client [" + clientId + "] and user [" + user.getId() + "]");
+ }
+
+ consentEntity = new GrantedConsentEntity();
+ consentEntity.setId(KeycloakModelUtils.generateId());
+ consentEntity.setUser(user);
+ consentEntity.setClientId(clientId);
+ em.persist(consentEntity);
+ em.flush();
+
+ updateGrantedConsentEntity(consentEntity, consent);
+
+ return consent;
+ }
+
+ @Override
+ public GrantedConsentModel getGrantedConsentByClient(String clientId) {
+ GrantedConsentEntity entity = getGrantedConsentEntity(clientId);
+ return toConsentModel(entity);
+ }
+
+ @Override
+ public List<GrantedConsentModel> getGrantedConsents() {
+ TypedQuery<GrantedConsentEntity> query = em.createNamedQuery("grantedConsentsByUser", GrantedConsentEntity.class);
+ query.setParameter("userId", getId());
+ List<GrantedConsentEntity> results = query.getResultList();
+
+ List<GrantedConsentModel> consents = new ArrayList<GrantedConsentModel>();
+ for (GrantedConsentEntity entity : results) {
+ GrantedConsentModel model = toConsentModel(entity);
+ consents.add(model);
+ }
+ return consents;
+ }
+
+ @Override
+ public void updateGrantedConsent(GrantedConsentModel consent) {
+ String clientId = consent.getClientId();
+ if (clientId == null) {
+ throw new ModelException("clientId needs to be for newly added consent!");
+ }
+
+ GrantedConsentEntity consentEntity = getGrantedConsentEntity(clientId);
+ if (consentEntity == null) {
+ throw new ModelException("Consent not found for client [" + clientId + "] and user [" + user.getId() + "]");
+ }
+
+ updateGrantedConsentEntity(consentEntity, consent);
+ }
+
+ @Override
+ public boolean revokeGrantedConsentForClient(String clientId) {
+ GrantedConsentEntity consentEntity = getGrantedConsentEntity(clientId);
+ if (consentEntity == null) return false;
+
+ em.remove(consentEntity);
+ em.flush();
+ return true;
+ }
+
+
+ private GrantedConsentEntity getGrantedConsentEntity(String clientId) {
+ TypedQuery<GrantedConsentEntity> query = em.createNamedQuery("grantedConsentByUserAndClient", GrantedConsentEntity.class);
+ query.setParameter("userId", getId());
+ query.setParameter("clientId", clientId);
+ List<GrantedConsentEntity> results = query.getResultList();
+ if (results.size() > 1) {
+ throw new ModelException("More results found for user [" + getUsername() + "] and client [" + clientId + "]");
+ } else if (results.size() == 1) {
+ return results.get(0);
+ } else {
+ return null;
+ }
+ }
+
+ private GrantedConsentModel toConsentModel(GrantedConsentEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+
+ GrantedConsentModel model = new GrantedConsentModel(entity.getClientId());
+
+ Collection<GrantedConsentRoleEntity> grantedRoleEntities = entity.getGrantedRoles();
+ if (grantedRoleEntities != null) {
+ for (GrantedConsentRoleEntity grantedRole : grantedRoleEntities) {
+ model.addGrantedRole(grantedRole.getRoleId());
+ }
+ }
+
+ Collection<GrantedConsentProtocolMapperEntity> grantedProtocolMapperEntities = entity.getGrantedProtocolMappers();
+ if (grantedProtocolMapperEntities != null) {
+ for (GrantedConsentProtocolMapperEntity grantedProtMapper : grantedProtocolMapperEntities) {
+ model.addGrantedProtocolMapper(grantedProtMapper.getProtocolMapperId());
+ }
+ }
+
+ return model;
+ }
+
+ // Update roles and protocolMappers to given consentEntity from the consentModel
+ private void updateGrantedConsentEntity(GrantedConsentEntity consentEntity, GrantedConsentModel consentModel) {
+ Collection<GrantedConsentProtocolMapperEntity> grantedProtocolMapperEntities = consentEntity.getGrantedProtocolMappers();
+ Collection<GrantedConsentProtocolMapperEntity> mappersToRemove = new HashSet<GrantedConsentProtocolMapperEntity>(grantedProtocolMapperEntities);
+
+ for (String protocolMapperId : consentModel.getGrantedProtocolMappers()) {
+ GrantedConsentProtocolMapperEntity grantedProtocolMapperEntity = new GrantedConsentProtocolMapperEntity();
+ grantedProtocolMapperEntity.setGrantedConsent(consentEntity);
+ grantedProtocolMapperEntity.setProtocolMapperId(protocolMapperId);
+
+ // Check if it's already there
+ if (!grantedProtocolMapperEntities.contains(grantedProtocolMapperEntity)) {
+ em.persist(grantedProtocolMapperEntity);
+ em.flush();
+ grantedProtocolMapperEntities.add(grantedProtocolMapperEntity);
+ } else {
+ mappersToRemove.remove(grantedProtocolMapperEntity);
+ }
+ }
+ // Those mappers were no longer on consentModel and will be removed
+ for (GrantedConsentProtocolMapperEntity toRemove : mappersToRemove) {
+ grantedProtocolMapperEntities.remove(toRemove);
+ em.remove(toRemove);
+ }
+
+ Collection<GrantedConsentRoleEntity> grantedRoleEntities = consentEntity.getGrantedRoles();
+ Set<GrantedConsentRoleEntity> rolesToRemove = new HashSet<GrantedConsentRoleEntity>(grantedRoleEntities);
+ for (String roleId : consentModel.getGrantedRoles()) {
+ GrantedConsentRoleEntity consentRoleEntity = new GrantedConsentRoleEntity();
+ consentRoleEntity.setGrantedConsent(consentEntity);
+ consentRoleEntity.setRoleId(roleId);
+
+ // Check if it's already there
+ if (!grantedRoleEntities.contains(consentRoleEntity)) {
+ em.persist(consentRoleEntity);
+ em.flush();
+ grantedRoleEntities.add(consentRoleEntity);
+ } else {
+ rolesToRemove.remove(consentRoleEntity);
+ }
+ }
+ // Those roles were no longer on consentModel and will be removed
+ for (GrantedConsentRoleEntity toRemove : rolesToRemove) {
+ grantedRoleEntities.remove(toRemove);
+ em.remove(toRemove);
+ }
+
+ em.flush();
+ }
+
+ @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof UserModel)) return false;
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
index 803cedf..8e07bc9 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
@@ -321,6 +321,8 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
public void removeProtocolMapper(ProtocolMapperModel mapping) {
for (ProtocolMapperEntity entity : getMongoEntity().getProtocolMappers()) {
if (entity.getId().equals(mapping.getId())) {
+ session.users().preRemove(this, mapping);
+
getMongoEntity().getProtocolMappers().remove(entity);
updateMongoEntity();
break;
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
index 862e387..f71c797 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
@@ -9,6 +9,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
@@ -352,6 +353,16 @@ public class MongoUserProvider implements UserProvider {
}
@Override
+ public void preRemove(RealmModel realm, ClientModel client) {
+ // TODO
+ }
+
+ @Override
+ public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+ // TODO
+ }
+
+ @Override
public void preRemove(RealmModel realm, RoleModel role) {
// todo not sure what to do for this
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index e515dde..27b62f6 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -639,6 +639,12 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override
public boolean removeClient(String id) {
+ if (id == null) return false;
+ ClientModel client = getClientById(id);
+ if (client == null) return false;
+
+ session.users().preRemove(this, client);
+
return getMongoStore().removeEntity(MongoClientEntity.class, id, invocationContext);
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
index 1214323..5fa3e52 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -4,6 +4,7 @@ import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.GrantedConsentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
@@ -16,6 +17,7 @@ import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
import org.keycloak.models.mongo.utils.MongoModelUtils;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
+import org.keycloak.util.Time;
import java.util.ArrayList;
import java.util.Collections;
@@ -239,7 +241,6 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
private CredentialEntity setCredentials(MongoUserEntity user, UserCredentialModel cred) {
CredentialEntity credentialEntity = new CredentialEntity();
credentialEntity.setType(cred.getType());
- credentialEntity.setCreatedDate(new Date().getTime());
credentialEntity.setDevice(cred.getDevice());
return credentialEntity;
}
@@ -253,6 +254,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
if (hashIterations == -1)
hashIterations = 1;
}
+ credentialEntity.setCreatedDate(Time.toMillis(Time.currentTime()));
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
@@ -420,6 +422,35 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
}
@Override
+ public GrantedConsentModel addGrantedConsent(GrantedConsentModel consent) {
+ // TODO
+ return null;
+ }
+
+ @Override
+ public GrantedConsentModel getGrantedConsentByClient(String clientId) {
+ // TODO
+ return null;
+ }
+
+ @Override
+ public List<GrantedConsentModel> getGrantedConsents() {
+ // TODO
+ return null;
+ }
+
+ @Override
+ public void updateGrantedConsent(GrantedConsentModel consent) {
+ // TODO
+ }
+
+ @Override
+ public boolean revokeGrantedConsentForClient(String clientId) {
+ // TODO
+ return false;
+ }
+
+ @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof UserModel)) return false;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index 8306b7a..93bf67b 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -91,6 +91,25 @@ public class AuthorizationEndpoint {
@GET
public Response build() {
+ MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
+
+ clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM);
+ responseType = params.getFirst(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
+ redirectUriParam = params.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM);
+ state = params.getFirst(OIDCLoginProtocol.STATE_PARAM);
+ scope = params.getFirst(OIDCLoginProtocol.SCOPE_PARAM);
+ loginHint = params.getFirst(OIDCLoginProtocol.LOGIN_HINT_PARAM);
+ prompt = params.getFirst(OIDCLoginProtocol.PROMPT_PARAM);
+ idpHint = params.getFirst(AdapterConstants.KC_IDP_HINT);
+
+ checkSsl();
+ checkRealm();
+ checkClient();
+ checkResponseType();
+ checkRedirectUri();
+
+ createClientSession();
+
switch (action) {
case REGISTER:
return buildRegister();
@@ -121,29 +140,6 @@ public class AuthorizationEndpoint {
return this;
}
- public AuthorizationEndpoint init() {
- MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
-
- clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM);
- responseType = params.getFirst(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
- redirectUriParam = params.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM);
- state = params.getFirst(OIDCLoginProtocol.STATE_PARAM);
- scope = params.getFirst(OIDCLoginProtocol.SCOPE_PARAM);
- loginHint = params.getFirst(OIDCLoginProtocol.LOGIN_HINT_PARAM);
- prompt = params.getFirst(OIDCLoginProtocol.PROMPT_PARAM);
- idpHint = params.getFirst(AdapterConstants.KC_IDP_HINT);
-
- checkSsl();
- checkRealm();
- checkClient();
- checkResponseType();
- checkRedirectUri();
-
- createClientSession();
-
- return this;
- }
-
private void checkSsl() {
if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
event.error(Errors.SSL_REQUIRED);
@@ -172,7 +168,7 @@ public class AuthorizationEndpoint {
throw new ErrorPageException(session, Messages.CLIENT_NOT_FOUND );
}
- if ((client instanceof ClientModel) && ((ClientModel) client).isBearerOnly()) {
+ if (client.isBearerOnly()) {
event.error(Errors.NOT_ALLOWED);
throw new ErrorPageException(session, Messages.BEARER_ONLY );
}
@@ -198,7 +194,9 @@ public class AuthorizationEndpoint {
event.detail(Details.RESPONSE_TYPE, responseType);
if (responseType.equals(OAuth2Constants.CODE)) {
- action = Action.CODE;
+ if (action == null) {
+ action = Action.CODE;
+ }
} else {
event.error(Errors.INVALID_REQUEST);
throw new ErrorPageException(session, Messages.INVALID_PARAMETER, OIDCLoginProtocol.RESPONSE_TYPE_PARAM );
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 43538ee..ffbc6a7 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -87,6 +87,14 @@ public class TokenEndpoint {
@POST
public Response build() {
+ formParams = request.getDecodedFormParameters();
+ grantType = formParams.getFirst(OIDCLoginProtocol.GRANT_TYPE_PARAM);
+
+ checkSsl();
+ checkRealm();
+ checkGrantType();
+ checkClient();
+
switch (action) {
case AUTHORIZATION_CODE:
return buildAuthorizationCodeAccessTokenResponse();
@@ -116,18 +124,6 @@ public class TokenEndpoint {
return this;
}
- public TokenEndpoint init() {
- formParams = request.getDecodedFormParameters();
- grantType = formParams.getFirst(OIDCLoginProtocol.GRANT_TYPE_PARAM);
-
- checkSsl();
- checkRealm();
- checkGrantType();
- checkClient();
-
- return this;
- }
-
private void checkSsl() {
if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
throw new ErrorResponseException("invalid_request", "HTTPS required", Response.Status.FORBIDDEN);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
index 666586f..e0c7170 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
@@ -107,7 +107,7 @@ public class OIDCLoginProtocolService {
public Object auth() {
AuthorizationEndpoint endpoint = new AuthorizationEndpoint(authManager, realm, event);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
- return endpoint.init();
+ return endpoint;
}
/**
@@ -117,7 +117,7 @@ public class OIDCLoginProtocolService {
public Object registerPage() {
AuthorizationEndpoint endpoint = new AuthorizationEndpoint(authManager, realm, event);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
- return endpoint.init().register();
+ return endpoint.register();
}
/**
@@ -127,7 +127,7 @@ public class OIDCLoginProtocolService {
public Object token() {
TokenEndpoint endpoint = new TokenEndpoint(tokenManager, authManager, realm, event);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
- return endpoint.init();
+ return endpoint;
}
@Path("login")
@@ -135,7 +135,7 @@ public class OIDCLoginProtocolService {
public Object loginPage() {
AuthorizationEndpoint endpoint = new AuthorizationEndpoint(authManager, realm, event);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
- return endpoint.legacy(OIDCLoginProtocol.CODE_PARAM).init();
+ return endpoint.legacy(OIDCLoginProtocol.CODE_PARAM);
}
@Path("login-status-iframe.html")
@@ -150,7 +150,7 @@ public class OIDCLoginProtocolService {
public Object grantAccessToken() {
TokenEndpoint endpoint = new TokenEndpoint(tokenManager, authManager, realm, event);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
- return endpoint.legacy(OAuth2Constants.PASSWORD).init();
+ return endpoint.legacy(OAuth2Constants.PASSWORD);
}
@Path("refresh")
@@ -158,7 +158,7 @@ public class OIDCLoginProtocolService {
public Object refreshAccessToken() {
TokenEndpoint endpoint = new TokenEndpoint(tokenManager, authManager, realm, event);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
- return endpoint.legacy(OAuth2Constants.REFRESH_TOKEN).init();
+ return endpoint.legacy(OAuth2Constants.REFRESH_TOKEN);
}
@Path("access/codes")
@@ -166,7 +166,7 @@ public class OIDCLoginProtocolService {
public Object accessCodeToToken() {
TokenEndpoint endpoint = new TokenEndpoint(tokenManager, authManager, realm, event);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
- return endpoint.legacy(OAuth2Constants.AUTHORIZATION_CODE).init();
+ return endpoint.legacy(OAuth2Constants.AUTHORIZATION_CODE);
}
@Path("validate")
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index bd512df..67e7570 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -14,11 +14,14 @@ import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.GrantedConsentModel;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.models.UserSessionModel;
@@ -41,12 +44,14 @@ import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
+
import java.net.URI;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
/**
* Stateless object that manages authentication
@@ -375,6 +380,7 @@ public class AuthenticationManager {
HttpRequest request, UriInfo uriInfo, EventBuilder event) {
RealmModel realm = clientSession.getRealm();
UserModel user = userSession.getUser();
+ isForcePasswordUpdateRequired(realm, user);
isTotpConfigurationRequired(realm, user);
isEmailVerificationRequired(realm, user);
ClientModel client = clientSession.getClient();
@@ -414,9 +420,17 @@ public class AuthenticationManager {
if (client.isConsentRequired()) {
accessCode.setAction(ClientSessionModel.Action.OAUTH_GRANT);
+ GrantedConsentModel grantedConsent = user.getGrantedConsentByClient(client.getId());
+
List<RoleModel> realmRoles = new LinkedList<RoleModel>();
MultivaluedMap<String, RoleModel> resourceRoles = new MultivaluedMapImpl<String, RoleModel>();
for (RoleModel r : accessCode.getRequestedRoles()) {
+
+ // Consent already granted by user
+ if (grantedConsent != null && grantedConsent.getGrantedRoles().contains(r.getId())) {
+ continue;
+ }
+
if (r.getContainer() instanceof RealmModel) {
realmRoles.add(r);
} else {
@@ -424,16 +438,52 @@ public class AuthenticationManager {
}
}
- return session.getProvider(LoginFormsProvider.class)
- .setClientSessionCode(accessCode.getCode())
- .setAccessRequest(realmRoles, resourceRoles)
- .createOAuthGrant(clientSession);
+ List<ProtocolMapperModel> protocolMappers = new LinkedList<ProtocolMapperModel>();
+ for (ProtocolMapperModel model : client.getProtocolMappers()) {
+ if (model.isConsentRequired() && model.getProtocol().equals(clientSession.getAuthMethod()) && model.getConsentText() != null) {
+ if (grantedConsent == null || !grantedConsent.getGrantedProtocolMappers().contains(model.getId())) {
+ protocolMappers.add(model);
+ }
+ }
+ }
+
+ // Skip grant screen if everything was already approved by this user
+ if (realmRoles.size() > 0 || resourceRoles.size() > 0 || protocolMappers.size() > 0) {
+ return session.getProvider(LoginFormsProvider.class)
+ .setClientSessionCode(accessCode.getCode())
+ .setAccessRequest(realmRoles, resourceRoles, protocolMappers)
+ .createOAuthGrant(clientSession);
+ }
}
event.success();
return redirectAfterSuccessfulFlow(session, realm , userSession, clientSession, request, uriInfo, clientConnection);
}
+
+ private static void isForcePasswordUpdateRequired(RealmModel realm, UserModel user) {
+ int daysToExpirePassword = realm.getPasswordPolicy().getDaysToExpirePassword();
+ if(daysToExpirePassword != -1) {
+ for (UserCredentialValueModel entity : user.getCredentialsDirectly()) {
+ if (entity.getType().equals(UserCredentialModel.PASSWORD)) {
+
+ if(entity.getCreatedDate() == null) {
+ user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+ logger.debug("User is required to update password");
+ } else {
+ long timeElapsed = Time.toMillis(Time.currentTime()) - entity.getCreatedDate();
+ long timeToExpire = TimeUnit.DAYS.toMillis(daysToExpirePassword);
+
+ if(timeElapsed > timeToExpire) {
+ user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+ logger.debug("User is required to update password");
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
protected static void isTotpConfigurationRequired(RealmModel realm, UserModel user) {
for (RequiredCredentialModel c : realm.getRequiredCredentials()) {
diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java
index 18d4d09..9e16fe9 100755
--- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java
@@ -143,13 +143,13 @@ public class ClientManager {
public InstallationAdapterConfig toInstallationRepresentation(RealmModel realmModel, ClientModel clientModel, URI baseUri) {
InstallationAdapterConfig rep = new InstallationAdapterConfig();
+ rep.setAuthServerUrl(baseUri.toString());
rep.setRealm(realmModel.getName());
rep.setRealmKey(realmModel.getPublicKeyPem());
rep.setSslRequired(realmModel.getSslRequired().name().toLowerCase());
if (clientModel.isPublicClient() && !clientModel.isBearerOnly()) rep.setPublicClient(true);
if (clientModel.isBearerOnly()) rep.setBearerOnly(true);
- if (!clientModel.isBearerOnly()) rep.setAuthServerUrl(baseUri.toString());
if (clientModel.getRoles().size() > 0) rep.setUseResourceRoleMappings(true);
rep.setResource(clientModel.getClientId());
@@ -169,14 +169,12 @@ public class ClientManager {
buffer.append("<secure-deployment name=\"WAR MODULE NAME.war\">\n");
buffer.append(" <realm>").append(realmModel.getName()).append("</realm>\n");
buffer.append(" <realm-public-key>").append(realmModel.getPublicKeyPem()).append("</realm-public-key>\n");
+ buffer.append(" <auth-server-url>").append(baseUri.toString()).append("</auth-server-url>\n");
if (clientModel.isBearerOnly()){
buffer.append(" <bearer-only>true</bearer-only>\n");
- } else {
- buffer.append(" <auth-server-url>").append(baseUri.toString()).append("</auth-server-url>\n");
- if (clientModel.isPublicClient() && !clientModel.isBearerOnly()) {
- buffer.append(" <public-client>true</public-client>\n");
- }
+ } else if (clientModel.isPublicClient()) {
+ buffer.append(" <public-client>true</public-client>\n");
}
buffer.append(" <ssl-required>").append(realmModel.getSslRequired().name()).append("</ssl-required>\n");
buffer.append(" <resource>").append(clientModel.getClientId()).append("</resource>\n");
diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java
index 0c219da..f142e9a 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -134,8 +134,6 @@ public class Messages {
public static final String SESSION_NOT_ACTIVE = "sessionNotActiveMessage";
- public static final String UNKNOWN_CODE = "unknownCodeMessage";
-
public static final String INVALID_CODE = "invalidCodeMessage";
public static final String IDENTITY_PROVIDER_UNEXPECTED_ERROR = "identityProviderUnexpectedErrorMessage";
@@ -154,6 +152,8 @@ public class Messages {
public static final String SUCCESS_TOTP = "successTotpMessage";
+ public static final String SUCCESS_GRANT_REVOKED = "successGrantRevokedMessage";
+
public static final String MISSING_IDENTITY_PROVIDER = "missingIdentityProviderMessage";
public static final String INVALID_FEDERATED_IDENTITY_ACTION = "invalidFederatedIdentityActionMessage";
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 3fb84c6..af1c872 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -41,6 +41,7 @@ import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
@@ -75,6 +76,7 @@ import javax.ws.rs.core.Variant;
import java.lang.reflect.Method;
import java.net.URI;
+import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -348,6 +350,12 @@ public class AccountService {
return forwardToPage("sessions", AccountPages.SESSIONS);
}
+ @Path("access")
+ @GET
+ public Response accessPage() {
+ return forwardToPage("access", AccountPages.ACCESS);
+ }
+
/**
* Check to see if form post has sessionId hidden field and match it against the session id.
*
@@ -483,6 +491,46 @@ public class AccountService {
return Response.seeOther(location).build();
}
+ @Path("revoke-grant")
+ @POST
+ public Response processRevokeGrant(final MultivaluedMap<String, String> formData) {
+ if (auth == null) {
+ return login("access");
+ }
+
+ require(AccountRoles.MANAGE_ACCOUNT);
+ csrfCheck(formData);
+
+ String clientId = formData.getFirst("clientId");
+ if (clientId == null) {
+ return account.setError(Messages.CLIENT_NOT_FOUND).createResponse(AccountPages.ACCESS);
+ }
+ ClientModel client = realm.getClientById(clientId);
+ if (client == null) {
+ return account.setError(Messages.CLIENT_NOT_FOUND).createResponse(AccountPages.ACCESS);
+ }
+
+ // Revoke grant in UserModel
+ UserModel user = auth.getUser();
+ user.revokeGrantedConsentForClient(client.getId());
+
+ // Logout clientSessions for this user and client
+ List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
+ for (UserSessionModel userSession : userSessions) {
+ List<ClientSessionModel> clientSessions = userSession.getClientSessions();
+ for (ClientSessionModel clientSession : clientSessions) {
+ if (clientSession.getClient().getId().equals(clientId)) {
+ TokenManager.dettachClientSession(session.sessions(), realm, clientSession);
+ }
+ }
+ }
+
+ event.event(EventType.REVOKE_GRANT).client(auth.getClient()).user(auth.getUser()).detail(Details.REVOKED_CLIENT, client.getClientId()).success();
+ setReferrerOnPage();
+
+ return account.setSuccess(Messages.SUCCESS_GRANT_REVOKED).createResponse(AccountPages.ACCESS);
+ }
+
/**
* Update the TOTP for this account.
*
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index f3f5937..a0eb4dc 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -34,8 +34,10 @@ import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.GrantedConsentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
+import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.UserCredentialModel;
@@ -195,7 +197,7 @@ public class LoginActionsService {
clientCode = ClientSessionCode.parse(code, session, realm);
if (clientCode == null) {
event.error(Errors.INVALID_CODE);
- response = ErrorPage.error(session, Messages.UNKNOWN_CODE);
+ response = ErrorPage.error(session, Messages.INVALID_CODE);
return false;
}
session.getContext().setClient(clientCode.getClientSession().getClient());
@@ -288,7 +290,7 @@ public class LoginActionsService {
ClientSessionCode clientCode = ClientSessionCode.parse(code, session, realm);
if (clientCode == null) {
event.error(Errors.INVALID_CODE);
- return ErrorPage.error(session, Messages.UNKNOWN_CODE);
+ return ErrorPage.error(session, Messages.INVALID_CODE);
}
ClientSessionModel clientSession = clientCode.getClientSession();
@@ -428,7 +430,7 @@ public class LoginActionsService {
ClientSessionCode clientCode = ClientSessionCode.parse(code, session, realm);
if (clientCode == null) {
event.error(Errors.INVALID_CODE);
- return ErrorPage.error(session, Messages.UNKNOWN_CODE);
+ return ErrorPage.error(session, Messages.INVALID_CODE);
}
if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE)) {
event.error(Errors.INVALID_CODE);
@@ -576,19 +578,19 @@ public class LoginActionsService {
event.detail(Details.CODE_ID, clientSession.getId());
String redirect = clientSession.getRedirectUri();
+ UserSessionModel userSession = clientSession.getUserSession();
+ UserModel user = userSession.getUser();
+ ClientModel client = clientSession.getClient();
- event.client(clientSession.getClient())
- .user(clientSession.getUserSession().getUser())
+ event.client(client)
+ .user(user)
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.REDIRECT_URI, redirect);
- UserSessionModel userSession = clientSession.getUserSession();
- if (userSession != null) {
- event.detail(Details.AUTH_METHOD, userSession.getAuthMethod());
- event.detail(Details.USERNAME, userSession.getLoginUsername());
- if (userSession.isRememberMe()) {
- event.detail(Details.REMEMBER_ME, "true");
- }
+ event.detail(Details.AUTH_METHOD, userSession.getAuthMethod());
+ event.detail(Details.USERNAME, userSession.getLoginUsername());
+ if (userSession.isRememberMe()) {
+ event.detail(Details.REMEMBER_ME, "true");
}
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
@@ -607,6 +609,21 @@ public class LoginActionsService {
return protocol.consentDenied(clientSession);
}
+ GrantedConsentModel grantedConsent = user.getGrantedConsentByClient(client.getId());
+ if (grantedConsent == null) {
+ grantedConsent = user.addGrantedConsent(new GrantedConsentModel(client.getId()));
+ }
+ for (String roleId : clientSession.getRoles()) {
+ grantedConsent.addGrantedRole(roleId);
+ }
+ // TODO: It's not 100% sure that approved protocolMappers are same like the protocolMappers retrieved here from the client. Maybe clientSession.setProtocolMappers/getProtocolMappers should be added...
+ for (ProtocolMapperModel protocolMapper : client.getProtocolMappers()) {
+ if (protocolMapper.isConsentRequired() && protocolMapper.getProtocol().equals(clientSession.getAuthMethod()) && protocolMapper.getConsentText() != null) {
+ grantedConsent.addGrantedProtocolMapper(protocolMapper.getId());
+ }
+ }
+ user.updateGrantedConsent(grantedConsent);
+
event.success();
return authManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection);
@@ -865,7 +882,7 @@ public class LoginActionsService {
ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm);
if (accessCode == null) {
event.error(Errors.INVALID_CODE);
- return ErrorPage.error(session, Messages.UNKNOWN_CODE);
+ return ErrorPage.error(session, Messages.INVALID_CODE);
}
ClientSessionModel clientSession = accessCode.getClientSession();
diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java
index a8cb811..0d3cc32 100755
--- a/services/src/main/java/org/keycloak/services/Urls.java
+++ b/services/src/main/java/org/keycloak/services/Urls.java
@@ -118,6 +118,11 @@ public class Urls {
.build(realmId);
}
+ public static URI accountRevokeClientPage(URI baseUri, String realmId) {
+ return accountBase(baseUri).path(AccountService.class, "processRevokeGrant")
+ .build(realmId);
+ }
+
public static URI accountLogout(URI baseUri, URI redirectUri, String realmId) {
return realmLogout(baseUri).queryParam("redirect_uri", redirectUri).build(realmId);
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
index 44ebaf1..73f7655 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
@@ -21,6 +21,7 @@ import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.Constants;
import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AccountAccessPage;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
@@ -88,6 +89,9 @@ public class ProfileTest {
protected AccountUpdateProfilePage profilePage;
@WebResource
+ protected AccountAccessPage accountAccessPage;
+
+ @WebResource
protected LoginPage loginPage;
@WebResource
@@ -186,6 +190,9 @@ public class ProfileTest {
JSONObject profile = new JSONObject(IOUtils.toString(response.getEntity().getContent()));
assertEquals("test-user@localhost", profile.getString("username"));
+
+ accountAccessPage.open();
+ accountAccessPage.revokeGrant("third-party");
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
index ea06aac..e09624c 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
@@ -89,7 +89,7 @@ public abstract class AbstractIdentityProviderTest {
public WebRule webRule = new WebRule(this);
@WebResource
- private WebDriver driver;
+ protected WebDriver driver;
@WebResource
private LoginPage loginPage;
@@ -122,6 +122,7 @@ public abstract class AbstractIdentityProviderTest {
@After
public void onAfter() {
+ revokeGrant();
brokerServerRule.stopSession(this.session, true);
}
@@ -387,6 +388,9 @@ public abstract class AbstractIdentityProviderTest {
assertTrue(accountFederatedIdentityPage.isCurrent());
assertTrue(driver.getPageSource().contains("id=\"remove-" + identityProviderModel.getAlias() + "\""));
+ // Revoke grant in account mgmt
+ revokeGrant();
+
// Logout from account management
accountFederatedIdentityPage.logout();
assertTrue(driver.getTitle().equals("Log in to realm-with-broker"));
@@ -401,6 +405,9 @@ public abstract class AbstractIdentityProviderTest {
accountFederatedIdentityPage.clickRemoveProvider(identityProviderModel.getAlias());
assertTrue(driver.getPageSource().contains("id=\"add-" + identityProviderModel.getAlias() + "\""));
+ // Revoke grant in account mgmt
+ revokeGrant();
+
// Logout from account management
accountFederatedIdentityPage.logout();
assertTrue(driver.getTitle().equals("Log in to realm-with-broker"));
@@ -637,6 +644,10 @@ public abstract class AbstractIdentityProviderTest {
}
+ protected void revokeGrant() {
+
+ }
+
protected abstract String getProviderId();
protected IdentityProviderModel getIdentityProviderModel() {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java
index 3a79a40..87c1b3b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java
@@ -1,19 +1,26 @@
package org.keycloak.testsuite.broker;
+import org.junit.After;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.services.Urls;
import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.Constants;
+import org.keycloak.testsuite.pages.AccountAccessPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testutils.KeycloakServer;
import org.keycloak.util.JsonSerialization;
+import org.openqa.selenium.NoSuchElementException;
import java.io.IOException;
+import javax.ws.rs.core.UriBuilder;
+
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
@@ -22,12 +29,14 @@ import static org.junit.Assert.fail;
*/
public class OIDCKeyCloakServerBrokerBasicTest extends AbstractIdentityProviderTest {
+ private static final int PORT = 8082;
+
@ClassRule
public static AbstractKeycloakRule samlServerRule = new AbstractKeycloakRule() {
@Override
protected void configureServer(KeycloakServer server) {
- server.getConfig().setPort(8082);
+ server.getConfig().setPort(PORT);
}
@Override
@@ -44,6 +53,25 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractIdentityProviderT
@WebResource
private OAuthGrantPage grantPage;
+ @WebResource
+ protected AccountAccessPage accountAccessPage;
+
+ @Override
+ protected void revokeGrant() {
+ String currentUrl = driver.getCurrentUrl();
+
+ String accountAccessPath = Urls.accountAccessPage(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT).port(PORT).build(), "realm-with-oidc-identity-provider").toString();
+ accountAccessPage.setPath(accountAccessPath);
+ accountAccessPage.open();
+ try {
+ accountAccessPage.revokeGrant("broker-app");
+ } catch (NoSuchElementException e) {
+ System.err.println("Couldn't revoke broker-app application, maybe because it wasn't granted or user not logged");
+ }
+
+ driver.navigate().to(currentUrl);
+ }
+
@Override
protected void doAfterProviderAuthentication() {
// grant access to broker-app
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
index e645ca3..644fff3 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
@@ -28,7 +28,9 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.events.Details;
import org.keycloak.events.Event;
+import org.keycloak.events.EventType;
import org.keycloak.models.BrowserSecurityHeaders;
+import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
@@ -39,6 +41,7 @@ import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
@@ -48,8 +51,10 @@ import org.openqa.selenium.WebDriver;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Response;
+
import java.util.Map;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -93,6 +98,9 @@ public class LoginTest {
@WebResource
protected LoginPage loginPage;
+
+ @WebResource
+ protected LoginPasswordUpdatePage updatePasswordPage;
private static String userId;
@@ -219,7 +227,86 @@ public class LoginTest {
events.expectLogin().user(userId).removeDetail(Details.USERNAME).detail(Details.AUTH_METHOD, "sso").assertEvent();
}
+
+ @Test
+ public void loginWithForcePasswordChangePolicy() {
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setPasswordPolicy(new PasswordPolicy("forceExpiredPasswordChange(1)"));
+ }
+ });
+
+ try {
+ // Setting offset to more than one day to force password update
+ // elapsedTime > timeToExpire
+ Time.setOffset(86405);
+
+ loginPage.open();
+
+ loginPage.login("login-test", "password");
+
+ updatePasswordPage.assertCurrent();
+
+ updatePasswordPage.changePassword("updatedPassword", "updatedPassword");
+
+ events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+
+ assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+
+ } finally {
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setPasswordPolicy(new PasswordPolicy(null));
+
+ UserModel user = manager.getSession().users().getUserByUsername("login-test", appRealm);
+ UserCredentialModel cred = new UserCredentialModel();
+ cred.setType(CredentialRepresentation.PASSWORD);
+ cred.setValue("password");
+ user.updateCredential(cred);
+ }
+ });
+ Time.setOffset(0);
+ }
+ }
+
+ @Test
+ public void loginWithoutForcePasswordChangePolicy() {
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setPasswordPolicy(new PasswordPolicy("forceExpiredPasswordChange(1)"));
+ }
+ });
+
+ try {
+ // Setting offset to less than one day to avoid forced password update
+ // elapsedTime < timeToExpire
+ Time.setOffset(86205);
+
+ loginPage.open();
+ loginPage.login("login-test", "password");
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+
+ events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+
+ } finally {
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setPasswordPolicy(new PasswordPolicy(null));
+ }
+ });
+ Time.setOffset(0);
+ }
+ }
+
@Test
public void loginNoTimeoutWithLongWait() {
try {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index f4e2c93..68abb4a 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -155,7 +155,7 @@ public class ResetPasswordTest {
events.expect(EventType.RESET_PASSWORD_ERROR).client((String) null).user((String) null).error("invalid_code").clearDetails().assertEvent();
assertTrue(errorPage.isCurrent());
- assertEquals("Unknown code, please login again through your application.", errorPage.getError());
+ assertEquals("An error occurred, please login again through your application.", errorPage.getError());
}
@Test
@@ -354,7 +354,7 @@ public class ResetPasswordTest {
errorPage.assertCurrent();
- assertEquals("Invalid code, please login again through your application.", errorPage.getError());
+ assertEquals("An error occurred, please login again through your application.", errorPage.getError());
events.expectRequiredAction(EventType.RESET_PASSWORD).error("invalid_code").client((String) null).user((String) null).session((String) null).clearDetails().assertEvent();
} finally {
@@ -538,19 +538,35 @@ public class ResetPasswordTest {
}
});
- resetPassword("login-test", "password1");
- resetPasswordInvalidPassword("login-test", "password1", "Invalid password: must not be equal to any of last 3 passwords.");
-
- resetPassword("login-test", "password2");
- resetPasswordInvalidPassword("login-test", "password1", "Invalid password: must not be equal to any of last 3 passwords.");
- resetPasswordInvalidPassword("login-test", "password2", "Invalid password: must not be equal to any of last 3 passwords.");
-
- resetPassword("login-test", "password3");
- resetPasswordInvalidPassword("login-test", "password1", "Invalid password: must not be equal to any of last 3 passwords.");
- resetPasswordInvalidPassword("login-test", "password2", "Invalid password: must not be equal to any of last 3 passwords.");
- resetPasswordInvalidPassword("login-test", "password3", "Invalid password: must not be equal to any of last 3 passwords.");
-
- resetPassword("login-test", "password");
+ try {
+ Time.setOffset(2000000);
+ resetPassword("login-test", "password1");
+
+ resetPasswordInvalidPassword("login-test", "password1", "Invalid password: must not be equal to any of last 3 passwords.");
+
+ Time.setOffset(4000000);
+ resetPassword("login-test", "password2");
+
+ resetPasswordInvalidPassword("login-test", "password1", "Invalid password: must not be equal to any of last 3 passwords.");
+ resetPasswordInvalidPassword("login-test", "password2", "Invalid password: must not be equal to any of last 3 passwords.");
+
+ Time.setOffset(8000000);
+ resetPassword("login-test", "password3");
+
+ resetPasswordInvalidPassword("login-test", "password1", "Invalid password: must not be equal to any of last 3 passwords.");
+ resetPasswordInvalidPassword("login-test", "password2", "Invalid password: must not be equal to any of last 3 passwords.");
+ resetPasswordInvalidPassword("login-test", "password3", "Invalid password: must not be equal to any of last 3 passwords.");
+
+ resetPassword("login-test", "password");
+ } finally {
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setPasswordPolicy(new PasswordPolicy(null));
+ }
+ });
+ Time.setOffset(0);
+ }
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/AccountPageTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/AccountPageTest.java
index 23b7398..d6055cc 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/AccountPageTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/AccountPageTest.java
@@ -77,7 +77,7 @@ public class AccountPageTest {
accountUpdateProfilePage.openLanguage("German");
Assert.assertEquals("Deutsch", accountUpdateProfilePage.getLanguageDropdownText());
- accountUpdateProfilePage.openLanguage("Englisch");
+ accountUpdateProfilePage.openLanguage("English");
Assert.assertEquals("English", accountUpdateProfilePage.getLanguageDropdownText());
accountUpdateProfilePage.logout();
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
index 39bfd99..2d0f9eb 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
@@ -75,7 +75,7 @@ public class LoginPageTest {
loginPage.openLanguage("German");
Assert.assertEquals("Deutsch", loginPage.getLanguageDropdownText());
- loginPage.openLanguage("Englisch");
+ loginPage.openLanguage("English");
Assert.assertEquals("English", loginPage.getLanguageDropdownText());
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GrantedConsentModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GrantedConsentModelTest.java
new file mode 100644
index 0000000..5c555b7
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GrantedConsentModelTest.java
@@ -0,0 +1,266 @@
+package org.keycloak.testsuite.model;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.GrantedConsentModel;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleContainerModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.UserPropertyMapper;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GrantedConsentModelTest extends AbstractModelTest {
+
+ @Before
+ public void setupEnv() {
+ RealmModel realm = realmManager.createRealm("original");
+
+ ClientModel fooClient = realm.addClient("foo-client");
+ ClientModel barClient = realm.addClient("bar-client");
+
+ RoleModel realmRole = realm.addRole("realm-role");
+ RoleModel barClientRole = barClient.addRole("bar-client-role");
+
+ ProtocolMapperModel fooMapper = new ProtocolMapperModel();
+ fooMapper.setName("foo");
+ fooMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ fooMapper.setProtocolMapper(UserPropertyMapper.PROVIDER_ID);
+ fooMapper = fooClient.addProtocolMapper(fooMapper);
+
+ ProtocolMapperModel barMapper = new ProtocolMapperModel();
+ barMapper.setName("bar");
+ barMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ barMapper.setProtocolMapper(UserPropertyMapper.PROVIDER_ID);
+ barMapper = barClient.addProtocolMapper(barMapper);
+
+ UserModel john = session.users().addUser(realm, "john");
+ UserModel mary = session.users().addUser(realm, "mary");
+
+ GrantedConsentModel johnFooGrant = new GrantedConsentModel(fooClient.getId());
+ johnFooGrant.addGrantedRole(realmRole.getId());
+ johnFooGrant.addGrantedRole(barClientRole.getId());
+ johnFooGrant.addGrantedProtocolMapper(fooMapper.getId());
+ john.addGrantedConsent(johnFooGrant);
+
+ GrantedConsentModel johnBarGrant = new GrantedConsentModel(barClient.getId());
+ johnBarGrant.addGrantedProtocolMapper(barMapper.getId());
+ johnBarGrant.addGrantedRole(realmRole.getId());
+
+ // Update should fail as grant doesn't yet exists
+ try {
+ john.updateGrantedConsent(johnBarGrant);
+ Assert.fail("Not expected to end here");
+ } catch (ModelException expected) {
+ }
+
+ john.addGrantedConsent(johnBarGrant);
+
+ GrantedConsentModel maryFooGrant = new GrantedConsentModel(fooClient.getId());
+ maryFooGrant.addGrantedRole(realmRole.getId());
+ maryFooGrant.addGrantedProtocolMapper(fooMapper.getId());
+ mary.addGrantedConsent(maryFooGrant);
+
+ commit();
+ }
+
+ @Test
+ public void basicConsentTest() {
+ RealmModel realm = realmManager.getRealm("original");
+ Map<String, ClientModel> clients = realm.getClientNameMap();
+ ClientModel fooClient = clients.get("foo-client");
+ ClientModel barClient = clients.get("bar-client");
+
+ UserModel john = session.users().getUserByUsername("john", realm);
+ UserModel mary = session.users().getUserByUsername("mary", realm);
+
+ GrantedConsentModel johnFooConsent = john.getGrantedConsentByClient(fooClient.getId());
+ Assert.assertEquals(johnFooConsent.getGrantedRoles().size(), 2);
+ Assert.assertEquals(johnFooConsent.getGrantedProtocolMappers().size(), 1);
+ Assert.assertTrue(isRoleGranted(realm, "realm-role", johnFooConsent));
+ Assert.assertTrue(isRoleGranted(barClient, "bar-client-role", johnFooConsent));
+ Assert.assertTrue(isMapperGranted(fooClient, "foo", johnFooConsent));
+
+ GrantedConsentModel johnBarConsent = john.getGrantedConsentByClient(barClient.getId());
+ Assert.assertEquals(johnBarConsent.getGrantedRoles().size(), 1);
+ Assert.assertEquals(johnBarConsent.getGrantedProtocolMappers().size(), 1);
+ Assert.assertTrue(isRoleGranted(realm, "realm-role", johnBarConsent));
+ Assert.assertTrue(isMapperGranted(barClient, "bar", johnBarConsent));
+
+ GrantedConsentModel maryConsent = mary.getGrantedConsentByClient(fooClient.getId());
+ Assert.assertEquals(maryConsent.getGrantedRoles().size(), 1);
+ Assert.assertEquals(maryConsent.getGrantedProtocolMappers().size(), 1);
+ Assert.assertTrue(isRoleGranted(realm, "realm-role", maryConsent));
+ Assert.assertFalse(isRoleGranted(barClient, "bar-client-role", maryConsent));
+ Assert.assertTrue(isMapperGranted(fooClient, "foo", maryConsent));
+
+ Assert.assertNull(mary.getGrantedConsentByClient(barClient.getId()));
+ }
+
+ @Test
+ public void getAllConsentTest() {
+ RealmModel realm = realmManager.getRealm("original");
+ Map<String, ClientModel> clients = realm.getClientNameMap();
+ ClientModel fooClient = clients.get("foo-client");
+
+ UserModel john = session.users().getUserByUsername("john", realm);
+ UserModel mary = session.users().getUserByUsername("mary", realm);
+
+ List<GrantedConsentModel> johnConsents = john.getGrantedConsents();
+ Assert.assertEquals(2, johnConsents.size());
+
+ List<GrantedConsentModel> maryConsents = mary.getGrantedConsents();
+ Assert.assertEquals(1, maryConsents.size());
+ GrantedConsentModel maryConsent = maryConsents.get(0);
+ Assert.assertEquals(maryConsent.getClientId(), fooClient.getId());
+ Assert.assertEquals(maryConsent.getGrantedRoles().size(), 1);
+ Assert.assertEquals(maryConsent.getGrantedProtocolMappers().size(), 1);
+ Assert.assertTrue(isRoleGranted(realm, "realm-role", maryConsent));
+ Assert.assertTrue(isMapperGranted(fooClient, "foo", maryConsent));
+ }
+
+ @Test
+ public void updateWithRoleRemovalTest() {
+ RealmModel realm = realmManager.getRealm("original");
+ ClientModel fooClient = realm.getClientNameMap().get("foo-client");
+ UserModel john = session.users().getUserByUsername("john", realm);
+
+ GrantedConsentModel johnConsent = john.getGrantedConsentByClient(fooClient.getId());
+
+ // Remove foo protocol mapper from johnConsent
+ ProtocolMapperModel protMapperModel = fooClient.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, "foo");
+ johnConsent.getGrantedProtocolMappers().remove(protMapperModel.getId());
+
+ // Remove realm-role and add new-realm-role to johnConsent
+ RoleModel realmRole = realm.getRole("realm-role");
+ johnConsent.getGrantedRoles().remove(realmRole.getId());
+
+ RoleModel newRealmRole = realm.addRole("new-realm-role");
+ johnConsent.addGrantedRole(newRealmRole.getId());
+
+ john.updateGrantedConsent(johnConsent);
+
+ commit();
+
+ realm = realmManager.getRealm("original");
+ fooClient = realm.getClientNameMap().get("foo-client");
+ john = session.users().getUserByUsername("john", realm);
+ johnConsent = john.getGrantedConsentByClient(fooClient.getId());
+
+ Assert.assertEquals(johnConsent.getGrantedRoles().size(), 2);
+ Assert.assertEquals(johnConsent.getGrantedProtocolMappers().size(), 0);
+ Assert.assertFalse(isRoleGranted(realm, "realm-role", johnConsent));
+ Assert.assertTrue(isRoleGranted(realm, "new-realm-role", johnConsent));
+ Assert.assertFalse(isMapperGranted(fooClient, "foo", johnConsent));
+ }
+
+ @Test
+ public void revokeTest() {
+ RealmModel realm = realmManager.getRealm("original");
+ ClientModel fooClient = realm.getClientNameMap().get("foo-client");
+ UserModel john = session.users().getUserByUsername("john", realm);
+
+ john.revokeGrantedConsentForClient(fooClient.getId());
+
+ commit();
+
+ realm = realmManager.getRealm("original");
+ john = session.users().getUserByUsername("john", realm);
+ Assert.assertNull(john.getGrantedConsentByClient(fooClient.getId()));
+ }
+
+ @Test
+ public void deleteUserTest() {
+ // Validate user deleted without any referential constraint errors
+ RealmModel realm = realmManager.getRealm("original");
+ UserModel john = session.users().getUserByUsername("john", realm);
+ session.users().removeUser(realm, john);
+ }
+
+ @Test
+ public void deleteProtocolMapperTest() {
+ RealmModel realm = realmManager.getRealm("original");
+ ClientModel fooClient = realm.getClientNameMap().get("foo-client");
+ ProtocolMapperModel fooMapper = fooClient.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, "foo");
+ String fooMapperId = fooMapper.getId();
+ fooClient.removeProtocolMapper(fooMapper);
+
+ commit();
+
+ realm = realmManager.getRealm("original");
+ fooClient = realm.getClientNameMap().get("foo-client");
+ UserModel john = session.users().getUserByUsername("john", realm);
+ GrantedConsentModel johnConsent = john.getGrantedConsentByClient(fooClient.getId());
+
+ Assert.assertEquals(johnConsent.getGrantedRoles().size(), 2);
+ Assert.assertEquals(johnConsent.getGrantedProtocolMappers().size(), 0);
+ Assert.assertFalse(johnConsent.isProtocolMapperGranted(fooMapperId));
+ }
+
+ @Test
+ public void deleteRoleTest() {
+ RealmModel realm = realmManager.getRealm("original");
+ RoleModel realmRole = realm.getRole("realm-role");
+ String realmRoleId = realmRole.getId();
+ realm.removeRole(realmRole);
+
+ commit();
+
+ realm = realmManager.getRealm("original");
+ Map<String, ClientModel> clients = realm.getClientNameMap();
+ ClientModel fooClient = clients.get("foo-client");
+ ClientModel barClient = clients.get("bar-client");
+ UserModel john = session.users().getUserByUsername("john", realm);
+ GrantedConsentModel johnConsent = john.getGrantedConsentByClient(fooClient.getId());
+
+ Assert.assertEquals(johnConsent.getGrantedRoles().size(), 1);
+ Assert.assertEquals(johnConsent.getGrantedProtocolMappers().size(), 1);
+ Assert.assertFalse(johnConsent.isRoleGranted(realmRoleId));
+ Assert.assertTrue(isRoleGranted(barClient, "bar-client-role", johnConsent));
+ }
+
+ @Test
+ public void deleteClientTest() {
+ RealmModel realm = realmManager.getRealm("original");
+ Map<String, ClientModel> clients = realm.getClientNameMap();
+ ClientModel barClient = clients.get("bar-client");
+ realm.removeClient(barClient.getId());
+
+ commit();
+
+ realm = realmManager.getRealm("original");
+ clients = realm.getClientNameMap();
+ ClientModel fooClient = clients.get("foo-client");
+ Assert.assertNull(clients.get("bar-client"));
+
+ UserModel john = session.users().getUserByUsername("john", realm);
+
+ GrantedConsentModel johnFooConsent = john.getGrantedConsentByClient(fooClient.getId());
+ Assert.assertEquals(johnFooConsent.getGrantedRoles().size(), 1);
+ Assert.assertEquals(johnFooConsent.getGrantedProtocolMappers().size(), 1);
+ Assert.assertTrue(isRoleGranted(realm, "realm-role", johnFooConsent));
+ Assert.assertTrue(isMapperGranted(fooClient, "foo", johnFooConsent));
+
+ Assert.assertNull(john.getGrantedConsentByClient(barClient.getId()));
+ }
+
+ private boolean isRoleGranted(RoleContainerModel roleContainer, String roleName, GrantedConsentModel consentModel) {
+ RoleModel role = roleContainer.getRole(roleName);
+ return consentModel.isRoleGranted(role.getId());
+ }
+
+ private boolean isMapperGranted(ClientModel client, String protocolMapperName, GrantedConsentModel consentModel) {
+ ProtocolMapperModel protocolMapper = client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, protocolMapperName);
+ return consentModel.isProtocolMapperGranted(protocolMapper.getId());
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountAccessPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountAccessPage.java
new file mode 100644
index 0000000..99b591a
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountAccessPage.java
@@ -0,0 +1,98 @@
+package org.keycloak.testsuite.pages;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.core.UriBuilder;
+
+import org.keycloak.services.Urls;
+import org.keycloak.testsuite.Constants;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AccountAccessPage extends AbstractAccountPage {
+
+ private String path = Urls.accountAccessPage(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT).build(), "test").toString();
+
+ @Override
+ public boolean isCurrent() {
+ return driver.getTitle().contains("Account Management") && driver.getCurrentUrl().endsWith("/account/access");
+ }
+
+ @Override
+ public void open() {
+ driver.navigate().to(path);
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public void revokeGrant(String clientId) {
+ driver.findElement(By.id("revoke-" + clientId)).click();
+ }
+
+ public Map<String, ClientGrant> getClientGrants() {
+ Map<String, ClientGrant> table = new HashMap<String, ClientGrant>();
+ for (WebElement r : driver.findElements(By.tagName("tr"))) {
+ int count = 0;
+ ClientGrant currentGrant = null;
+
+ for (WebElement col : r.findElements(By.tagName("td"))) {
+ count++;
+ switch (count) {
+ case 1:
+ currentGrant = new ClientGrant();
+ String clientId = col.getText();
+ table.put(clientId, currentGrant);
+ break;
+ case 2:
+ String protMappersStr = col.getText();
+ String[] protMappers = protMappersStr.split(",");
+ for (String protMapper : protMappers) {
+ protMapper = protMapper.trim();
+ currentGrant.addMapper(protMapper);
+ }
+ break;
+ case 3:
+ String rolesStr = col.getText();
+ String[] roles = rolesStr.split(",");
+ for (String role : roles) {
+ role = role.trim();
+ currentGrant.addRole(role);
+ }
+ break;
+ }
+ }
+ }
+ table.remove("Client");
+ return table;
+ }
+
+ public static class ClientGrant {
+
+ private final List<String> protocolMapperDescriptions = new ArrayList<String>();
+ private final List<String> roleDescriptions = new ArrayList<String>();
+
+ private void addMapper(String protocolMapper) {
+ protocolMapperDescriptions.add(protocolMapper);
+ }
+
+ private void addRole(String role) {
+ roleDescriptions.add(role);
+ }
+
+ public List<String> getProtocolMapperDescriptions() {
+ return protocolMapperDescriptions;
+ }
+
+ public List<String> getRoleDescriptions() {
+ return roleDescriptions;
+ }
+ }
+}
diff --git a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-kc-oidc.json b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-kc-oidc.json
index 01a1c2c..cf0f508 100755
--- a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-kc-oidc.json
+++ b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-kc-oidc.json
@@ -32,7 +32,10 @@
{ "type" : "password",
"value" : "password" }
],
- "realmRoles": ["manager"]
+ "realmRoles": ["manager"],
+ "applicationRoles": {
+ "account": [ "manage-account" ]
+ }
},
{
"username" : "test-user-noemail",
@@ -43,7 +46,10 @@
{ "type" : "password",
"value" : "password" }
],
- "realmRoles": ["manager"]
+ "realmRoles": ["manager"],
+ "applicationRoles": {
+ "account": [ "manage-account" ]
+ }
},
{
"username" : "pedroigor",
@@ -53,7 +59,10 @@
{ "type" : "password",
"value" : "password" }
],
- "realmRoles": ["manager"]
+ "realmRoles": ["manager"],
+ "applicationRoles": {
+ "account": [ "manage-account" ]
+ }
}
],
"roles" : {