keycloak-aplcache
Changes
.gitignore 4(+4 -0)
.travis.yml 26(+10 -16)
adapters/oidc/adapter-core/pom.xml 2(+1 -1)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java 1(+1 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java 31(+26 -5)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java 4(+3 -1)
adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java 1(+1 -0)
adapters/oidc/as7-eap6/pom.xml 2(+1 -1)
adapters/oidc/cli-sso/login.sh 10(+10 -0)
adapters/oidc/cli-sso/logout.sh 9(+9 -0)
adapters/oidc/cli-sso/pom.xml 84(+84 -0)
adapters/oidc/cli-sso/README.md 9(+9 -0)
adapters/oidc/installed/pom.xml 2(+1 -1)
adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java 266(+266 -0)
adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java 152(+139 -13)
adapters/oidc/jetty/jetty8.1/pom.xml 2(+1 -1)
adapters/oidc/jetty/jetty9.1/pom.xml 2(+1 -1)
adapters/oidc/jetty/jetty9.2/pom.xml 2(+1 -1)
adapters/oidc/jetty/jetty9.3/pom.xml 2(+1 -1)
adapters/oidc/jetty/jetty9.4/pom.xml 2(+1 -1)
adapters/oidc/jetty/pom.xml 2(+1 -1)
adapters/oidc/js/pom.xml 2(+1 -1)
adapters/oidc/osgi-adapter/pom.xml 2(+1 -1)
adapters/oidc/pom.xml 3(+2 -1)
adapters/oidc/servlet-filter/pom.xml 2(+1 -1)
adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java 84(+54 -30)
adapters/oidc/spring-boot/pom.xml 2(+1 -1)
adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java 6(+5 -1)
adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacadeTest.java 41(+41 -0)
adapters/oidc/tomcat/pom.xml 2(+1 -1)
adapters/oidc/tomcat/tomcat6/pom.xml 2(+1 -1)
adapters/oidc/tomcat/tomcat7/pom.xml 2(+1 -1)
adapters/oidc/tomcat/tomcat8/pom.xml 2(+1 -1)
adapters/oidc/undertow/pom.xml 2(+1 -1)
adapters/oidc/wildfly/pom.xml 2(+1 -1)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java 56(+56 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java 2(+2 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java 85(+85 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java 61(+61 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java 38(+38 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java 44(+44 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java 36(+36 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties 8(+7 -1)
adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml 2(+2 -0)
adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java 5(+5 -0)
adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java 2(+1 -1)
adapters/pom.xml 2(+1 -1)
adapters/saml/as7-eap6/pom.xml 2(+1 -1)
adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/FormattingXMLStreamWriter.java 10(+5 -5)
adapters/saml/core/pom.xml 2(+1 -1)
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java 5(+2 -3)
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java 6(+3 -3)
adapters/saml/core-public/pom.xml 2(+1 -1)
adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java 17(+16 -1)
adapters/saml/jetty/jetty8.1/pom.xml 2(+1 -1)
adapters/saml/jetty/jetty9.1/pom.xml 2(+1 -1)
adapters/saml/jetty/jetty9.2/pom.xml 2(+1 -1)
adapters/saml/jetty/jetty9.3/pom.xml 2(+1 -1)
adapters/saml/jetty/jetty9.4/pom.xml 2(+1 -1)
adapters/saml/jetty/pom.xml 2(+1 -1)
adapters/saml/pom.xml 2(+1 -1)
adapters/saml/servlet-filter/pom.xml 2(+1 -1)
adapters/saml/tomcat/pom.xml 2(+1 -1)
adapters/saml/tomcat/tomcat6/pom.xml 2(+1 -1)
adapters/saml/tomcat/tomcat7/pom.xml 2(+1 -1)
adapters/saml/tomcat/tomcat8/pom.xml 2(+1 -1)
adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/SamlAuthenticatorValve.java 10(+10 -0)
adapters/saml/undertow/pom.xml 2(+1 -1)
adapters/saml/wildfly/pom.xml 2(+1 -1)
adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java 15(+8 -7)
adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java 2(+1 -1)
adapters/spi/adapter-spi/pom.xml 2(+1 -1)
adapters/spi/pom.xml 2(+1 -1)
authz/client/pom.xml 2(+1 -1)
authz/policy/common/pom.xml 2(+1 -1)
authz/policy/drools/pom.xml 2(+1 -1)
authz/policy/pom.xml 2(+1 -1)
authz/pom.xml 2(+1 -1)
boms/adapter/pom.xml 40(+20 -20)
boms/pom.xml 2(+1 -1)
boms/spi/pom.xml 6(+3 -3)
common/pom.xml 2(+1 -1)
core/pom.xml 2(+1 -1)
dependencies/pom.xml 2(+1 -1)
dependencies/server-all/pom.xml 2(+1 -1)
dependencies/server-min/pom.xml 2(+1 -1)
distribution/adapters/osgi/pom.xml 2(+1 -1)
distribution/adapters/pom.xml 2(+1 -1)
distribution/api-docs-dist/pom.xml 32(+24 -8)
distribution/demo-dist/pom.xml 2(+1 -1)
distribution/downloads/pom.xml 2(+1 -1)
distribution/examples-dist/pom.xml 2(+1 -1)
distribution/feature-packs/pom.xml 2(+1 -1)
distribution/pom.xml 2(+1 -1)
distribution/proxy-dist/pom.xml 2(+1 -1)
distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml 1(+1 -0)
distribution/saml-adapters/pom.xml 2(+1 -1)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml 1(+1 -0)
distribution/server-dist/pom.xml 14(+13 -1)
distribution/server-overlay/pom.xml 2(+1 -1)
examples/admin-client/pom.xml 2(+1 -1)
examples/authz/hello-world/pom.xml 2(+1 -1)
examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java 4(+2 -2)
examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java 2(+1 -1)
examples/authz/photoz/pom.xml 2(+1 -1)
examples/authz/pom.xml 2(+1 -1)
examples/authz/servlet-authz/pom.xml 2(+1 -1)
examples/basic-auth/pom.xml 2(+1 -1)
examples/broker/pom.xml 2(+1 -1)
examples/cors/pom.xml 2(+1 -1)
examples/demo-template/pom.xml 2(+1 -1)
examples/fuse/camel/pom.xml 2(+1 -1)
examples/fuse/cxf-jaxrs/pom.xml 2(+1 -1)
examples/fuse/cxf-jaxws/pom.xml 2(+1 -1)
examples/fuse/features/pom.xml 2(+1 -1)
examples/fuse/pom.xml 2(+1 -1)
examples/js-console/pom.xml 2(+1 -1)
examples/kerberos/pom.xml 2(+1 -1)
examples/ldap/pom.xml 2(+1 -1)
examples/multi-tenant/pom.xml 2(+1 -1)
examples/pom.xml 2(+1 -1)
examples/providers/pom.xml 2(+1 -1)
examples/providers/rest/pom.xml 2(+1 -1)
examples/saml/pom.xml 2(+1 -1)
examples/saml/servlet-filter/pom.xml 2(+1 -1)
examples/themes/pom.xml 2(+1 -1)
federation/kerberos/pom.xml 2(+1 -1)
federation/ldap/pom.xml 2(+1 -1)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java 2(+1 -1)
federation/pom.xml 2(+1 -1)
federation/sssd/pom.xml 2(+1 -1)
integration/admin-client/pom.xml 2(+1 -1)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java 7(+7 -0)
integration/client-cli/pom.xml 2(+1 -1)
integration/pom.xml 2(+1 -1)
misc/keycloak-test-helper/pom.xml 2(+1 -1)
misc/pom.xml 2(+1 -1)
misc/spring-boot-starter/pom.xml 4(+2 -2)
misc/Testsuite.md 9(+9 -0)
model/infinispan/pom.xml 2(+1 -1)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java 6(+6 -0)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java 43(+39 -4)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java 36(+17 -19)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java 134(+103 -31)
model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java 16(+15 -1)
model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java 89(+77 -12)
model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java 17(+17 -0)
model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java 4(+2 -2)
model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java 10(+5 -5)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java 11(+6 -5)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java 2(+1 -1)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java 6(+6 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java 10(+5 -5)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java 9(+4 -5)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java 156(+119 -37)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java 38(+38 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java 242(+242 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java 104(+104 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java 155(+155 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java 81(+81 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshEvent.java 73(+73 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java 112(+112 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStore.java 101(+101 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStoreFactory.java 74(+74 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/SessionData.java 74(+74 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdatesList.java 60(+28 -32)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java 87(+87 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionClientSessionUpdateTask.java 51(+51 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java 38(+38 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java 55(+54 -1)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java 10(+9 -1)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java 134(+132 -2)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractAuthSessionClusterListener.java 64(+64 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractUserSessionClusterListener.java 82(+82 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/ClientRemovedSessionEvent.java 43(+43 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RealmRemovedSessionEvent.java 17(+3 -14)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RemoveAllUserLoginFailuresEvent.java 23(+3 -20)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RemoveUserSessionsEvent.java 24(+24 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionClusterEvent.java 81(+81 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionEventsSenderTransaction.java 73(+73 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java 6(+1 -5)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java 28(+14 -14)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java 63(+52 -11)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java 58(+53 -5)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java 8(+4 -4)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java 11(+9 -2)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 519(+389 -130)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java 206(+188 -18)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/BaseCacheInitializer.java 159(+159 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/CacheInitializer.java 55(+55 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/DBLockBasedCacheInitializer.java 81(+81 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanCacheInitializer.java 141(+12 -129)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentUserSessionLoader.java 49(+47 -2)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java 8(+4 -4)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java 42(+41 -1)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SingleWorkerCacheInitializer.java 51(+51 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserLoginFailureMapper.java 69(+0 -69)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionMapper.java 120(+0 -120)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionNoteMapper.java 88(+0 -88)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStore.java 105(+105 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfiguration.java 40(+40 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfigurationBuilder.java 39(+39 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java 166(+166 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java 208(+208 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java 120(+120 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java 58(+40 -18)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/SessionPredicate.java 7(+4 -3)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java 11(+4 -7)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java 131(+89 -42)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/InfinispanUtil.java 95(+95 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java 164(+164 -0)
model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheClientListenersTest.java 239(+239 -0)
model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java 59(+5 -54)
model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java 292(+292 -0)
model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java 354(+354 -0)
model/infinispan/src/test/java/org/keycloak/cluster/infinispan/TestCacheManagerFactory.java 85(+85 -0)
model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java 253(+253 -0)
model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java 216(+216 -0)
model/jpa/pom.xml 2(+1 -1)
model/pom.xml 2(+1 -1)
pom.xml 11(+9 -2)
proxy/launcher/pom.xml 2(+1 -1)
proxy/pom.xml 2(+1 -1)
proxy/proxy-server/pom.xml 2(+1 -1)
saml-core/pom.xml 2(+1 -1)
saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLAssertionWriter.java 11(+10 -1)
saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java 11(+10 -1)
saml-core-api/pom.xml 2(+1 -1)
server-spi/pom.xml 2(+1 -1)
server-spi-private/pom.xml 2(+1 -1)
server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java 5(+0 -5)
server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java 25(+25 -0)
services/pom.xml 8(+7 -1)
services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java 7(+4 -3)
services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java 21(+21 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java 12(+12 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java 34(+27 -7)
services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java 12(+12 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java 31(+27 -4)
services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java 2(+1 -1)
services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java 72(+61 -11)
services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java 2(+1 -1)
services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerCertFileUtils.java 37(+37 -0)
services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java 62(+62 -0)
services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeYamlFile.java 70(+70 -0)
services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeZipContent.java 35(+35 -0)
services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java 148(+148 -0)
services/src/main/java/org/keycloak/protocol/docker/installation/DockerRegistryConfigFileInstallationProvider.java 81(+81 -0)
services/src/main/java/org/keycloak/protocol/docker/installation/DockerVariableOverrideInstallationProvider.java 81(+81 -0)
services/src/main/java/org/keycloak/protocol/docker/mapper/AllowAllDockerProtocolMapper.java 52(+52 -0)
services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2AttributeMapper.java 15(+15 -0)
services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2ProtocolMapper.java 51(+51 -0)
services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java 28(+16 -12)
services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java 2(+1 -1)
services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java 4(+3 -1)
services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java 209(+72 -137)
services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticatorFactory.java 115(+115 -0)
services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java 5(+4 -1)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java 19(+18 -1)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java 40(+34 -6)
services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java 2(+0 -2)
services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java 1(+1 -0)
services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java 5(+5 -0)
services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java 4(+4 -0)
services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java 8(+8 -0)
services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java 184(+168 -16)
services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java 12(+9 -3)
services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java 23(+22 -1)
services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java 12(+10 -2)
services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java 18(+10 -8)
services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProviderFactory.java 46(+46 -0)
services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory 3(+2 -1)
services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory 2(+2 -0)
services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider 4(+3 -1)
services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java 193(+193 -0)
services/src/test/java/org/keycloak/procotol/docker/installation/DockerKeyIdentifierTest.java 41(+41 -0)
services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-above-signature.xml 89(+89 -0)
services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-in-signature.xml 88(+88 -0)
testsuite/integration/pom.xml 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java 13(+9 -4)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java 3(+3 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java 6(+3 -3)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java 6(+3 -3)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java 6(+3 -3)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java 6(+3 -3)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java 28(+27 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java 137(+114 -23)
testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/ClusterProviderTaskCommand.java 76(+76 -0)
testsuite/integration-arquillian/HOW-TO-RUN.md 162(+159 -3)
testsuite/integration-arquillian/pom.xml 10(+8 -2)
testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config-auth.cli 2(+1 -1)
testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl 12(+11 -1)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/JGroupsStats.java 84(+84 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/RemoteCacheStats.java 67(+67 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java 60(+60 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java 25(+22 -3)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/runonserver/SerializationUtil.java 3(+2 -1)
testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java 6(+3 -3)
testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl 25(+21 -4)
testsuite/integration-arquillian/test-apps/cors/database-service/src/main/java/org/keycloak/example/oauth/ProductService.java 8(+8 -0)
testsuite/integration-arquillian/test-apps/cors/database-service/src/main/webapp/WEB-INF/keycloak.json 3(+2 -1)
testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java 3(+2 -1)
testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/AdminController.java 59(+59 -0)
testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/SpringBootAdapterApplication.java 12(+12 -0)
testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/application.properties 12(+12 -0)
testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/admin/index.html 12(+12 -0)
testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/index.html 12(+12 -0)
testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/tokens.html 11(+11 -0)
testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/test/java/org/keycloak/SpringBootAdapterApplicationTests.java 16(+16 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AngularCorsProductTestApp.java 6(+6 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostServlet.java 1(+1 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java 3(+2 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java 3(+2 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AppServerTestEnricher.java 8(+8 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java 9(+2 -7)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java 26(+15 -11)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java 73(+69 -4)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java 10(+10 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java 4(+2 -2)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java 20(+20 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java 5(+5 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java 5(+5 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/crossdc/DC.java 31(+31 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java 17(+17 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ProceedPage.java 51(+51 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java 4(+4 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java 52(+47 -5)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java 34(+16 -18)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/IdentityProviderAttributeUpdater.java 55(+55 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/SetSystemProperty.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java 4(+4 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/IOUtil.java 1(+0 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java 0(+0 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/Matchers.java 0(+0 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/HttpResponseBodyMatcher.java 0(+0 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java 0(+0 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java 0(+0 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java 0(+0 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java 0(+0 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java 0(+0 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java 0(+0 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java 1(+0 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java 92(+71 -21)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateAuthnRequestStepBuilder.java 108(+108 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java 116(+116 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/IdPInitiatedLoginBuilder.java 52(+52 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/LoginBuilder.java 144(+144 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/ModifySamlResponseStepBuilder.java 227(+227 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/RequiredConsentBuilder.java 128(+128 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/SamlDocumentStepBuilder.java 147(+147 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java 400(+73 -327)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java 139(+139 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java 5(+5 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java 56(+49 -7)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java 13(+11 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseAdminAdapterTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/cors/AbstractCorsExampleAdapterTest.java 1(+1 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java 12(+7 -5)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java 156(+110 -46)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowClientInitiatedAccountLinkTest.java 12(+12 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java 9(+9 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java 1(+1 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java 13(+12 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java 17(+6 -11)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java 111(+83 -28)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java 127(+127 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceServerManagementTest.java 68(+68 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java 115(+60 -55)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java 339(+169 -170)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java 289(+192 -97)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java 201(+164 -37)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java 8(+8 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ImpersonationDisabledTest.java 7(+5 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.java 122(+122 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java 122(+119 -3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java 27(+16 -11)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java 38(+20 -18)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java 81(+70 -11)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java 17(+17 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java 116(+116 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java 11(+7 -4)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java 84(+84 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java 158(+141 -17)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java 26(+17 -9)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java 113(+113 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java 239(+239 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LoginCrossDCTest.java 56(+56 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java 396(+396 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerClientTest.java 200(+200 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostIpSupplier.java 45(+45 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostVersionSupplier.java 43(+43 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerTestRealmSetup.java 87(+87 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerVersion.java 99(+99 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java 25(+24 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java 57(+54 -3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java 39(+37 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java 15(+15 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java 33(+33 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriStateTest.java 68(+68 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java 8(+8 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java 58(+58 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java 204(+204 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java 37(+30 -7)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java 3(+1 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/RunOnServerTest.java 6(+6 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/ServerVersion.java 22(+22 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java 41(+13 -28)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java 19(+11 -8)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java 3(+2 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java 53(+25 -28)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java 253(+127 -126)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java 25(+14 -11)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/LastSessionRefreshUnitTest.java 178(+178 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserManager.java 6(+6 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json 1(+1 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/client-with-authz-settings.json 866(+866 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/docker-realm-public-key.pem 17(+17 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt 35(+35 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.key 51(+51 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/docker-compose.yaml 15(+15 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker 45(+45 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/registry-config.txt 6(+6 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/variable-override.txt 4(+4 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/docker-test-realm.json 1315(+1315 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json 10(+2 -8)
testsuite/integration-arquillian/tests/other/adapters/was/was8/src/test/java/org/keycloak/testsuite/adapter/WASSAMLFilterAdapterTest.java 9(+9 -0)
testsuite/integration-arquillian/tests/other/adapters/wls/wls12/src/test/java/org/keycloak/testsuite/adapter/WLSSAMLFilterAdapterTest.java 9(+9 -0)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java 56(+56 -0)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java 21(+16 -5)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java 28(+16 -12)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java 19(+16 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java 16(+14 -2)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/users/Users.java 2(+1 -1)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java 49(+49 -0)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/AggregatePolicyManagementTest.java 16(+16 -0)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ClientPolicyManagementTest.java 18(+17 -1)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java 19(+19 -0)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/JSPolicyManagementTest.java 16(+16 -0)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java 11(+10 -1)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourcePermissionManagementTest.java 17(+17 -0)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RolePolicyManagementTest.java 18(+18 -0)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RulePolicyManagementTest.java 12(+12 -0)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java 9(+9 -0)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopePermissionManagementTest.java 17(+17 -0)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/TimePolicyManagementTest.java 27(+27 -0)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/UserPolicyManagementTest.java 18(+17 -1)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java 4(+1 -3)
testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringAdminPage.java 22(+22 -0)
testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringApplicationPage.java 40(+40 -0)
testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/TokenPage.java 19(+19 -0)
testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java 217(+217 -0)
testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java 61(+61 -0)
testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java 154(+154 -0)
testsuite/integration-arquillian/tests/pom.xml 21(+11 -10)
testsuite/integration-arquillian/test-utils/src/main/java/org/keycloak/testsuite/util/junit/AggregateResultsReporter.java 277(+0 -277)
testsuite/jetty/jetty81/pom.xml 2(+1 -1)
testsuite/jetty/jetty91/pom.xml 2(+1 -1)
testsuite/jetty/jetty92/pom.xml 2(+1 -1)
testsuite/jetty/jetty93/pom.xml 2(+1 -1)
testsuite/jetty/jetty94/pom.xml 2(+1 -1)
testsuite/jetty/pom.xml 2(+1 -1)
testsuite/pom.xml 3(+2 -1)
testsuite/proxy/pom.xml 2(+1 -1)
testsuite/tomcat6/pom.xml 2(+1 -1)
testsuite/tomcat7/pom.xml 2(+1 -1)
testsuite/tomcat8/pom.xml 2(+1 -1)
testsuite/utils/pom.xml 36(+36 -0)
themes/pom.xml 45(+44 -1)
themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html 14(+13 -1)
themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-permissions.html 2(+1 -1)
themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-role-permissions.html 2(+1 -1)
themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/group-permissions.html 2(+1 -1)
themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/realm-role-permissions.html 2(+1 -1)
themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/users-permissions.html 2(+1 -1)
themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html 1(+0 -1)
themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-bitbucket.html 130(+130 -0)
themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-gitlab.html 130(+130 -0)
themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html 2(+1 -1)
themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-cookies.js 321(+0 -321)
themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-loader.js 443(+0 -443)
themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-resource.js 669(+0 -669)
themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-sanitize.js 683(+0 -683)
themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate.js 2904(+0 -2904)
themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-loader-url.js 75(+0 -75)
themes/src/main/resources/theme/keycloak/common/resources/lib/autofill-event/autofill-event-1.0.0.js 117(+0 -117)
themes/src/main/resources/theme/keycloak/common/resources/lib/jquery/jquery-1.10.2.js 9789(+0 -9789)
themes/src/main/resources/theme/keycloak/common/resources/lib/select2-3.4.1/select2.css 680(+0 -680)
themes/src/main/resources/theme/keycloak/common/resources/lib/select2-3.4.1/select2.js 3137(+0 -3137)
themes/src/main/resources/theme/keycloak/common/resources/lib/select2-3.4.1/select2.min.js 22(+0 -22)
themes/src/main/resources/theme/keycloak/common/resources/lib/select2-3.4.1/select2-spinner.gif 0(+0 -0)
travis-run-tests.sh 52(+25 -27)
util/embedded-ldap/pom.xml 2(+1 -1)
util/pom.xml 2(+1 -1)
wildfly/adduser/pom.xml 2(+1 -1)
wildfly/extensions/pom.xml 2(+1 -1)
wildfly/pom.xml 2(+1 -1)
wildfly/server-subsystem/pom.xml 2(+1 -1)
Details
.gitignore 4(+4 -0)
diff --git a/.gitignore b/.gitignore
index 32e898b..bf5d094 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,3 +49,7 @@ target
# Maven shade
#############
*dependency-reduced-pom.xml
+
+# nodejs #
+##########
+node_modules
\ No newline at end of file
.travis.yml 26(+10 -16)
diff --git a/.travis.yml b/.travis.yml
index ec6ed80..d3ea4f1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,33 +1,27 @@
language: java
+dist: precise
cache:
- directories:
- - $HOME/.m2
-
-before_cache:
- - rm -rf $HOME/.m2/repository/org/keycloak
+ cache: false
env:
global:
- MAVEN_SKIP_RC=true
- - MAVEN_OPTS="-Xms512m -Xmx2048m"
+ - MAVEN_OPTS="-Xms512m -Xmx1536m"
matrix:
- - TESTS=group1
- - TESTS=group2
- - TESTS=group3
- - TESTS=group4
+ - TESTS=unit
+ - TESTS=server-group1
+ - TESTS=server-group2
+ - TESTS=server-group3
+ - TESTS=server-group4
- TESTS=old
jdk:
- oraclejdk8
-before_script:
- - export MAVEN_SKIP_RC=true
-
-install:
- - travis_wait 60 mvn install --no-snapshot-updates -Pdistribution -DskipTestsuite -B -V -q
+install: true
-script:
+script:
- ./travis-run-tests.sh $TESTS
sudo: false
adapters/oidc/adapter-core/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/adapter-core/pom.xml b/adapters/oidc/adapter-core/pom.xml
index c2afe45..a2f5b5b 100755
--- a/adapters/oidc/adapter-core/pom.xml
+++ b/adapters/oidc/adapter-core/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
index 45c4557..d5761bc 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
@@ -91,6 +91,8 @@ public class KeycloakDeployment {
// https://tools.ietf.org/html/rfc7636
protected boolean pkce = false;
protected boolean ignoreOAuthQueryParameter;
+
+ protected Map<String, String> redirectRewriteRules;
public KeycloakDeployment() {
}
@@ -446,4 +448,14 @@ public class KeycloakDeployment {
public boolean isOAuthQueryParameterEnabled() {
return !this.ignoreOAuthQueryParameter;
}
+
+ public Map<String, String> getRedirectRewriteRules() {
+ return redirectRewriteRules;
+ }
+
+ public void setRewriteRedirectRules(Map<String, String> redirectRewriteRules) {
+ this.redirectRewriteRules = redirectRewriteRules;
+ }
+
+
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
index eca6849..7fca1f1 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
@@ -116,6 +116,7 @@ public class KeycloakDeploymentBuilder {
deployment.setMinTimeBetweenJwksRequests(adapterConfig.getMinTimeBetweenJwksRequests());
deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl());
deployment.setIgnoreOAuthQueryParameter(adapterConfig.isIgnoreOAuthQueryParameter());
+ deployment.setRewriteRedirectRules(adapterConfig.getRedirectRewriteRules());
if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) {
throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url");
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
index fe8dfdc..ee3f214 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
@@ -25,7 +25,6 @@ import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.common.VerificationException;
-import org.keycloak.common.util.Encode;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.UriUtils;
import org.keycloak.constants.AdapterConstants;
@@ -38,7 +37,10 @@ import org.keycloak.representations.IDToken;
import org.keycloak.util.TokenUtil;
import java.io.IOException;
-import java.util.concurrent.atomic.AtomicLong;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Map;
+import java.util.logging.Level;
/**
@@ -141,6 +143,7 @@ public class OAuthRequestAuthenticator {
protected String getRedirectUri(String state) {
String url = getRequestUrl();
log.debugf("callback uri: %s", url);
+
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
int port = sslRedirectPort();
if (port < 0) {
@@ -170,7 +173,7 @@ public class OAuthRequestAuthenticator {
KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone()
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
- .queryParam(OAuth2Constants.REDIRECT_URI, Encode.encodeQueryParamAsIs(url)) // Need to encode uri ourselves as queryParam() will not encode % characters.
+ .queryParam(OAuth2Constants.REDIRECT_URI, rewrittenRedirectUri(url))
.queryParam(OAuth2Constants.STATE, state)
.queryParam("login", "true");
if(loginHint != null && loginHint.length() > 0){
@@ -320,10 +323,11 @@ public class OAuthRequestAuthenticator {
AccessTokenResponse tokenResponse = null;
strippedOauthParametersRequestUri = stripOauthParametersFromRedirect();
+
try {
// For COOKIE store we don't have httpSessionId and single sign-out won't be available
String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.changeHttpSessionId(true) : null;
- tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri, httpSessionId);
+ tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, rewrittenRedirectUri(strippedOauthParametersRequestUri), httpSessionId);
} catch (ServerRequest.HttpFailure failure) {
log.error("failed to turn code into token");
log.error("status from server: " + failure.getStatus());
@@ -375,6 +379,23 @@ public class OAuthRequestAuthenticator {
.replaceQueryParam(OAuth2Constants.STATE, null);
return builder.build().toString();
}
-
+
+ private String rewrittenRedirectUri(String originalUri) {
+ Map<String, String> rewriteRules = deployment.getRedirectRewriteRules();
+ if(rewriteRules != null && !rewriteRules.isEmpty()) {
+ try {
+ URL url = new URL(originalUri);
+ Map.Entry<String, String> rule = rewriteRules.entrySet().iterator().next();
+ StringBuilder redirectUriBuilder = new StringBuilder(url.getProtocol());
+ redirectUriBuilder.append("://"+ url.getAuthority());
+ redirectUriBuilder.append(url.getPath().replaceFirst(rule.getKey(), rule.getValue()));
+ return redirectUriBuilder.toString();
+ } catch (MalformedURLException ex) {
+ log.error("Not a valid request url");
+ throw new RuntimeException(ex);
+ }
+ }
+ return originalUri;
+ }
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java
index c70bce1..fa1cefd 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java
@@ -155,7 +155,9 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
this.refreshToken = response.getRefreshToken();
}
this.tokenString = tokenString;
- tokenStore.refreshCallback(this);
+ if (tokenStore != null) {
+ tokenStore.refreshCallback(this);
+ }
return true;
}
diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
index cd191e2..af58b33 100644
--- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
+++ b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
@@ -75,6 +75,7 @@ public class KeycloakDeploymentBuilderTest {
assertEquals(10, deployment.getTokenMinimumTimeToLive());
assertEquals(20, deployment.getMinTimeBetweenJwksRequests());
assertEquals(120, deployment.getPublicKeyCacheTtl());
+ assertEquals("/api/$1", deployment.getRedirectRewriteRules().get("^/wsmaster/api/(.*)$"));
}
@Test
diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak.json b/adapters/oidc/adapter-core/src/test/resources/keycloak.json
index 521b8a9..e1b8881 100644
--- a/adapters/oidc/adapter-core/src/test/resources/keycloak.json
+++ b/adapters/oidc/adapter-core/src/test/resources/keycloak.json
@@ -33,5 +33,8 @@
"token-minimum-time-to-live": 10,
"min-time-between-jwks-requests": 20,
"public-key-cache-ttl": 120,
- "ignore-oauth-query-parameter": true
+ "ignore-oauth-query-parameter": true,
+ "redirect-rewrite-rules" : {
+ "^/wsmaster/api/(.*)$" : "/api/$1"
+ }
}
\ No newline at end of file
diff --git a/adapters/oidc/as7-eap6/as7-adapter/pom.xml b/adapters/oidc/as7-eap6/as7-adapter/pom.xml
index ed8c3c2..47e344f 100755
--- a/adapters/oidc/as7-eap6/as7-adapter/pom.xml
+++ b/adapters/oidc/as7-eap6/as7-adapter/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-as7-integration-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/oidc/as7-eap6/as7-adapter-spi/pom.xml b/adapters/oidc/as7-eap6/as7-adapter-spi/pom.xml
index 0ed189d..89faf8b 100755
--- a/adapters/oidc/as7-eap6/as7-adapter-spi/pom.xml
+++ b/adapters/oidc/as7-eap6/as7-adapter-spi/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-as7-integration-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/oidc/as7-eap6/as7-subsystem/pom.xml b/adapters/oidc/as7-eap6/as7-subsystem/pom.xml
index de9b424..5a2d24e 100755
--- a/adapters/oidc/as7-eap6/as7-subsystem/pom.xml
+++ b/adapters/oidc/as7-eap6/as7-subsystem/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-as7-integration-pom</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
adapters/oidc/as7-eap6/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/as7-eap6/pom.xml b/adapters/oidc/as7-eap6/pom.xml
index 3d3a174..4b5a427 100755
--- a/adapters/oidc/as7-eap6/pom.xml
+++ b/adapters/oidc/as7-eap6/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak AS7 / JBoss EAP 6 Integration</name>
adapters/oidc/cli-sso/login.sh 10(+10 -0)
diff --git a/adapters/oidc/cli-sso/login.sh b/adapters/oidc/cli-sso/login.sh
new file mode 100755
index 0000000..ff33a01
--- /dev/null
+++ b/adapters/oidc/cli-sso/login.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+export KC_AUTH_SERVER=http://localhost:8080/auth
+export KC_REALM=master
+export KC_CLIENT=cli
+
+export KC_ACCESS_TOKEN=`java -DKEYCLOAK_AUTH_SERVER=$KC_AUTH_SERVER -DKEYCLOAK_REALM=$KC_REALM -DKEYCLOAK_CLIENT=$KC_CLIENT -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar login`
+
+
+
+
adapters/oidc/cli-sso/logout.sh 9(+9 -0)
diff --git a/adapters/oidc/cli-sso/logout.sh b/adapters/oidc/cli-sso/logout.sh
new file mode 100644
index 0000000..ca99f88
--- /dev/null
+++ b/adapters/oidc/cli-sso/logout.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+java -DKEYCLOAK_AUTH_SERVER=$KC_AUTH_SERVER -DKEYCLOAK_REALM=$KC_REALM -DKEYCLOAK_CLIENT=$KC_CLIENT -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar logout
+
+unset KC_ACCESS_TOKEN
+
+
+
+
adapters/oidc/cli-sso/pom.xml 84(+84 -0)
diff --git a/adapters/oidc/cli-sso/pom.xml b/adapters/oidc/cli-sso/pom.xml
new file mode 100755
index 0000000..216c3b7
--- /dev/null
+++ b/adapters/oidc/cli-sso/pom.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0"?>
+<!--
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>3.3.0.CR1-SNAPSHOT</version>
+ <relativePath>../../../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-cli-sso</artifactId>
+ <name>Keycloak CLI SSO Framework</name>
+ <description/>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-installed-adapter</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>${maven.compiler.source}</source>
+ <target>${maven.compiler.target}</target>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>3.0.0</version>
+ <configuration>
+ <transformers>
+ <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+ <mainClass>org.keycloak.adapters.KeycloakCliSsoMain</mainClass>
+ </transformer>
+ </transformers>
+
+ <filters>
+ <filter>
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>META-INF/*.SF</exclude>
+ <exclude>META-INF/*.DSA</exclude>
+ <exclude>META-INF/*.RSA</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
adapters/oidc/cli-sso/README.md 9(+9 -0)
diff --git a/adapters/oidc/cli-sso/README.md b/adapters/oidc/cli-sso/README.md
new file mode 100755
index 0000000..fb0fdbe
--- /dev/null
+++ b/adapters/oidc/cli-sso/README.md
@@ -0,0 +1,9 @@
+CLI Single Sign On
+===================================
+
+This java-based utility is meant for providing Keycloak integration to
+command line applications that are either written in Java or another language. The
+idea is that the Java app provided by this utility performs a login for a specific
+client, parses responses, and exports an access token as an environment variable
+that can be used by the command line utility you are accessing.
+
diff --git a/adapters/oidc/cli-sso/src/main/java/org/keycloak/adapters/KeycloakCliSsoMain.java b/adapters/oidc/cli-sso/src/main/java/org/keycloak/adapters/KeycloakCliSsoMain.java
new file mode 100644
index 0000000..3aaeb9b
--- /dev/null
+++ b/adapters/oidc/cli-sso/src/main/java/org/keycloak/adapters/KeycloakCliSsoMain.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters;
+
+import org.keycloak.adapters.installed.KeycloakCliSso;
+import org.keycloak.adapters.installed.KeycloakInstalled;
+import org.keycloak.common.util.Time;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class KeycloakCliSsoMain extends KeycloakCliSso {
+
+ public static void main(String[] args) throws Exception {
+ new KeycloakCliSsoMain().mainCmd(args);
+ }
+}
adapters/oidc/installed/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/installed/pom.xml b/adapters/oidc/installed/pom.xml
index b509be4..3c14afa 100755
--- a/adapters/oidc/installed/pom.xml
+++ b/adapters/oidc/installed/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java
new file mode 100644
index 0000000..3c1d365
--- /dev/null
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters.installed;
+
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.adapters.ServerRequest;
+import org.keycloak.common.util.Time;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ *
+ *
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class KeycloakCliSso {
+
+ public void mainCmd(String[] args) throws Exception {
+ if (args.length != 1) {
+ printHelp();
+ return;
+ }
+
+ if (args[0].equalsIgnoreCase("login")) {
+ login();
+ } else if (args[0].equalsIgnoreCase("login-manual")) {
+ loginManual();
+ } else if (args[0].equalsIgnoreCase("token")) {
+ token();
+ } else if (args[0].equalsIgnoreCase("logout")) {
+ logout();
+ } else if (args[0].equalsIgnoreCase("env")) {
+ System.out.println(System.getenv().toString());
+ } else {
+ printHelp();
+ }
+ }
+
+
+ public void printHelp() {
+ System.err.println("Commands:");
+ System.err.println(" login - login with desktop browser if available, otherwise do manual login. Output is access token.");
+ System.err.println(" login-manual - manual login");
+ System.err.println(" token - print access token if logged in");
+ System.err.println(" logout - logout.");
+ System.exit(1);
+ }
+
+ public AdapterConfig getConfig() {
+ String url = System.getProperty("KEYCLOAK_AUTH_SERVER");
+ if (url == null) {
+ System.err.println("KEYCLOAK_AUTH_SERVER property not set");
+ System.exit(1);
+ }
+ String realm = System.getProperty("KEYCLOAK_REALM");
+ if (realm == null) {
+ System.err.println("KEYCLOAK_REALM property not set");
+ System.exit(1);
+
+ }
+ String client = System.getProperty("KEYCLOAK_CLIENT");
+ if (client == null) {
+ System.err.println("KEYCLOAK_CLIENT property not set");
+ System.exit(1);
+ }
+ String secret = System.getProperty("KEYCLOAK_CLIENT_SECRET");
+
+
+
+ AdapterConfig config = new AdapterConfig();
+ config.setAuthServerUrl(url);
+ config.setRealm(realm);
+ config.setResource(client);
+ config.setSslRequired("external");
+ if (secret != null) {
+ Map<String, Object> creds = new HashMap<>();
+ creds.put("secret", secret);
+ config.setCredentials(creds);
+ } else {
+ config.setPublicClient(true);
+ }
+ return config;
+ }
+
+ public boolean checkToken() throws Exception {
+ String token = getTokenResponse();
+ if (token == null) return false;
+
+
+ if (token != null) {
+ Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
+ if (m.find()) {
+ String json = m.group(0);
+ try {
+ AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
+ if (Time.currentTime() < tokenResponse.getExpiresIn()) {
+ return true;
+ }
+ AdapterConfig config = getConfig();
+ KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
+ installed.refreshToken(tokenResponse.getRefreshToken());
+ processResponse(installed);
+ return true;
+ } catch (Exception e) {
+ System.err.println("Error processing existing token");
+ e.printStackTrace();
+ }
+
+ }
+ }
+ return false;
+
+ }
+
+ private String getTokenResponse() throws IOException {
+ String token = null;
+ File tokenFile = getTokenFilePath();
+ if (tokenFile.exists()) {
+ FileInputStream fis = new FileInputStream(tokenFile);
+ byte[] data = new byte[(int) tokenFile.length()];
+ fis.read(data);
+ fis.close();
+ token = new String(data, "UTF-8");
+ }
+ return token;
+ }
+
+ public void token() throws Exception {
+ String token = getTokenResponse();
+ if (token == null) {
+ System.err.println("There is no token for client");
+ System.exit(1);
+ } else {
+ Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
+ if (m.find()) {
+ String json = m.group(0);
+ try {
+ AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
+ if (Time.currentTime() < tokenResponse.getExpiresIn()) {
+ System.out.println(tokenResponse.getToken());
+ return;
+ } else {
+ System.err.println("token in response file is expired");
+ System.exit(1);
+ }
+ } catch (Exception e) {
+ System.err.println("Failure processing token response file");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ } else {
+ System.err.println("Could not find json within token response file");
+ System.exit(1);
+ }
+ }
+ }
+
+ public void login() throws Exception {
+ if (checkToken()) return;
+ AdapterConfig config = getConfig();
+ KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
+ installed.login();
+ processResponse(installed);
+ }
+
+ public String getHome() {
+ String home = System.getenv("HOME");
+ if (home == null) {
+ home = System.getProperty("HOME");
+ if (home == null) {
+ home = Paths.get("").toAbsolutePath().normalize().toString();
+ }
+ }
+ return home;
+ }
+
+ public File getTokenDirectory() {
+ return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM")).toFile();
+ }
+
+ public File getTokenFilePath() {
+ return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM"), System.getProperty("KEYCLOAK_CLIENT") + ".json").toFile();
+ }
+
+ private void processResponse(KeycloakInstalled installed) throws IOException {
+ AccessTokenResponse tokenResponse = installed.getTokenResponse();
+ tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
+ tokenResponse.setIdToken(null);
+ String output = JsonSerialization.writeValueAsString(tokenResponse);
+ getTokenDirectory().mkdirs();
+ FileOutputStream fos = new FileOutputStream(getTokenFilePath());
+ fos.write(output.getBytes("UTF-8"));
+ fos.flush();
+ fos.close();
+ System.out.println(tokenResponse.getToken());
+ }
+
+ public void loginManual() throws Exception {
+ if (checkToken()) return;
+ AdapterConfig config = getConfig();
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
+ KeycloakInstalled installed = new KeycloakInstalled(deployment);
+ installed.loginManual();
+ processResponse(installed);
+ }
+
+ public void logout() throws Exception {
+ String token = getTokenResponse();
+ if (token != null) {
+ Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
+ if (m.find()) {
+ String json = m.group(0);
+ try {
+ AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
+ if (Time.currentTime() > tokenResponse.getExpiresIn()) {
+ System.err.println("Login is expired");
+ System.exit(1);
+ }
+ AdapterConfig config = getConfig();
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
+ ServerRequest.invokeLogout(deployment, tokenResponse.getRefreshToken());
+ for (File fp : getTokenDirectory().listFiles()) fp.delete();
+ System.out.println("logout complete");
+ } catch (Exception e) {
+ System.err.println("Failure processing token response file");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ } else {
+ System.err.println("Could not find json within token response file");
+ System.exit(1);
+ }
+ } else {
+ System.err.println("Not logged in");
+ System.exit(1);
+ }
+ }
+}
diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
index 9834fe2..61ca06e 100644
--- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
@@ -17,6 +17,7 @@
package org.keycloak.adapters.installed;
+import org.apache.commons.codec.Charsets;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.adapters.KeycloakDeployment;
@@ -24,6 +25,7 @@ import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.ServerRequest;
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier;
import org.keycloak.common.VerificationException;
+import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.AccessToken;
@@ -43,6 +45,7 @@ import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
+import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@@ -51,6 +54,11 @@ import java.util.concurrent.TimeUnit;
*/
public class KeycloakInstalled {
+ public interface HttpResponseWriter {
+ void success(PrintWriter pw, KeycloakInstalled ki);
+ void failure(PrintWriter pw, KeycloakInstalled ki);
+ }
+
private static final String KEYCLOAK_JSON = "META-INF/keycloak.json";
private KeycloakDeployment deployment;
@@ -59,12 +67,18 @@ public class KeycloakInstalled {
LOGGED_MANUAL, LOGGED_DESKTOP
}
+ private AccessTokenResponse tokenResponse;
private String tokenString;
private String idTokenString;
private IDToken idToken;
private AccessToken token;
private String refreshToken;
private Status status;
+ private Locale locale;
+ private HttpResponseWriter loginResponseWriter;
+ private HttpResponseWriter logoutResponseWriter;
+
+
public KeycloakInstalled() {
InputStream config = Thread.currentThread().getContextClassLoader().getResourceAsStream(KEYCLOAK_JSON);
@@ -75,6 +89,92 @@ public class KeycloakInstalled {
deployment = KeycloakDeploymentBuilder.build(config);
}
+ public KeycloakInstalled(KeycloakDeployment deployment) {
+ this.deployment = deployment;
+ }
+
+ private static HttpResponseWriter defaultLoginWriter = new HttpResponseWriter() {
+ @Override
+ public void success(PrintWriter pw, KeycloakInstalled ki) {
+ pw.println("HTTP/1.1 200 OK");
+ pw.println("Content-Type: text/html");
+ pw.println();
+ pw.println("<html><h1>Login completed.</h1><div>");
+ pw.println("This browser will remain logged in until you close it, logout, or the session expires.");
+ pw.println("</div></html>");
+ pw.flush();
+
+ }
+
+ @Override
+ public void failure(PrintWriter pw, KeycloakInstalled ki) {
+ pw.println("HTTP/1.1 200 OK");
+ pw.println("Content-Type: text/html");
+ pw.println();
+ pw.println("<html><h1>Login attempt failed.</h1><div>");
+ pw.println("</div></html>");
+ pw.flush();
+
+ }
+ };
+ private static HttpResponseWriter defaultLogoutWriter = new HttpResponseWriter() {
+ @Override
+ public void success(PrintWriter pw, KeycloakInstalled ki) {
+ pw.println("HTTP/1.1 200 OK");
+ pw.println("Content-Type: text/html");
+ pw.println();
+ pw.println("<html><h1>Logout completed.</h1><div>");
+ pw.println("You may close this browser tab.");
+ pw.println("</div></html>");
+ pw.flush();
+
+ }
+
+ @Override
+ public void failure(PrintWriter pw, KeycloakInstalled ki) {
+ pw.println("HTTP/1.1 200 OK");
+ pw.println("Content-Type: text/html");
+ pw.println();
+ pw.println("<html><h1>Logout failed.</h1><div>");
+ pw.println("You may close this browser tab.");
+ pw.println("</div></html>");
+ pw.flush();
+
+ }
+ };
+
+ public HttpResponseWriter getLoginResponseWriter() {
+ if (loginResponseWriter == null) {
+ return defaultLoginWriter;
+ } else {
+ return loginResponseWriter;
+ }
+ }
+
+ public HttpResponseWriter getLogoutResponseWriter() {
+ if (logoutResponseWriter == null) {
+ return defaultLogoutWriter;
+ } else {
+ return logoutResponseWriter;
+ }
+ }
+
+ public void setLoginResponseWriter(HttpResponseWriter loginResponseWriter) {
+ this.loginResponseWriter = loginResponseWriter;
+ }
+
+ public void setLogoutResponseWriter(HttpResponseWriter logoutResponseWriter) {
+ this.logoutResponseWriter = logoutResponseWriter;
+ }
+
+ public Locale getLocale() {
+ return locale;
+ }
+
+ public void setLocale(Locale locale) {
+ this.locale = locale;
+ }
+
public void login() throws IOException, ServerRequest.HttpFailure, VerificationException, InterruptedException, OAuthErrorException, URISyntaxException {
if (isDesktopSupported()) {
loginDesktop();
@@ -108,19 +208,22 @@ public class KeycloakInstalled {
}
public void loginDesktop() throws IOException, VerificationException, OAuthErrorException, URISyntaxException, ServerRequest.HttpFailure, InterruptedException {
- CallbackListener callback = new CallbackListener();
+ CallbackListener callback = new CallbackListener(getLoginResponseWriter());
callback.start();
String redirectUri = "http://localhost:" + callback.server.getLocalPort();
String state = UUID.randomUUID().toString();
- String authUrl = deployment.getAuthUrl().clone()
+ KeycloakUriBuilder builder = deployment.getAuthUrl().clone()
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
.queryParam(OAuth2Constants.STATE, state)
- .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
- .build().toString();
+ .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID);
+ if (locale != null) {
+ builder.queryParam(OAuth2Constants.UI_LOCALES_PARAM, locale.getLanguage());
+ }
+ String authUrl = builder.build().toString();
Desktop.getDesktop().browse(new URI(authUrl));
@@ -144,7 +247,7 @@ public class KeycloakInstalled {
}
private void logoutDesktop() throws IOException, URISyntaxException, InterruptedException {
- CallbackListener callback = new CallbackListener();
+ CallbackListener callback = new CallbackListener(getLogoutResponseWriter());
callback.start();
String redirectUri = "http://localhost:" + callback.server.getLocalPort();
@@ -167,9 +270,6 @@ public class KeycloakInstalled {
}
public void loginManual(PrintStream printer, Reader reader) throws IOException, ServerRequest.HttpFailure, VerificationException {
- CallbackListener callback = new CallbackListener();
- callback.start();
-
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
String authUrl = deployment.getAuthUrl().clone()
@@ -208,7 +308,14 @@ public class KeycloakInstalled {
parseAccessToken(tokenResponse);
}
+ public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException {
+ AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
+ parseAccessToken(tokenResponse);
+
+ }
+
private void parseAccessToken(AccessTokenResponse tokenResponse) throws VerificationException {
+ this.tokenResponse = tokenResponse;
tokenString = tokenResponse.getToken();
refreshToken = tokenResponse.getRefreshToken();
idTokenString = tokenResponse.getIdToken();
@@ -240,6 +347,10 @@ public class KeycloakInstalled {
return refreshToken;
}
+ public AccessTokenResponse getTokenResponse() {
+ return tokenResponse;
+ }
+
public boolean isDesktopSupported() {
return Desktop.isDesktopSupported();
}
@@ -248,6 +359,8 @@ public class KeycloakInstalled {
return deployment;
}
+
+
private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null);
parseAccessToken(tokenResponse);
@@ -269,6 +382,7 @@ public class KeycloakInstalled {
return sb.toString();
}
+
public class CallbackListener extends Thread {
private ServerSocket server;
@@ -283,14 +397,19 @@ public class KeycloakInstalled {
private String state;
- public CallbackListener() throws IOException {
+ private Socket socket;
+
+ private HttpResponseWriter writer;
+
+ public CallbackListener(HttpResponseWriter writer) throws IOException {
+ this.writer = writer;
server = new ServerSocket(0);
}
@Override
public void run() {
try {
- Socket socket = server.accept();
+ socket = server.accept();
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String request = br.readLine();
@@ -314,10 +433,15 @@ public class KeycloakInstalled {
}
}
- PrintWriter pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
- pw.println("Please close window and return to application");
- pw.flush();
+ OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream());
+ PrintWriter pw = new PrintWriter(out);
+ if (error == null) {
+ writer.success(pw, KeycloakInstalled.this);
+ } else {
+ writer.failure(pw, KeycloakInstalled.this);
+ }
+ pw.flush();
socket.close();
} catch (IOException e) {
errorException = e;
@@ -328,6 +452,8 @@ public class KeycloakInstalled {
} catch (IOException e) {
}
}
+
}
+
}
diff --git a/adapters/oidc/jaxrs-oauth-client/pom.xml b/adapters/oidc/jaxrs-oauth-client/pom.xml
index bcd585f..6f46e7c 100755
--- a/adapters/oidc/jaxrs-oauth-client/pom.xml
+++ b/adapters/oidc/jaxrs-oauth-client/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/oidc/jetty/jetty8.1/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/jetty/jetty8.1/pom.xml b/adapters/oidc/jetty/jetty8.1/pom.xml
index 7a3fa0c..3124755 100755
--- a/adapters/oidc/jetty/jetty8.1/pom.xml
+++ b/adapters/oidc/jetty/jetty8.1/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/oidc/jetty/jetty9.1/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/jetty/jetty9.1/pom.xml b/adapters/oidc/jetty/jetty9.1/pom.xml
index 1c3edd0..c5a4784 100755
--- a/adapters/oidc/jetty/jetty9.1/pom.xml
+++ b/adapters/oidc/jetty/jetty9.1/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/oidc/jetty/jetty9.2/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/jetty/jetty9.2/pom.xml b/adapters/oidc/jetty/jetty9.2/pom.xml
index 6d34e8b..f8b6335 100755
--- a/adapters/oidc/jetty/jetty9.2/pom.xml
+++ b/adapters/oidc/jetty/jetty9.2/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/oidc/jetty/jetty9.3/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/jetty/jetty9.3/pom.xml b/adapters/oidc/jetty/jetty9.3/pom.xml
index 0077b8d..c4cb374 100644
--- a/adapters/oidc/jetty/jetty9.3/pom.xml
+++ b/adapters/oidc/jetty/jetty9.3/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/oidc/jetty/jetty9.4/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/jetty/jetty9.4/pom.xml b/adapters/oidc/jetty/jetty9.4/pom.xml
index 377ddc0..acb36c6 100644
--- a/adapters/oidc/jetty/jetty9.4/pom.xml
+++ b/adapters/oidc/jetty/jetty9.4/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/oidc/jetty/jetty-core/pom.xml b/adapters/oidc/jetty/jetty-core/pom.xml
index 2f0ad16..3ee1c5e 100755
--- a/adapters/oidc/jetty/jetty-core/pom.xml
+++ b/adapters/oidc/jetty/jetty-core/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/oidc/jetty/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/jetty/pom.xml b/adapters/oidc/jetty/pom.xml
index ede26fc..5ec0c16 100755
--- a/adapters/oidc/jetty/pom.xml
+++ b/adapters/oidc/jetty/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak Jetty Integration</name>
adapters/oidc/js/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/js/pom.xml b/adapters/oidc/js/pom.xml
index 7d939c7..8b4cc67 100755
--- a/adapters/oidc/js/pom.xml
+++ b/adapters/oidc/js/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/oidc/js/src/main/resources/keycloak.js b/adapters/oidc/js/src/main/resources/keycloak.js
index 89b15b8..a784936 100755
--- a/adapters/oidc/js/src/main/resources/keycloak.js
+++ b/adapters/oidc/js/src/main/resources/keycloak.js
@@ -33,6 +33,13 @@
interval: 5
};
+ var scripts = document.getElementsByTagName('script');
+ for (var i = 0; i < scripts.length; i++) {
+ if ((scripts[i].src.indexOf('keycloak.js') !== -1 || scripts[i].src.indexOf('keycloak.min.js') !== -1) && scripts[i].src.indexOf('version=') !== -1) {
+ kc.iframeVersion = scripts[i].src.substring(scripts[i].src.indexOf('version=') + 8).split('&')[0];
+ }
+ }
+
kc.init = function (initOptions) {
kc.authenticated = false;
@@ -831,6 +838,10 @@
}
var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html';
+ if (kc.iframeVersion) {
+ src = src + '?version=' + kc.iframeVersion;
+ }
+
iframe.setAttribute('src', src );
iframe.style.display = 'none';
document.body.appendChild(iframe);
diff --git a/adapters/oidc/js/src/main/resources/login-status-iframe.html b/adapters/oidc/js/src/main/resources/login-status-iframe.html
index b1012f7..f58f76a 100755
--- a/adapters/oidc/js/src/main/resources/login-status-iframe.html
+++ b/adapters/oidc/js/src/main/resources/login-status-iframe.html
@@ -28,7 +28,7 @@
} else if (!init) {
var req = new XMLHttpRequest();
- var url = location.href + "/init";
+ var url = location.href.split("?")[0] + "/init";
url += "?client_id=" + encodeURIComponent(clientId);
url += "&origin=" + encodeURIComponent(origin);
adapters/oidc/osgi-adapter/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/osgi-adapter/pom.xml b/adapters/oidc/osgi-adapter/pom.xml
index 7bb5004..26a90ec 100755
--- a/adapters/oidc/osgi-adapter/pom.xml
+++ b/adapters/oidc/osgi-adapter/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/oidc/pom.xml 3(+2 -1)
diff --git a/adapters/oidc/pom.xml b/adapters/oidc/pom.xml
index d68f359..9207401 100755
--- a/adapters/oidc/pom.xml
+++ b/adapters/oidc/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<name>Keycloak OIDC Client Adapter Modules</name>
@@ -34,6 +34,7 @@
<module>adapter-core</module>
<module>as7-eap6</module>
<module>installed</module>
+ <module>cli-sso</module>
<module>jaxrs-oauth-client</module>
<module>jetty</module>
<module>js</module>
adapters/oidc/servlet-filter/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/servlet-filter/pom.xml b/adapters/oidc/servlet-filter/pom.xml
index 053710b..3d81dc7 100755
--- a/adapters/oidc/servlet-filter/pom.xml
+++ b/adapters/oidc/servlet-filter/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java
index 2763ff1..c51b9db 100755
--- a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java
+++ b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java
@@ -54,72 +54,96 @@ import java.util.regex.Pattern;
*/
public class KeycloakOIDCFilter implements Filter {
+ private final static Logger log = Logger.getLogger("" + KeycloakOIDCFilter.class);
+
public static final String SKIP_PATTERN_PARAM = "keycloak.config.skipPattern";
+ public static final String CONFIG_RESOLVER_PARAM = "keycloak.config.resolver";
+
+ public static final String CONFIG_FILE_PARAM = "keycloak.config.file";
+
+ public static final String CONFIG_PATH_PARAM = "keycloak.config.path";
+
protected AdapterDeploymentContext deploymentContext;
+
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
+
protected NodesRegistrationManagement nodesRegistrationManagement;
+
protected Pattern skipPattern;
- private final static Logger log = Logger.getLogger(""+KeycloakOIDCFilter.class);
+ private final KeycloakConfigResolver definedconfigResolver;
+
+ /**
+ * Constructor that can be used to define a {@code KeycloakConfigResolver} that will be used at initialization to
+ * provide the {@code KeycloakDeployment}.
+ * @param definedconfigResolver the resolver
+ */
+ public KeycloakOIDCFilter(KeycloakConfigResolver definedconfigResolver) {
+ this.definedconfigResolver = definedconfigResolver;
+ }
+
+ public KeycloakOIDCFilter() {
+ this(null);
+ }
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
-
String skipPatternDefinition = filterConfig.getInitParameter(SKIP_PATTERN_PARAM);
if (skipPatternDefinition != null) {
skipPattern = Pattern.compile(skipPatternDefinition, Pattern.DOTALL);
}
- String configResolverClass = filterConfig.getInitParameter("keycloak.config.resolver");
- if (configResolverClass != null) {
- try {
- KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance();
- deploymentContext = new AdapterDeploymentContext(configResolver);
- log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
- } catch (Exception ex) {
- log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
- deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
- }
+ if (definedconfigResolver != null) {
+ deploymentContext = new AdapterDeploymentContext(definedconfigResolver);
+ log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", definedconfigResolver.getClass());
} else {
- String fp = filterConfig.getInitParameter("keycloak.config.file");
- InputStream is = null;
- if (fp != null) {
+ String configResolverClass = filterConfig.getInitParameter(CONFIG_RESOLVER_PARAM);
+ if (configResolverClass != null) {
try {
- is = new FileInputStream(fp);
- } catch (FileNotFoundException e) {
- throw new RuntimeException(e);
+ KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance();
+ deploymentContext = new AdapterDeploymentContext(configResolver);
+ log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
+ } catch (Exception ex) {
+ log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
+ deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
}
} else {
- String path = "/WEB-INF/keycloak.json";
- String pathParam = filterConfig.getInitParameter("keycloak.config.path");
- if (pathParam != null) path = pathParam;
- is = filterConfig.getServletContext().getResourceAsStream(path);
+ String fp = filterConfig.getInitParameter(CONFIG_FILE_PARAM);
+ InputStream is = null;
+ if (fp != null) {
+ try {
+ is = new FileInputStream(fp);
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ String path = "/WEB-INF/keycloak.json";
+ String pathParam = filterConfig.getInitParameter(CONFIG_PATH_PARAM);
+ if (pathParam != null) path = pathParam;
+ is = filterConfig.getServletContext().getResourceAsStream(path);
+ }
+ KeycloakDeployment kd = createKeycloakDeploymentFrom(is);
+ deploymentContext = new AdapterDeploymentContext(kd);
+ log.fine("Keycloak is using a per-deployment configuration.");
}
- KeycloakDeployment kd = createKeycloakDeploymentFrom(is);
- deploymentContext = new AdapterDeploymentContext(kd);
- log.fine("Keycloak is using a per-deployment configuration.");
}
filterConfig.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
nodesRegistrationManagement = new NodesRegistrationManagement();
}
private KeycloakDeployment createKeycloakDeploymentFrom(InputStream is) {
-
if (is == null) {
log.fine("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
return new KeycloakDeployment();
}
-
return KeycloakDeploymentBuilder.build(is);
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
-
log.fine("Keycloak OIDC Filter");
- //System.err.println("Keycloak OIDC Filter: " + ((HttpServletRequest)req).getRequestURL().toString());
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
@@ -201,7 +225,7 @@ public class KeycloakOIDCFilter implements Filter {
*
* @param request the request to check
* @return {@code true} if the request should not be handled,
- * {@code false} otherwise.
+ * {@code false} otherwise.
*/
private boolean shouldSkip(HttpServletRequest request) {
diff --git a/adapters/oidc/servlet-oauth-client/pom.xml b/adapters/oidc/servlet-oauth-client/pom.xml
index 836186f..bb709f1 100755
--- a/adapters/oidc/servlet-oauth-client/pom.xml
+++ b/adapters/oidc/servlet-oauth-client/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/oidc/spring-boot/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/spring-boot/pom.xml b/adapters/oidc/spring-boot/pom.xml
index abd1512..6a720f6 100755
--- a/adapters/oidc/spring-boot/pom.xml
+++ b/adapters/oidc/spring-boot/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/oidc/spring-boot-container-bundle/pom.xml b/adapters/oidc/spring-boot-container-bundle/pom.xml
index 4f68494..49cab4e 100644
--- a/adapters/oidc/spring-boot-container-bundle/pom.xml
+++ b/adapters/oidc/spring-boot-container-bundle/pom.xml
@@ -4,7 +4,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<artifactId>spring-boot-container-bundle</artifactId>
diff --git a/adapters/oidc/spring-security/pom.xml b/adapters/oidc/spring-security/pom.xml
index d1a975a..b304cd5 100755
--- a/adapters/oidc/spring-security/pom.xml
+++ b/adapters/oidc/spring-security/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java
index cb9ddcd..2c9876e 100755
--- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java
+++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java
@@ -19,6 +19,9 @@ package org.keycloak.adapters.springsecurity.facade;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.OIDCHttpFacade;
+import org.keycloak.adapters.spi.KeycloakAccount;
+import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount;
+import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.Assert;
@@ -57,7 +60,8 @@ public class SimpleHttpFacade implements OIDCHttpFacade {
SecurityContext context = SecurityContextHolder.getContext();
if (context != null && context.getAuthentication() != null) {
- return (KeycloakSecurityContext) context.getAuthentication().getDetails();
+ KeycloakAuthenticationToken authentication = (KeycloakAuthenticationToken) context.getAuthentication();
+ return authentication.getAccount().getKeycloakSecurityContext();
}
return null;
diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacadeTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacadeTest.java
new file mode 100644
index 0000000..28c6ce8
--- /dev/null
+++ b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacadeTest.java
@@ -0,0 +1,41 @@
+package org.keycloak.adapters.springsecurity.facade;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
+import org.keycloak.adapters.spi.KeycloakAccount;
+import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount;
+import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
+import org.mockito.internal.util.collections.Sets;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.security.Principal;
+import java.util.Set;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+
+public class SimpleHttpFacadeTest {
+
+ @Before
+ public void setup() {
+ SecurityContext springSecurityContext = SecurityContextHolder.createEmptyContext();
+ SecurityContextHolder.setContext(springSecurityContext);
+ Set<String> roles = Sets.newSet("user");
+ Principal principal = mock(Principal.class);
+ RefreshableKeycloakSecurityContext keycloakSecurityContext = mock(RefreshableKeycloakSecurityContext.class);
+ KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, keycloakSecurityContext);
+ KeycloakAuthenticationToken token = new KeycloakAuthenticationToken(account);
+ springSecurityContext.setAuthentication(token);
+ }
+
+ @Test
+ public void shouldRetrieveKeycloakSecurityContext() {
+ SimpleHttpFacade facade = new SimpleHttpFacade(new MockHttpServletRequest(), new MockHttpServletResponse());
+
+ assertNotNull(facade.getSecurityContext());
+ }
+}
adapters/oidc/tomcat/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/tomcat/pom.xml b/adapters/oidc/tomcat/pom.xml
index d691c1d..d733dbd 100755
--- a/adapters/oidc/tomcat/pom.xml
+++ b/adapters/oidc/tomcat/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak Tomcat Integration</name>
adapters/oidc/tomcat/tomcat6/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/tomcat/tomcat6/pom.xml b/adapters/oidc/tomcat/tomcat6/pom.xml
index 47972ac..d0b7059 100755
--- a/adapters/oidc/tomcat/tomcat6/pom.xml
+++ b/adapters/oidc/tomcat/tomcat6/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-tomcat-integration-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/oidc/tomcat/tomcat7/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/tomcat/tomcat7/pom.xml b/adapters/oidc/tomcat/tomcat7/pom.xml
index 01f12ef..d425305 100755
--- a/adapters/oidc/tomcat/tomcat7/pom.xml
+++ b/adapters/oidc/tomcat/tomcat7/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-tomcat-integration-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/oidc/tomcat/tomcat8/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/tomcat/tomcat8/pom.xml b/adapters/oidc/tomcat/tomcat8/pom.xml
index ef6ea29..8bf8606 100755
--- a/adapters/oidc/tomcat/tomcat8/pom.xml
+++ b/adapters/oidc/tomcat/tomcat8/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-tomcat-integration-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/oidc/tomcat/tomcat-core/pom.xml b/adapters/oidc/tomcat/tomcat-core/pom.xml
index 397f622..c47cc4d 100755
--- a/adapters/oidc/tomcat/tomcat-core/pom.xml
+++ b/adapters/oidc/tomcat/tomcat-core/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-tomcat-integration-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/oidc/undertow/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/undertow/pom.xml b/adapters/oidc/undertow/pom.xml
index 048e249..ccdc34e 100755
--- a/adapters/oidc/undertow/pom.xml
+++ b/adapters/oidc/undertow/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/oidc/wildfly/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/wildfly/pom.xml b/adapters/oidc/wildfly/pom.xml
index d6a2184..e93be16 100755
--- a/adapters/oidc/wildfly/pom.xml
+++ b/adapters/oidc/wildfly/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak WildFly Integration</name>
diff --git a/adapters/oidc/wildfly/wf8-subsystem/pom.xml b/adapters/oidc/wildfly/wf8-subsystem/pom.xml
index e3e2c34..2afcf14 100755
--- a/adapters/oidc/wildfly/wf8-subsystem/pom.xml
+++ b/adapters/oidc/wildfly/wf8-subsystem/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
diff --git a/adapters/oidc/wildfly/wildfly-adapter/pom.xml b/adapters/oidc/wildfly/wildfly-adapter/pom.xml
index 89d3870..686661d 100644
--- a/adapters/oidc/wildfly/wildfly-adapter/pom.xml
+++ b/adapters/oidc/wildfly/wildfly-adapter/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/pom.xml b/adapters/oidc/wildfly/wildfly-subsystem/pom.xml
index 596f00f..1d8e6cf 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/pom.xml
+++ b/adapters/oidc/wildfly/wildfly-subsystem/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java
index e96a5e5..5a71e61 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java
@@ -37,6 +37,8 @@ import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD
public final class KeycloakAdapterConfigService {
private static final String CREDENTIALS_JSON_NAME = "credentials";
+
+ private static final String REDIRECT_REWRITE_RULE_JSON_NAME = "redirect-rewrite-rule";
private static final KeycloakAdapterConfigService INSTANCE = new KeycloakAdapterConfigService();
@@ -129,6 +131,56 @@ public final class KeycloakAdapterConfigService {
ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
return deployment.get(CREDENTIALS_JSON_NAME);
}
+
+ public void addRedirectRewriteRule(ModelNode operation, ModelNode model) {
+ ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
+ if (!redirectRewritesRules.isDefined()) {
+ redirectRewritesRules = new ModelNode();
+ }
+
+ String redirectRewriteRuleName = redirectRewriteRule(operation);
+ if (!redirectRewriteRuleName.contains(".")) {
+ redirectRewritesRules.get(redirectRewriteRuleName).set(model.get("value").asString());
+ } else {
+ String[] parts = redirectRewriteRuleName.split("\\.");
+ String provider = parts[0];
+ String property = parts[1];
+ ModelNode redirectRewriteRule = redirectRewritesRules.get(provider);
+ if (!redirectRewriteRule.isDefined()) {
+ redirectRewriteRule = new ModelNode();
+ }
+ redirectRewriteRule.get(property).set(model.get("value").asString());
+ redirectRewritesRules.set(provider, redirectRewriteRule);
+ }
+
+ ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
+ deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME).set(redirectRewritesRules);
+ }
+
+ public void removeRedirectRewriteRule(ModelNode operation) {
+ ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
+ if (!redirectRewritesRules.isDefined()) {
+ throw new RuntimeException("Can not remove redirect rewrite rule. No rules defined for deployment in op " + operation.toString());
+ }
+
+ String ruleName = credentialNameFromOp(operation);
+ redirectRewritesRules.remove(ruleName);
+ }
+
+ public void updateRedirectRewriteRule(ModelNode operation, String attrName, ModelNode resolvedValue) {
+ ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
+ if (!redirectRewritesRules.isDefined()) {
+ throw new RuntimeException("Can not update redirect rewrite rule. No rules defined for deployment in op " + operation.toString());
+ }
+
+ String ruleName = credentialNameFromOp(operation);
+ redirectRewritesRules.get(ruleName).set(resolvedValue);
+ }
+
+ private ModelNode redirectRewriteRuleFromOp(ModelNode operation) {
+ ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
+ return deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME);
+ }
private String realmNameFromOp(ModelNode operation) {
return valueFromOpAddress(RealmDefinition.TAG_NAME, operation);
@@ -141,6 +193,10 @@ public final class KeycloakAdapterConfigService {
private String credentialNameFromOp(ModelNode operation) {
return valueFromOpAddress(CredentialDefinition.TAG_NAME, operation);
}
+
+ private String redirectRewriteRule(ModelNode operation) {
+ return valueFromOpAddress(RedirecRewritetRuleDefinition.TAG_NAME, operation);
+ }
private String valueFromOpAddress(String addrElement, ModelNode operation) {
String deploymentName = getValueOfAddrElement(operation.get(ADDRESS), addrElement);
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java
index 541454a..d04e72d 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java
@@ -48,6 +48,7 @@ public class KeycloakExtension implements Extension {
static final RealmDefinition REALM_DEFINITION = new RealmDefinition();
static final SecureDeploymentDefinition SECURE_DEPLOYMENT_DEFINITION = new SecureDeploymentDefinition();
static final CredentialDefinition CREDENTIAL_DEFINITION = new CredentialDefinition();
+ static final RedirecRewritetRuleDefinition REDIRECT_RULE_DEFINITON = new RedirecRewritetRuleDefinition();
public static StandardResourceDescriptionResolver getResourceDescriptionResolver(final String... keyPrefix) {
StringBuilder prefix = new StringBuilder(SUBSYSTEM_NAME);
@@ -77,6 +78,7 @@ public class KeycloakExtension implements Extension {
registration.registerSubModel(REALM_DEFINITION);
ManagementResourceRegistration secureDeploymentRegistration = registration.registerSubModel(SECURE_DEPLOYMENT_DEFINITION);
secureDeploymentRegistration.registerSubModel(CREDENTIAL_DEFINITION);
+ secureDeploymentRegistration.registerSubModel(REDIRECT_RULE_DEFINITON);
subsystem.registerXMLElementWriter(PARSER);
}
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java
index d4ddc02..79555e3 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java
@@ -96,12 +96,17 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
PathElement.pathElement(SecureDeploymentDefinition.TAG_NAME, name));
addSecureDeployment.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode());
List<ModelNode> credentialsToAdd = new ArrayList<ModelNode>();
+ List<ModelNode> redirectRulesToAdd = new ArrayList<ModelNode>();
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName();
if (tagName.equals(CredentialDefinition.TAG_NAME)) {
readCredential(reader, addr, credentialsToAdd);
continue;
}
+ if (tagName.equals(RedirecRewritetRuleDefinition.TAG_NAME)) {
+ readRewriteRule(reader, addr, redirectRulesToAdd);
+ continue;
+ }
SimpleAttributeDefinition def = SecureDeploymentDefinition.lookup(tagName);
if (def == null) throw new XMLStreamException("Unknown secure-deployment tag " + tagName);
@@ -111,6 +116,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
// Must add credentials after the deployment is added.
resourcesToAdd.add(addSecureDeployment);
resourcesToAdd.addAll(credentialsToAdd);
+ resourcesToAdd.addAll(redirectRulesToAdd);
}
public void readCredential(XMLExtendedStreamReader reader, PathAddress parent, List<ModelNode> credentialsToAdd) throws XMLStreamException {
@@ -149,6 +155,43 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
}
}
}
+
+ public void readRewriteRule(XMLExtendedStreamReader reader, PathAddress parent, List<ModelNode> rewriteRuleToToAdd) throws XMLStreamException {
+ String name = readNameAttribute(reader);
+
+ Map<String, String> values = new HashMap<>();
+ String textValue = null;
+ while (reader.hasNext()) {
+ int next = reader.next();
+ if (next == CHARACTERS) {
+ // text value of redirect rule element
+ String text = reader.getText();
+ if (text == null || text.trim().isEmpty()) {
+ continue;
+ }
+ textValue = text;
+ } else if (next == START_ELEMENT) {
+ String key = reader.getLocalName();
+ reader.next();
+ String value = reader.getText();
+ reader.next();
+
+ values.put(key, value);
+ } else if (next == END_ELEMENT) {
+ break;
+ }
+ }
+
+ if (textValue != null) {
+ ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name, textValue);
+ rewriteRuleToToAdd.add(addRedirectRule);
+ } else {
+ for (Map.Entry<String, String> entry : values.entrySet()) {
+ ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name + "." + entry.getKey(), entry.getValue());
+ rewriteRuleToToAdd.add(addRedirectRule);
+ }
+ }
+ }
private ModelNode getCredentialToAdd(PathAddress parent, String name, String value) {
ModelNode addCredential = new ModelNode();
@@ -158,6 +201,15 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
addCredential.get(CredentialDefinition.VALUE.getName()).set(value);
return addCredential;
}
+
+ private ModelNode getRedirectRuleToAdd(PathAddress parent, String name, String value) {
+ ModelNode addRedirectRule = new ModelNode();
+ addRedirectRule.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD);
+ PathAddress addr = PathAddress.pathAddress(parent, PathElement.pathElement(RedirecRewritetRuleDefinition.TAG_NAME, name));
+ addRedirectRule.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode());
+ addRedirectRule.get(RedirecRewritetRuleDefinition.VALUE.getName()).set(value);
+ return addRedirectRule;
+ }
// expects that the current tag will have one single attribute called "name"
private String readNameAttribute(XMLExtendedStreamReader reader) throws XMLStreamException {
@@ -219,6 +271,11 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
if (credentials.isDefined()) {
writeCredentials(writer, credentials);
}
+
+ ModelNode redirectRewriteRule = deploymentElements.get(RedirecRewritetRuleDefinition.TAG_NAME);
+ if (redirectRewriteRule.isDefined()) {
+ writeRedirectRules(writer, redirectRewriteRule);
+ }
writer.writeEndElement();
}
@@ -265,6 +322,34 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
writer.writeEndElement();
}
}
+
+ private void writeRedirectRules(XMLExtendedStreamWriter writer, ModelNode redirectRules) throws XMLStreamException {
+ Map<String, Object> parsed = new LinkedHashMap<>();
+ for (Property redirectRule : redirectRules.asPropertyList()) {
+ String ruleName = redirectRule.getName();
+ String ruleValue = redirectRule.getValue().get(RedirecRewritetRuleDefinition.VALUE.getName()).asString();
+ parsed.put(ruleName, ruleValue);
+ }
+
+ for (Map.Entry<String, Object> entry : parsed.entrySet()) {
+ writer.writeStartElement(RedirecRewritetRuleDefinition.TAG_NAME);
+ writer.writeAttribute("name", entry.getKey());
+
+ Object value = entry.getValue();
+ if (value instanceof String) {
+ writeCharacters(writer, (String) value);
+ } else {
+ Map<String, String> redirectRulesProps = (Map<String, String>) value;
+ for (Map.Entry<String, String> prop : redirectRulesProps.entrySet()) {
+ writer.writeStartElement(prop.getKey());
+ writeCharacters(writer, prop.getValue());
+ writer.writeEndElement();
+ }
+ }
+
+ writer.writeEndElement();
+ }
+ }
// code taken from org.jboss.as.controller.AttributeMarshaller
private void writeCharacters(XMLExtendedStreamWriter writer, String content) throws XMLStreamException {
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java
new file mode 100644
index 0000000..a9095c7
--- /dev/null
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.subsystem.adapter.extension;
+
+import org.jboss.as.controller.AttributeDefinition;
+import org.jboss.as.controller.PathElement;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.as.controller.SimpleResourceDefinition;
+import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler;
+import org.jboss.as.controller.operations.validation.StringLengthValidator;
+import org.jboss.as.controller.registry.ManagementResourceRegistration;
+import org.jboss.dmr.ModelType;
+
+/**
+ *
+ * @author sblanc
+ */
+public class RedirecRewritetRuleDefinition extends SimpleResourceDefinition {
+
+ public static final String TAG_NAME = "redirect-rewrite-rule";
+
+ protected static final AttributeDefinition VALUE =
+ new SimpleAttributeDefinitionBuilder("value", ModelType.STRING, false)
+ .setAllowExpression(true)
+ .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, false, true))
+ .build();
+
+ public RedirecRewritetRuleDefinition() {
+ super(PathElement.pathElement(TAG_NAME),
+ KeycloakExtension.getResourceDescriptionResolver(TAG_NAME),
+ new RedirectRewriteRuleAddHandler(VALUE),
+ RedirectRewriteRuleRemoveHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerOperations(ManagementResourceRegistration resourceRegistration) {
+ super.registerOperations(resourceRegistration);
+ resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerAttributes(ManagementResourceRegistration resourceRegistration) {
+ super.registerAttributes(resourceRegistration);
+ resourceRegistration.registerReadWriteAttribute(VALUE, null, new RedirectRewriteRuleReadWriteAttributeHandler());
+ }
+}
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java
new file mode 100644
index 0000000..2fc25f7
--- /dev/null
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.subsystem.adapter.extension;
+
+import org.jboss.as.controller.AbstractAddStepHandler;
+import org.jboss.as.controller.AttributeDefinition;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.dmr.ModelNode;
+
+public class RedirectRewriteRuleAddHandler extends AbstractAddStepHandler {
+
+ public RedirectRewriteRuleAddHandler(AttributeDefinition... attributes) {
+ super(attributes);
+ }
+
+ @Override
+ protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
+ KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance();
+ ckService.addRedirectRewriteRule(operation, context.resolveExpressions(model));
+ }
+
+}
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java
new file mode 100644
index 0000000..171e755
--- /dev/null
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.subsystem.adapter.extension;
+
+import org.jboss.as.controller.AbstractWriteAttributeHandler;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.dmr.ModelNode;
+
+public class RedirectRewriteRuleReadWriteAttributeHandler extends AbstractWriteAttributeHandler<KeycloakAdapterConfigService> {
+
+ @Override
+ protected boolean applyUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName,
+ ModelNode resolvedValue, ModelNode currentValue, AbstractWriteAttributeHandler.HandbackHolder<KeycloakAdapterConfigService> hh) throws OperationFailedException {
+
+ KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance();
+ ckService.updateRedirectRewriteRule(operation, attributeName, resolvedValue);
+
+ hh.setHandback(ckService);
+
+ return false;
+ }
+
+ @Override
+ protected void revertUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName,
+ ModelNode valueToRestore, ModelNode valueToRevert, KeycloakAdapterConfigService ckService) throws OperationFailedException {
+ ckService.updateRedirectRewriteRule(operation, attributeName, valueToRestore);
+ }
+
+}
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java
new file mode 100644
index 0000000..de17c96
--- /dev/null
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.subsystem.adapter.extension;
+
+import org.jboss.as.controller.AbstractRemoveStepHandler;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.dmr.ModelNode;
+
+public class RedirectRewriteRuleRemoveHandler extends AbstractRemoveStepHandler {
+
+ public static RedirectRewriteRuleRemoveHandler INSTANCE = new RedirectRewriteRuleRemoveHandler();
+
+ private RedirectRewriteRuleRemoveHandler() {}
+
+ @Override
+ protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
+ KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance();
+ ckService.removeRedirectRewriteRule(operation);
+ }
+
+}
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties
index 1df5979..c9cea77 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties
@@ -65,6 +65,7 @@ keycloak.secure-deployment.connection-pool-size=Connection pool size for the cli
keycloak.secure-deployment.resource=Application name
keycloak.secure-deployment.use-resource-role-mappings=Use resource level permissions from token
keycloak.secure-deployment.credentials=Adapter credentials
+keycloak.secure-deployment.redirect-rewrite-rule=Apply a rewrite rule for the redirect URI
keycloak.secure-deployment.bearer-only=Bearer Token Auth only
keycloak.secure-deployment.enable-basic-auth=Enable Basic Authentication
keycloak.secure-deployment.public-client=Public client
@@ -94,4 +95,9 @@ keycloak.secure-deployment.credential=Credential value
keycloak.credential=Credential
keycloak.credential.value=Credential value
keycloak.credential.add=Credential add
-keycloak.credential.remove=Credential remove
\ No newline at end of file
+keycloak.credential.remove=Credential remove
+
+keycloak.redirect-rewrite-rule=redirect-rewrite-rule
+keycloak.redirect-rewrite-rule.value=redirect-rewrite-rule value
+keycloak.redirect-rewrite-rule.add=redirect-rewrite-rule add
+keycloak.redirect-rewrite-rule.remove=redirect-rewrite-rule remove
\ No newline at end of file
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd
index 604e6ac..d8f5bc3 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd
@@ -101,6 +101,7 @@
<xs:element name="ssl-required" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:element name="realm-public-key" type="xs:string" minOccurs="1" maxOccurs="1"/>
<xs:element name="credential" type="credential-type" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="redirect-rewrite-rule" type="redirect-rewrite-rule-type" minOccurs="1" maxOccurs="1"/>
<xs:element name="auth-server-url-for-backend-requests" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="always-refresh-token" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="register-node-at-startup" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
@@ -127,4 +128,10 @@
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required" />
</xs:complexType>
+ <xs:complexType name="redirect-rewrite-rule-type" mixed="true">
+ <xs:sequence maxOccurs="unbounded" minOccurs="0">
+ <xs:any processContents="lax"></xs:any>
+ </xs:sequence>
+ <xs:attribute name="name" type="xs:string" use="required" />
+ </xs:complexType>
</xs:schema>
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml b/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml
index 3dcb61d..246d768 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml
@@ -53,6 +53,7 @@
<auth-server-url>http://localhost:8080/auth</auth-server-url>
<ssl-required>EXTERNAL</ssl-required>
<credential name="secret">0aa31d98-e0aa-404c-b6e0-e771dba1e798</credential>
+ <redirect-rewrite-rule name="^/wsmaster/api/(.*)$">api/$1/</redirect-rewrite-rule>
</secure-deployment>
<secure-deployment name="http-endpoint">
<realm>master</realm>
@@ -66,5 +67,6 @@
<credential name="jwt">
<client-keystore-file>/tmp/keystore.jks</client-keystore-file>
</credential>
+ <redirect-rewrite-rule name="^/wsmaster/api/(.*)$">/api/$1/</redirect-rewrite-rule>
</secure-deployment>
</subsystem>
\ No newline at end of file
diff --git a/adapters/oidc/wildfly-elytron/pom.xml b/adapters/oidc/wildfly-elytron/pom.xml
index edfd4aa..71d5681 100755
--- a/adapters/oidc/wildfly-elytron/pom.xml
+++ b/adapters/oidc/wildfly-elytron/pom.xml
@@ -22,7 +22,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java
index bc2e903..4472af7 100644
--- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java
+++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java
@@ -90,6 +90,11 @@ class ElytronHttpFacade implements OIDCHttpFacade {
void authenticationComplete() {
if (securityIdentity != null) {
+ HttpScope requestScope = request.getScope(Scope.EXCHANGE);
+ RefreshableKeycloakSecurityContext keycloakSecurityContext = account.getKeycloakSecurityContext();
+
+ requestScope.setAttachment(KeycloakSecurityContext.class.getName(), keycloakSecurityContext);
+
this.request.authenticationComplete(response -> {
if (!restored) {
responseConsumer.accept(response);
diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java
index 3fcf9bf..8d0cd1d 100644
--- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java
+++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java
@@ -71,7 +71,7 @@ class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticat
AdapterDeploymentContext deploymentContext = getDeploymentContext(request);
if (deploymentContext == null) {
- LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI());
+ LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI(), getMechanismName());
request.noAuthenticationInProgress();
return;
}
adapters/pom.xml 2(+1 -1)
diff --git a/adapters/pom.xml b/adapters/pom.xml
index 847b84e..21e23b9 100755
--- a/adapters/pom.xml
+++ b/adapters/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Keycloak Adapters</name>
diff --git a/adapters/saml/as7-eap6/adapter/pom.xml b/adapters/saml/as7-eap6/adapter/pom.xml
index 43cfcb1..dd8aff5 100755
--- a/adapters/saml/as7-eap6/adapter/pom.xml
+++ b/adapters/saml/as7-eap6/adapter/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-saml-eap-integration-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/saml/as7-eap6/pom.xml 2(+1 -1)
diff --git a/adapters/saml/as7-eap6/pom.xml b/adapters/saml/as7-eap6/pom.xml
index 7233dfb..66bdf0d 100755
--- a/adapters/saml/as7-eap6/pom.xml
+++ b/adapters/saml/as7-eap6/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak SAML EAP Integration</name>
diff --git a/adapters/saml/as7-eap6/subsystem/pom.xml b/adapters/saml/as7-eap6/subsystem/pom.xml
index e89cbd7..d3c2a5d 100755
--- a/adapters/saml/as7-eap6/subsystem/pom.xml
+++ b/adapters/saml/as7-eap6/subsystem/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-eap-integration-pom</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/FormattingXMLStreamWriter.java b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/FormattingXMLStreamWriter.java
index 0d56659..2334a63 100644
--- a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/FormattingXMLStreamWriter.java
+++ b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/FormattingXMLStreamWriter.java
@@ -81,7 +81,7 @@ public final class FormattingXMLStreamWriter implements XMLExtendedStreamWriter,
public void writeStartElement(final String localName) throws XMLStreamException {
ArrayDeque<String> namespaces = unspecifiedNamespaces;
String namespace = namespaces.getFirst();
- if (namespace != NO_NAMESPACE) {
+ if (namespace == null ? NO_NAMESPACE != null : ! namespace.equals(NO_NAMESPACE)) {
writeStartElement(namespace, localName);
return;
}
@@ -140,9 +140,9 @@ public final class FormattingXMLStreamWriter implements XMLExtendedStreamWriter,
attrQueue.add(new ArgRunnable() {
public void run(int arg) throws XMLStreamException {
if (arg == 0) {
- delegate.writeStartElement(prefix, namespaceURI, localName);
+ delegate.writeStartElement(prefix, localName, namespaceURI);
} else {
- delegate.writeEmptyElement(prefix, namespaceURI, localName);
+ delegate.writeEmptyElement(prefix, localName, namespaceURI);
}
}
});
@@ -165,14 +165,14 @@ public final class FormattingXMLStreamWriter implements XMLExtendedStreamWriter,
runAttrQueue();
nl();
indent();
- delegate.writeEmptyElement(prefix, namespaceURI, localName);
+ delegate.writeEmptyElement(prefix, localName, namespaceURI);
state = END_ELEMENT;
}
@Override
public void writeEmptyElement(final String localName) throws XMLStreamException {
String namespace = unspecifiedNamespaces.getFirst();
- if (namespace != NO_NAMESPACE) {
+ if (namespace == null ? NO_NAMESPACE != null : ! namespace.equals(NO_NAMESPACE)) {
writeEmptyElement(namespace, localName);
return;
}
adapters/saml/core/pom.xml 2(+1 -1)
diff --git a/adapters/saml/core/pom.xml b/adapters/saml/core/pom.xml
index 244e69e..be1e686 100755
--- a/adapters/saml/core/pom.xml
+++ b/adapters/saml/core/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java
index 0858675..b8d5d66 100644
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java
@@ -34,6 +34,7 @@ import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ParsingException;
+import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.core.util.NamespaceContext;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -65,9 +66,7 @@ public class SamlDescriptorIDPKeysExtractor {
MultivaluedHashMap<String, KeyInfo> res = new MultivaluedHashMap<>();
try {
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- factory.setNamespaceAware(true);
- DocumentBuilder builder = factory.newDocumentBuilder();
+ DocumentBuilder builder = DocumentUtil.getDocumentBuilder();
Document doc = builder.parse(stream);
XPathExpression expr = xpath.compile("/m:EntitiesDescriptor/m:EntityDescriptor/m:IDPSSODescriptor/m:KeyDescriptor");
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
index 08ce4a9..2b40a42 100644
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
@@ -407,8 +407,8 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
SubjectType subject = assertion.getSubject();
SubjectType.STSubType subType = subject.getSubType();
- NameIDType subjectNameID = (NameIDType) subType.getBaseID();
- String principalName = subjectNameID.getValue();
+ NameIDType subjectNameID = subType == null ? null : (NameIDType) subType.getBaseID();
+ String principalName = subjectNameID == null ? null : subjectNameID.getValue();
final Set<String> roles = new HashSet<>();
MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
@@ -473,7 +473,7 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
}
- URI nameFormat = subjectNameID.getFormat();
+ URI nameFormat = subjectNameID == null ? null : subjectNameID.getFormat();
String nameFormatString = nameFormat == null ? JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get() : nameFormat.toString();
final SamlPrincipal principal = new SamlPrincipal(assertion, principalName, principalName, nameFormatString, attributes, friendlyAttributes);
String index = authn == null ? null : authn.getSessionIndex();
adapters/saml/core-public/pom.xml 2(+1 -1)
diff --git a/adapters/saml/core-public/pom.xml b/adapters/saml/core-public/pom.xml
index e56da0e..29e35a9 100755
--- a/adapters/saml/core-public/pom.xml
+++ b/adapters/saml/core-public/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java b/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java
index 29bbbfa..f44534d 100755
--- a/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java
+++ b/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java
@@ -18,7 +18,10 @@
package org.keycloak.adapters.saml;
import org.keycloak.adapters.spi.AuthenticationError;
+import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
+import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
+import java.util.Objects;
/**
* Object that describes the SAML error that happened.
@@ -27,6 +30,7 @@ import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
* @version $Revision: 1 $
*/
public class SamlAuthenticationError implements AuthenticationError {
+
public static enum Reason {
EXTRACTION_FAILURE,
INVALID_SIGNATURE,
@@ -59,7 +63,18 @@ public class SamlAuthenticationError implements AuthenticationError {
@Override
public String toString() {
- return "SamlAuthenticationError [reason=" + reason + ", status=" + status + "]";
+ return "SamlAuthenticationError [reason=" + reason + ", status="
+ + ((status == null || status.getStatus() == null) ? "UNKNOWN" : extractStatusCode(status.getStatus().getStatusCode()))
+ + "]";
}
+ private String extractStatusCode(StatusCodeType statusCode) {
+ if (statusCode == null || statusCode.getValue() == null) {
+ return "UNKNOWN";
+ }
+ if (Objects.equals(JBossSAMLURIConstants.STATUS_RESPONDER.get(), statusCode.getValue().toString())) {
+ return extractStatusCode(statusCode.getStatusCode());
+ }
+ return statusCode.getValue().toString();
+ }
}
adapters/saml/jetty/jetty8.1/pom.xml 2(+1 -1)
diff --git a/adapters/saml/jetty/jetty8.1/pom.xml b/adapters/saml/jetty/jetty8.1/pom.xml
index 62ac8f3..ca56982 100755
--- a/adapters/saml/jetty/jetty8.1/pom.xml
+++ b/adapters/saml/jetty/jetty8.1/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/saml/jetty/jetty9.1/pom.xml 2(+1 -1)
diff --git a/adapters/saml/jetty/jetty9.1/pom.xml b/adapters/saml/jetty/jetty9.1/pom.xml
index f613b6b..c01af40 100755
--- a/adapters/saml/jetty/jetty9.1/pom.xml
+++ b/adapters/saml/jetty/jetty9.1/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/saml/jetty/jetty9.2/pom.xml 2(+1 -1)
diff --git a/adapters/saml/jetty/jetty9.2/pom.xml b/adapters/saml/jetty/jetty9.2/pom.xml
index d66b670..88bf685 100755
--- a/adapters/saml/jetty/jetty9.2/pom.xml
+++ b/adapters/saml/jetty/jetty9.2/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/saml/jetty/jetty9.3/pom.xml 2(+1 -1)
diff --git a/adapters/saml/jetty/jetty9.3/pom.xml b/adapters/saml/jetty/jetty9.3/pom.xml
index 8104214..d34f83e 100644
--- a/adapters/saml/jetty/jetty9.3/pom.xml
+++ b/adapters/saml/jetty/jetty9.3/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/saml/jetty/jetty9.4/pom.xml 2(+1 -1)
diff --git a/adapters/saml/jetty/jetty9.4/pom.xml b/adapters/saml/jetty/jetty9.4/pom.xml
index 23b05d0..cee8d45 100644
--- a/adapters/saml/jetty/jetty9.4/pom.xml
+++ b/adapters/saml/jetty/jetty9.4/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/saml/jetty/jetty-core/pom.xml b/adapters/saml/jetty/jetty-core/pom.xml
index c6a7cc0..316cb7e 100755
--- a/adapters/saml/jetty/jetty-core/pom.xml
+++ b/adapters/saml/jetty/jetty-core/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/saml/jetty/pom.xml 2(+1 -1)
diff --git a/adapters/saml/jetty/pom.xml b/adapters/saml/jetty/pom.xml
index 621ae3c..2f53996 100755
--- a/adapters/saml/jetty/pom.xml
+++ b/adapters/saml/jetty/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak SAML Jetty Integration</name>
adapters/saml/pom.xml 2(+1 -1)
diff --git a/adapters/saml/pom.xml b/adapters/saml/pom.xml
index 18e32cf..4ae655c 100755
--- a/adapters/saml/pom.xml
+++ b/adapters/saml/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<name>Keycloak SAML Client Adapter Modules</name>
adapters/saml/servlet-filter/pom.xml 2(+1 -1)
diff --git a/adapters/saml/servlet-filter/pom.xml b/adapters/saml/servlet-filter/pom.xml
index c7688b6..fd7f939 100755
--- a/adapters/saml/servlet-filter/pom.xml
+++ b/adapters/saml/servlet-filter/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/saml/tomcat/pom.xml 2(+1 -1)
diff --git a/adapters/saml/tomcat/pom.xml b/adapters/saml/tomcat/pom.xml
index 3f76eba..0911796 100755
--- a/adapters/saml/tomcat/pom.xml
+++ b/adapters/saml/tomcat/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak SAML Tomcat Integration</name>
adapters/saml/tomcat/tomcat6/pom.xml 2(+1 -1)
diff --git a/adapters/saml/tomcat/tomcat6/pom.xml b/adapters/saml/tomcat/tomcat6/pom.xml
index fb55ecc..12ad22b 100755
--- a/adapters/saml/tomcat/tomcat6/pom.xml
+++ b/adapters/saml/tomcat/tomcat6/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-saml-tomcat-integration-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/saml/tomcat/tomcat7/pom.xml 2(+1 -1)
diff --git a/adapters/saml/tomcat/tomcat7/pom.xml b/adapters/saml/tomcat/tomcat7/pom.xml
index ef00d9b..ff59bfc 100755
--- a/adapters/saml/tomcat/tomcat7/pom.xml
+++ b/adapters/saml/tomcat/tomcat7/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-saml-tomcat-integration-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/saml/tomcat/tomcat8/pom.xml 2(+1 -1)
diff --git a/adapters/saml/tomcat/tomcat8/pom.xml b/adapters/saml/tomcat/tomcat8/pom.xml
index b87505f..835e4d5 100755
--- a/adapters/saml/tomcat/tomcat8/pom.xml
+++ b/adapters/saml/tomcat/tomcat8/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-saml-tomcat-integration-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/SamlAuthenticatorValve.java b/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/SamlAuthenticatorValve.java
index eef8c6a..caf1bf9 100755
--- a/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/SamlAuthenticatorValve.java
+++ b/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/SamlAuthenticatorValve.java
@@ -41,10 +41,20 @@ import java.util.List;
* @version $Revision: 1 $
*/
public class SamlAuthenticatorValve extends AbstractSamlAuthenticatorValve {
+ /**
+ * Method called by Tomcat < 8.5.5
+ */
public boolean authenticate(Request request, HttpServletResponse response) throws IOException {
return authenticateInternal(request, response, request.getContext().getLoginConfig());
}
+ /**
+ * Method called by Tomcat >= 8.5.5
+ */
+ protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException {
+ return this.authenticate(request, response);
+ }
+
@Override
protected boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException {
if (loginConfig == null) return false;
diff --git a/adapters/saml/tomcat/tomcat-core/pom.xml b/adapters/saml/tomcat/tomcat-core/pom.xml
index 466b6b5..e493969 100755
--- a/adapters/saml/tomcat/tomcat-core/pom.xml
+++ b/adapters/saml/tomcat/tomcat-core/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-saml-tomcat-integration-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/saml/undertow/pom.xml 2(+1 -1)
diff --git a/adapters/saml/undertow/pom.xml b/adapters/saml/undertow/pom.xml
index 4075716..b314f7e 100755
--- a/adapters/saml/undertow/pom.xml
+++ b/adapters/saml/undertow/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/saml/wildfly/pom.xml 2(+1 -1)
diff --git a/adapters/saml/wildfly/pom.xml b/adapters/saml/wildfly/pom.xml
index 4306960..43108bb 100755
--- a/adapters/saml/wildfly/pom.xml
+++ b/adapters/saml/wildfly/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak SAML Wildfly Integration</name>
diff --git a/adapters/saml/wildfly/wildfly-adapter/pom.xml b/adapters/saml/wildfly/wildfly-adapter/pom.xml
index 1ba1057..3be5e7e 100755
--- a/adapters/saml/wildfly/wildfly-adapter/pom.xml
+++ b/adapters/saml/wildfly/wildfly-adapter/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/saml/wildfly/wildfly-subsystem/pom.xml b/adapters/saml/wildfly/wildfly-subsystem/pom.xml
index 7195cf4..acaf7f1 100755
--- a/adapters/saml/wildfly/wildfly-subsystem/pom.xml
+++ b/adapters/saml/wildfly/wildfly-subsystem/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
diff --git a/adapters/saml/wildfly-elytron/pom.xml b/adapters/saml/wildfly-elytron/pom.xml
index 8d6df2e..4161b09 100755
--- a/adapters/saml/wildfly-elytron/pom.xml
+++ b/adapters/saml/wildfly-elytron/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java
index 88e96f8..68c6922 100644
--- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java
+++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java
@@ -47,10 +47,8 @@ import org.wildfly.security.auth.callback.AnonymousAuthorizationCallback;
import org.wildfly.security.auth.callback.AuthenticationCompleteCallback;
import org.wildfly.security.auth.callback.SecurityIdentityCallback;
import org.wildfly.security.auth.server.SecurityIdentity;
-import org.wildfly.security.http.HttpAuthenticationException;
import org.wildfly.security.http.HttpScope;
import org.wildfly.security.http.HttpServerCookie;
-import org.wildfly.security.http.HttpServerMechanismsResponder;
import org.wildfly.security.http.HttpServerRequest;
import org.wildfly.security.http.HttpServerResponse;
import org.wildfly.security.http.Scope;
@@ -87,11 +85,14 @@ class ElytronHttpFacade implements HttpFacade {
void authenticationComplete() {
this.securityIdentity = SecurityIdentityUtil.authorize(this.callbackHandler, samlSession.getPrincipal());
- this.request.authenticationComplete(response -> {
- if (!restored) {
- responseConsumer.accept(response);
- }
- }, () -> ((ElytronTokeStore) sessionStore).logout(true));
+
+ if (this.securityIdentity != null) {
+ this.request.authenticationComplete(response -> {
+ if (!restored) {
+ responseConsumer.accept(response);
+ }
+ }, () -> ((ElytronTokeStore) sessionStore).logout(true));
+ }
}
void authenticationCompleteAnonymous() {
diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java
index 9fce501..1f71bae 100644
--- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java
+++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java
@@ -65,7 +65,7 @@ class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticat
SamlDeploymentContext deploymentContext = getDeploymentContext(request);
if (deploymentContext == null) {
- LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI());
+ LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI(), getMechanismName());
request.noAuthenticationInProgress();
return;
}
adapters/spi/adapter-spi/pom.xml 2(+1 -1)
diff --git a/adapters/spi/adapter-spi/pom.xml b/adapters/spi/adapter-spi/pom.xml
index 23963fe..0145009 100755
--- a/adapters/spi/adapter-spi/pom.xml
+++ b/adapters/spi/adapter-spi/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/spi/jboss-adapter-core/pom.xml b/adapters/spi/jboss-adapter-core/pom.xml
index ccd687d..41be1d3 100755
--- a/adapters/spi/jboss-adapter-core/pom.xml
+++ b/adapters/spi/jboss-adapter-core/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/spi/jetty-adapter-spi/pom.xml b/adapters/spi/jetty-adapter-spi/pom.xml
index 0841616..03adfdf 100755
--- a/adapters/spi/jetty-adapter-spi/pom.xml
+++ b/adapters/spi/jetty-adapter-spi/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
adapters/spi/pom.xml 2(+1 -1)
diff --git a/adapters/spi/pom.xml b/adapters/spi/pom.xml
index ce65615..4580517 100755
--- a/adapters/spi/pom.xml
+++ b/adapters/spi/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<name>Keycloak Client Adapter SPI Modules</name>
diff --git a/adapters/spi/servlet-adapter-spi/pom.xml b/adapters/spi/servlet-adapter-spi/pom.xml
index b54d266..c9228b6 100755
--- a/adapters/spi/servlet-adapter-spi/pom.xml
+++ b/adapters/spi/servlet-adapter-spi/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/spi/tomcat-adapter-spi/pom.xml b/adapters/spi/tomcat-adapter-spi/pom.xml
index ba39fa0..dcf55e5 100755
--- a/adapters/spi/tomcat-adapter-spi/pom.xml
+++ b/adapters/spi/tomcat-adapter-spi/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/spi/undertow-adapter-spi/pom.xml b/adapters/spi/undertow-adapter-spi/pom.xml
index 9256187..0f04bdb 100755
--- a/adapters/spi/undertow-adapter-spi/pom.xml
+++ b/adapters/spi/undertow-adapter-spi/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
authz/client/pom.xml 2(+1 -1)
diff --git a/authz/client/pom.xml b/authz/client/pom.xml
index c1c0631..c45476a 100644
--- a/authz/client/pom.xml
+++ b/authz/client/pom.xml
@@ -7,7 +7,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
authz/policy/common/pom.xml 2(+1 -1)
diff --git a/authz/policy/common/pom.xml b/authz/policy/common/pom.xml
index 7e309a5..193dcf4 100644
--- a/authz/policy/common/pom.xml
+++ b/authz/policy/common/pom.xml
@@ -25,7 +25,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-provider-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
authz/policy/drools/pom.xml 2(+1 -1)
diff --git a/authz/policy/drools/pom.xml b/authz/policy/drools/pom.xml
index 54fdfe9..eb66741 100644
--- a/authz/policy/drools/pom.xml
+++ b/authz/policy/drools/pom.xml
@@ -7,7 +7,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-provider-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
authz/policy/pom.xml 2(+1 -1)
diff --git a/authz/policy/pom.xml b/authz/policy/pom.xml
index 3c064d4..f119652 100644
--- a/authz/policy/pom.xml
+++ b/authz/policy/pom.xml
@@ -7,7 +7,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
authz/pom.xml 2(+1 -1)
diff --git a/authz/pom.xml b/authz/pom.xml
index 6121b23..36c54ba 100644
--- a/authz/pom.xml
+++ b/authz/pom.xml
@@ -7,7 +7,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
boms/adapter/pom.xml 40(+20 -20)
diff --git a/boms/adapter/pom.xml b/boms/adapter/pom.xml
index 46c6272..4733361 100644
--- a/boms/adapter/pom.xml
+++ b/boms/adapter/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-bom-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<groupId>org.keycloak.bom</groupId>
@@ -37,97 +37,97 @@
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-spi</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-wildfly-adapter-dist</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-adapter-core</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-adapter-api-public</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-tomcat8-adapter</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-tomcat7-adapter</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-tomcat6-adapter</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-jetty81-adapter</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-jetty91-adapter</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-jetty92-adapter</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-jetty93-adapter</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-undertow-adapter</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-adapter</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>spring-boot-container-bundle</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
boms/pom.xml 2(+1 -1)
diff --git a/boms/pom.xml b/boms/pom.xml
index a5f75a8..bbbc0db 100644
--- a/boms/pom.xml
+++ b/boms/pom.xml
@@ -26,7 +26,7 @@
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-bom-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<packaging>pom</packaging>
boms/spi/pom.xml 6(+3 -3)
diff --git a/boms/spi/pom.xml b/boms/spi/pom.xml
index 555ca37..9d970d1 100644
--- a/boms/spi/pom.xml
+++ b/boms/spi/pom.xml
@@ -23,7 +23,7 @@
<parent>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-bom-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<groupId>org.keycloak.bom</groupId>
@@ -38,12 +38,12 @@
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
common/pom.xml 2(+1 -1)
diff --git a/common/pom.xml b/common/pom.xml
index 1039ff8..70476b6 100755
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java
index 91b0a80..7f97e55 100755
--- a/common/src/main/java/org/keycloak/common/Profile.java
+++ b/common/src/main/java/org/keycloak/common/Profile.java
@@ -35,13 +35,13 @@ import java.util.Set;
public class Profile {
public enum Feature {
- AUTHORIZATION, IMPERSONATION, SCRIPTS
+ AUTHORIZATION, IMPERSONATION, SCRIPTS, DOCKER
}
private enum ProfileValue {
- PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS),
+ PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS, Feature.DOCKER),
PREVIEW,
- COMMUNITY;
+ COMMUNITY(Feature.DOCKER);
private List<Feature> disabled;
diff --git a/common/src/main/java/org/keycloak/common/util/Encode.java b/common/src/main/java/org/keycloak/common/util/Encode.java
index 63b8f36..b195362 100755
--- a/common/src/main/java/org/keycloak/common/util/Encode.java
+++ b/common/src/main/java/org/keycloak/common/util/Encode.java
@@ -24,6 +24,7 @@ import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -36,7 +37,7 @@ import java.util.regex.Pattern;
*/
public class Encode
{
- private static final String UTF_8 = "UTF-8";
+ private static final String UTF_8 = StandardCharsets.UTF_8.name();
private static final Pattern PARAM_REPLACEMENT = Pattern.compile("_resteasy_uri_parameter");
@@ -84,9 +85,7 @@ public class Encode
case '@':
continue;
}
- StringBuffer sb = new StringBuffer();
- sb.append((char) i);
- pathEncoding[i] = URLEncoder.encode(sb.toString());
+ pathEncoding[i] = URLEncoder.encode(String.valueOf((char) i));
}
pathEncoding[' '] = "%20";
System.arraycopy(pathEncoding, 0, matrixParameterEncoding, 0, pathEncoding.length);
@@ -119,9 +118,7 @@ public class Encode
queryNameValueEncoding[i] = "+";
continue;
}
- StringBuffer sb = new StringBuffer();
- sb.append((char) i);
- queryNameValueEncoding[i] = URLEncoder.encode(sb.toString());
+ queryNameValueEncoding[i] = URLEncoder.encode(String.valueOf((char) i));
}
/*
@@ -159,9 +156,7 @@ public class Encode
queryStringEncoding[i] = "%20";
continue;
}
- StringBuffer sb = new StringBuffer();
- sb.append((char) i);
- queryStringEncoding[i] = URLEncoder.encode(sb.toString());
+ queryStringEncoding[i] = URLEncoder.encode(String.valueOf((char) i));
}
}
@@ -194,7 +189,7 @@ public class Encode
*/
public static String encodeFragment(String value)
{
- return encodeValue(value, queryNameValueEncoding);
+ return encodeValue(value, queryStringEncoding);
}
/**
@@ -221,18 +216,19 @@ public class Encode
public static String decodePath(String path)
{
Matcher matcher = encodedCharsMulti.matcher(path);
- StringBuffer buf = new StringBuffer();
+ int start=0;
+ StringBuilder builder = new StringBuilder();
CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder();
while (matcher.find())
{
+ builder.append(path, start, matcher.start());
decoder.reset();
String decoded = decodeBytes(matcher.group(1), decoder);
- decoded = decoded.replace("\\", "\\\\");
- decoded = decoded.replace("$", "\\$");
- matcher.appendReplacement(buf, decoded);
+ builder.append(decoded);
+ start = matcher.end();
}
- matcher.appendTail(buf);
- return buf.toString();
+ builder.append(path, start, path.length());
+ return builder.toString();
}
private static String decodeBytes(String enc, CharsetDecoder decoder)
@@ -264,7 +260,7 @@ public class Encode
public static String encodeNonCodes(String string)
{
Matcher matcher = nonCodes.matcher(string);
- StringBuffer buf = new StringBuffer();
+ StringBuilder builder = new StringBuilder();
// FYI: we do not use the no-arg matcher.find()
@@ -276,29 +272,32 @@ public class Encode
while (matcher.find(idx))
{
int start = matcher.start();
- buf.append(string.substring(idx, start));
- buf.append("%25");
+ builder.append(string.substring(idx, start));
+ builder.append("%25");
idx = start + 1;
}
- buf.append(string.substring(idx));
- return buf.toString();
+ builder.append(string.substring(idx));
+ return builder.toString();
}
- private static boolean savePathParams(String segment, StringBuffer newSegment, List<String> params)
+ public static boolean savePathParams(String segment, StringBuilder newSegment, List<String> params)
{
boolean foundParam = false;
// Regular expressions can have '{' and '}' characters. Replace them to do match
segment = PathHelper.replaceEnclosedCurlyBraces(segment);
Matcher matcher = PathHelper.URI_TEMPLATE_PATTERN.matcher(segment);
+ int start = 0;
while (matcher.find())
{
+ newSegment.append(segment, start, matcher.start());
foundParam = true;
String group = matcher.group();
// Regular expressions can have '{' and '}' characters. Recover earlier replacement
params.add(PathHelper.recoverEnclosedCurlyBraces(group));
- matcher.appendReplacement(newSegment, "_resteasy_uri_parameter");
+ newSegment.append("_resteasy_uri_parameter");
+ start = matcher.end();
}
- matcher.appendTail(newSegment);
+ newSegment.append(segment, start, segment.length());
return foundParam;
}
@@ -309,11 +308,11 @@ public class Encode
* @param encoding
* @return
*/
- private static String encodeValue(String segment, String[] encoding)
+ public static String encodeValue(String segment, String[] encoding)
{
ArrayList<String> params = new ArrayList<String>();
boolean foundParam = false;
- StringBuffer newSegment = new StringBuffer();
+ StringBuilder newSegment = new StringBuilder();
if (savePathParams(segment, newSegment, params))
{
foundParam = true;
@@ -411,21 +410,21 @@ public class Encode
return encodeFromArray(nameOrValue, queryNameValueEncoding, true);
}
- private static String encodeFromArray(String segment, String[] encodingMap, boolean encodePercent)
+ protected static String encodeFromArray(String segment, String[] encodingMap, boolean encodePercent)
{
- StringBuffer result = new StringBuffer();
+ StringBuilder result = new StringBuilder();
for (int i = 0; i < segment.length(); i++)
{
- if (!encodePercent && segment.charAt(i) == '%')
+ char currentChar = segment.charAt(i);
+ if (!encodePercent && currentChar == '%')
{
- result.append(segment.charAt(i));
+ result.append(currentChar);
continue;
}
- int idx = segment.charAt(i);
- String encoding = encode(idx, encodingMap);
+ String encoding = encode(currentChar, encodingMap);
if (encoding == null)
{
- result.append(segment.charAt(i));
+ result.append(currentChar);
}
else
{
@@ -461,20 +460,20 @@ public class Encode
return encoded;
}
- private static String pathParamReplacement(String segment, List<String> params)
+ public static String pathParamReplacement(String segment, List<String> params)
{
- StringBuffer newSegment = new StringBuffer();
+ StringBuilder newSegment = new StringBuilder();
Matcher matcher = PARAM_REPLACEMENT.matcher(segment);
int i = 0;
+ int start = 0;
while (matcher.find())
{
+ newSegment.append(segment, start, matcher.start());
String replacement = params.get(i++);
- // double encode slashes, so that slashes stay where they are
- replacement = replacement.replace("\\", "\\\\");
- replacement = replacement.replace("$", "\\$");
- matcher.appendReplacement(newSegment, replacement);
+ newSegment.append(replacement);
+ start = matcher.end();
}
- matcher.appendTail(newSegment);
+ newSegment.append(segment, start, segment.length());
segment = newSegment.toString();
return segment;
}
@@ -505,6 +504,38 @@ public class Encode
}
return decoded;
}
+
+ /**
+ * decode an encoded map
+ *
+ * @param map
+ * @param charset
+ * @return
+ */
+ public static MultivaluedHashMap<String, String> decode(MultivaluedHashMap<String, String> map, String charset)
+ {
+ if (charset == null)
+ {
+ charset = UTF_8;
+ }
+ MultivaluedHashMap<String, String> decoded = new MultivaluedHashMap<String, String>();
+ for (Map.Entry<String, List<String>> entry : map.entrySet())
+ {
+ List<String> values = entry.getValue();
+ for (String value : values)
+ {
+ try
+ {
+ decoded.add(URLDecoder.decode(entry.getKey(), charset), URLDecoder.decode(value, charset));
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ return decoded;
+ }
public static MultivaluedHashMap<String, String> encode(MultivaluedHashMap<String, String> map)
{
diff --git a/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java b/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java
index f064163..a03c53c 100755
--- a/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java
+++ b/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java
@@ -614,7 +614,7 @@ public class KeycloakUriBuilder {
if (value == null) throw new IllegalArgumentException("A passed in value was null");
if (query == null) query = "";
else query += "&";
- query += Encode.encodeQueryParam(name) + "=" + Encode.encodeQueryParam(value.toString());
+ query += Encode.encodeQueryParamAsIs(name) + "=" + Encode.encodeQueryParamAsIs(value.toString());
}
return this;
}
diff --git a/common/src/main/java/org/keycloak/common/Version.java b/common/src/main/java/org/keycloak/common/Version.java
index 862ccd2..75fbe92 100755
--- a/common/src/main/java/org/keycloak/common/Version.java
+++ b/common/src/main/java/org/keycloak/common/Version.java
@@ -45,6 +45,10 @@ public class Version {
Version.VERSION = props.getProperty("version");
Version.BUILD_TIME = props.getProperty("build-time");
Version.RESOURCES_VERSION = Version.VERSION.toLowerCase();
+
+ if (Version.RESOURCES_VERSION.endsWith("-snapshot")) {
+ Version.RESOURCES_VERSION = Version.RESOURCES_VERSION.replace("-snapshot", "-" + Version.BUILD_TIME.replace(" ", "").replace(":", "").replace("-", ""));
+ }
} catch (IOException e) {
Version.VERSION = Version.UNKNOWN;
Version.BUILD_TIME = Version.UNKNOWN;
core/pom.xml 2(+1 -1)
diff --git a/core/pom.xml b/core/pom.xml
index 77fd3b7..c759a8a 100755
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 234b632..2e585c3 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -50,6 +50,7 @@ public interface OAuth2Constants {
String AUTHORIZATION_CODE = "authorization_code";
+
String IMPLICIT = "implicit";
String PASSWORD = "password";
@@ -92,6 +93,16 @@ public interface OAuth2Constants {
String PKCE_METHOD_PLAIN = "plain";
String PKCE_METHOD_S256 = "S256";
+ String TOKEN_EXCHANGE_GRANT_TYPE="urn:ietf:params:oauth:grant-type:token-exchange";
+ String AUDIENCE="audience";
+ String SUBJECT_TOKEN="subject_token";
+ String SUBJECT_TOKEN_TYPE="subject_token_type";
+ String ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:access_token";
+ String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token";
+ String JWT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt";
+ String ID_TOKEN_TYPE="urn:ietf:params:oauth:token-type:id_token";
+
+
}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java
index 4a2b7e2..ebd49ab 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java
@@ -62,7 +62,8 @@ public class BaseAdapterConfig extends BaseRealmConfig {
protected boolean publicClient;
@JsonProperty("credentials")
protected Map<String, Object> credentials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-
+ @JsonProperty("redirect-rewrite-rules")
+ protected Map<String, String> redirectRewriteRules;
public boolean isUseResourceRoleMappings() {
return useResourceRoleMappings;
@@ -167,4 +168,14 @@ public class BaseAdapterConfig extends BaseRealmConfig {
public void setPublicClient(boolean publicClient) {
this.publicClient = publicClient;
}
+
+ public Map<String, String> getRedirectRewriteRules() {
+ return redirectRewriteRules;
+ }
+
+ public void setRedirectRewriteRules(Map<String, String> redirectRewriteRules) {
+ this.redirectRewriteRules = redirectRewriteRules;
+ }
+
+
}
diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerAccess.java b/core/src/main/java/org/keycloak/representations/docker/DockerAccess.java
new file mode 100644
index 0000000..969bcb0
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/docker/DockerAccess.java
@@ -0,0 +1,119 @@
+package org.keycloak.representations.docker;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+
+/**
+ * Per the docker auth v2 spec, access is defined like this:
+ *
+ * {
+ * "type": "repository",
+ * "name": "samalba/my-app",
+ * "actions": [
+ * "push",
+ * "pull"
+ * ]
+ * }
+ *
+ */
+public class DockerAccess {
+
+ public static final int ACCESS_TYPE = 0;
+ public static final int REPOSITORY_NAME = 1;
+ public static final int PERMISSIONS = 2;
+ public static final String DECODE_ENCODING = "UTF-8";
+
+ @JsonProperty("type")
+ protected String type;
+ @JsonProperty("name")
+ protected String name;
+ @JsonProperty("actions")
+ protected List<String> actions;
+
+ public DockerAccess() {
+ }
+
+ public DockerAccess(final String scopeParam) {
+ if (scopeParam != null) {
+ try {
+ final String unencoded = URLDecoder.decode(scopeParam, DECODE_ENCODING);
+ final String[] parts = unencoded.split(":");
+ if (parts.length != 3) {
+ throw new IllegalArgumentException(String.format("Expecting input string to have %d parts delineated by a ':' character. " +
+ "Found %d parts: %s", 3, parts.length, unencoded));
+ }
+
+ type = parts[ACCESS_TYPE];
+ name = parts[REPOSITORY_NAME];
+ if (parts[PERMISSIONS] != null) {
+ actions = Arrays.asList(parts[PERMISSIONS].split(","));
+ }
+ } catch (final UnsupportedEncodingException e) {
+ throw new IllegalStateException("Error attempting to decode scope parameter using encoding: " + DECODE_ENCODING);
+ }
+ }
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public DockerAccess setType(final String type) {
+ this.type = type;
+ return this;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public DockerAccess setName(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ public List<String> getActions() {
+ return actions;
+ }
+
+ public DockerAccess setActions(final List<String> actions) {
+ this.actions = actions;
+ return this;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DockerAccess)) return false;
+
+ final DockerAccess that = (DockerAccess) o;
+
+ if (type != null ? !type.equals(that.type) : that.type != null) return false;
+ if (name != null ? !name.equals(that.name) : that.name != null) return false;
+ return actions != null ? actions.equals(that.actions) : that.actions == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = type != null ? type.hashCode() : 0;
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (actions != null ? actions.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "DockerAccess{" +
+ "type='" + type + '\'' +
+ ", name='" + name + '\'' +
+ ", actions=" + actions +
+ '}';
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerError.java b/core/src/main/java/org/keycloak/representations/docker/DockerError.java
new file mode 100644
index 0000000..b33bb58
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/docker/DockerError.java
@@ -0,0 +1,84 @@
+package org.keycloak.representations.docker;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/**
+ * JSON Representation of a Docker Error in the following format:
+ *
+ *
+ * {
+ * "code": "UNAUTHORIZED",
+ * "message": "access to the requested resource is not authorized",
+ * "detail": [
+ * {
+ * "Type": "repository",
+ * "Name": "samalba/my-app",
+ * "Action": "pull"
+ * },
+ * {
+ * "Type": "repository",
+ * "Name": "samalba/my-app",
+ * "Action": "push"
+ * }
+ * ]
+ * }
+ */
+public class DockerError {
+
+
+ @JsonProperty("code")
+ private final String errorCode;
+ @JsonProperty("message")
+ private final String message;
+ @JsonProperty("detail")
+ private final List<DockerAccess> dockerErrorDetails;
+
+ public DockerError(final String errorCode, final String message, final List<DockerAccess> dockerErrorDetails) {
+ this.errorCode = errorCode;
+ this.message = message;
+ this.dockerErrorDetails = dockerErrorDetails;
+ }
+
+ public String getErrorCode() {
+ return errorCode;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public List<DockerAccess> getDockerErrorDetails() {
+ return dockerErrorDetails;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DockerError)) return false;
+
+ final DockerError that = (DockerError) o;
+
+ if (errorCode != that.errorCode) return false;
+ if (message != null ? !message.equals(that.message) : that.message != null) return false;
+ return dockerErrorDetails != null ? dockerErrorDetails.equals(that.dockerErrorDetails) : that.dockerErrorDetails == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = errorCode != null ? errorCode.hashCode() : 0;
+ result = 31 * result + (message != null ? message.hashCode() : 0);
+ result = 31 * result + (dockerErrorDetails != null ? dockerErrorDetails.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "DockerError{" +
+ "errorCode=" + errorCode +
+ ", message='" + message + '\'' +
+ ", dockerErrorDetails=" + dockerErrorDetails +
+ '}';
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerErrorResponseToken.java b/core/src/main/java/org/keycloak/representations/docker/DockerErrorResponseToken.java
new file mode 100644
index 0000000..3d961ce
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/docker/DockerErrorResponseToken.java
@@ -0,0 +1,38 @@
+package org.keycloak.representations.docker;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+public class DockerErrorResponseToken {
+
+
+ @JsonProperty("errors")
+ private final List<DockerError> errorList;
+
+ public DockerErrorResponseToken(final List<DockerError> errorList) {
+ this.errorList = errorList;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DockerErrorResponseToken)) return false;
+
+ final DockerErrorResponseToken that = (DockerErrorResponseToken) o;
+
+ return errorList != null ? errorList.equals(that.errorList) : that.errorList == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return errorList != null ? errorList.hashCode() : 0;
+ }
+
+ @Override
+ public String toString() {
+ return "DockerErrorResponseToken{" +
+ "errorList=" + errorList +
+ '}';
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerResponse.java b/core/src/main/java/org/keycloak/representations/docker/DockerResponse.java
new file mode 100644
index 0000000..98074fa
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/docker/DockerResponse.java
@@ -0,0 +1,88 @@
+package org.keycloak.representations.docker;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Creates a response understandable by the docker client in the form:
+ *
+ {
+ "token" : "eyJh...nSQ",
+ "expires_in" : 300,
+ "issued_at" : "2016-09-02T10:56:33Z"
+ }
+ */
+public class DockerResponse {
+
+ @JsonProperty("token")
+ private String token;
+ @JsonProperty("expires_in")
+ private Integer expires_in;
+ @JsonProperty("issued_at")
+ private String issued_at;
+
+ public DockerResponse() {
+ }
+
+ public DockerResponse(final String token, final Integer expires_in, final String issued_at) {
+ this.token = token;
+ this.expires_in = expires_in;
+ this.issued_at = issued_at;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public DockerResponse setToken(final String token) {
+ this.token = token;
+ return this;
+ }
+
+ public Integer getExpires_in() {
+ return expires_in;
+ }
+
+ public DockerResponse setExpires_in(final Integer expires_in) {
+ this.expires_in = expires_in;
+ return this;
+ }
+
+ public String getIssued_at() {
+ return issued_at;
+ }
+
+ public DockerResponse setIssued_at(final String issued_at) {
+ this.issued_at = issued_at;
+ return this;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DockerResponse)) return false;
+
+ final DockerResponse that = (DockerResponse) o;
+
+ if (token != null ? !token.equals(that.token) : that.token != null) return false;
+ if (expires_in != null ? !expires_in.equals(that.expires_in) : that.expires_in != null) return false;
+ return issued_at != null ? issued_at.equals(that.issued_at) : that.issued_at == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = token != null ? token.hashCode() : 0;
+ result = 31 * result + (expires_in != null ? expires_in.hashCode() : 0);
+ result = 31 * result + (issued_at != null ? issued_at.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "DockerResponse{" +
+ "token='" + token + '\'' +
+ ", expires_in='" + expires_in + '\'' +
+ ", issued_at='" + issued_at + '\'' +
+ '}';
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerResponseToken.java b/core/src/main/java/org/keycloak/representations/docker/DockerResponseToken.java
new file mode 100644
index 0000000..faee452
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/docker/DockerResponseToken.java
@@ -0,0 +1,97 @@
+package org.keycloak.representations.docker;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.keycloak.representations.JsonWebToken;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * * {
+ * "iss": "auth.docker.com",
+ * "sub": "jlhawn",
+ * "aud": "registry.docker.com",
+ * "exp": 1415387315,
+ * "nbf": 1415387015,
+ * "iat": 1415387015,
+ * "jti": "tYJCO1c6cnyy7kAn0c7rKPgbV1H1bFws",
+ * "access": [
+ * {
+ * "type": "repository",
+ * "name": "samalba/my-app",
+ * "actions": [
+ * "push"
+ * ]
+ * }
+ * ]
+ * }
+ */
+public class DockerResponseToken extends JsonWebToken {
+
+ @JsonProperty("access")
+ protected List<DockerAccess> accessItems = new ArrayList<>();
+
+ public List<DockerAccess> getAccessItems() {
+ return accessItems;
+ }
+
+ @Override
+ public DockerResponseToken id(final String id) {
+ super.id(id);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken expiration(final int expiration) {
+ super.expiration(expiration);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken notBefore(final int notBefore) {
+ super.notBefore(notBefore);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken issuedNow() {
+ super.issuedNow();
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken issuedAt(final int issuedAt) {
+ super.issuedAt(issuedAt);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken issuer(final String issuer) {
+ super.issuer(issuer);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken audience(final String... audience) {
+ super.audience(audience);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken subject(final String subject) {
+ super.subject(subject);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken type(final String type) {
+ super.type(type);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken issuedFor(final String issuedFor) {
+ super.issuedFor(issuedFor);
+ return this;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java
index d597cf3..95c7ca3 100755
--- a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java
@@ -155,4 +155,101 @@ public class CredentialRepresentation {
public void setConfig(MultivaluedHashMap<String, String> config) {
this.config = config;
}
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((algorithm == null) ? 0 : algorithm.hashCode());
+ result = prime * result + ((config == null) ? 0 : config.hashCode());
+ result = prime * result + ((counter == null) ? 0 : counter.hashCode());
+ result = prime * result + ((createdDate == null) ? 0 : createdDate.hashCode());
+ result = prime * result + ((device == null) ? 0 : device.hashCode());
+ result = prime * result + ((digits == null) ? 0 : digits.hashCode());
+ result = prime * result + ((hashIterations == null) ? 0 : hashIterations.hashCode());
+ result = prime * result + ((hashedSaltedValue == null) ? 0 : hashedSaltedValue.hashCode());
+ result = prime * result + ((period == null) ? 0 : period.hashCode());
+ result = prime * result + ((salt == null) ? 0 : salt.hashCode());
+ result = prime * result + ((temporary == null) ? 0 : temporary.hashCode());
+ result = prime * result + ((type == null) ? 0 : type.hashCode());
+ result = prime * result + ((value == null) ? 0 : value.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ CredentialRepresentation other = (CredentialRepresentation) obj;
+ if (algorithm == null) {
+ if (other.algorithm != null)
+ return false;
+ } else if (!algorithm.equals(other.algorithm))
+ return false;
+ if (config == null) {
+ if (other.config != null)
+ return false;
+ } else if (!config.equals(other.config))
+ return false;
+ if (counter == null) {
+ if (other.counter != null)
+ return false;
+ } else if (!counter.equals(other.counter))
+ return false;
+ if (createdDate == null) {
+ if (other.createdDate != null)
+ return false;
+ } else if (!createdDate.equals(other.createdDate))
+ return false;
+ if (device == null) {
+ if (other.device != null)
+ return false;
+ } else if (!device.equals(other.device))
+ return false;
+ if (digits == null) {
+ if (other.digits != null)
+ return false;
+ } else if (!digits.equals(other.digits))
+ return false;
+ if (hashIterations == null) {
+ if (other.hashIterations != null)
+ return false;
+ } else if (!hashIterations.equals(other.hashIterations))
+ return false;
+ if (hashedSaltedValue == null) {
+ if (other.hashedSaltedValue != null)
+ return false;
+ } else if (!hashedSaltedValue.equals(other.hashedSaltedValue))
+ return false;
+ if (period == null) {
+ if (other.period != null)
+ return false;
+ } else if (!period.equals(other.period))
+ return false;
+ if (salt == null) {
+ if (other.salt != null)
+ return false;
+ } else if (!salt.equals(other.salt))
+ return false;
+ if (temporary == null) {
+ if (other.temporary != null)
+ return false;
+ } else if (!temporary.equals(other.temporary))
+ return false;
+ if (type == null) {
+ if (other.type != null)
+ return false;
+ } else if (!type.equals(other.type))
+ return false;
+ if (value == null) {
+ if (other.value != null)
+ return false;
+ } else if (!value.equals(other.value))
+ return false;
+ return true;
+ }
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index 670e1d8..c3dd733 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -137,6 +137,7 @@ public class RealmRepresentation {
protected String directGrantFlow;
protected String resetCredentialsFlow;
protected String clientAuthenticationFlow;
+ protected String dockerAuthenticationFlow;
protected Map<String, String> attributes;
@@ -884,6 +885,15 @@ public class RealmRepresentation {
this.clientAuthenticationFlow = clientAuthenticationFlow;
}
+ public String getDockerAuthenticationFlow() {
+ return dockerAuthenticationFlow;
+ }
+
+ public RealmRepresentation setDockerAuthenticationFlow(final String dockerAuthenticationFlow) {
+ this.dockerAuthenticationFlow = dockerAuthenticationFlow;
+ return this;
+ }
+
public String getKeycloakVersion() {
return keycloakVersion;
}
diff --git a/core/src/main/java/org/keycloak/representations/info/ProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/info/ProviderRepresentation.java
index e1b704e..8dcf006 100644
--- a/core/src/main/java/org/keycloak/representations/info/ProviderRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/info/ProviderRepresentation.java
@@ -21,8 +21,18 @@ import java.util.Map;
public class ProviderRepresentation {
+ private int order;
+
private Map<String, String> operationalInfo;
+ public int getOrder() {
+ return order;
+ }
+
+ public void setOrder(int priorityUI) {
+ this.order = priorityUI;
+ }
+
public Map<String, String> getOperationalInfo() {
return operationalInfo;
}
dependencies/pom.xml 2(+1 -1)
diff --git a/dependencies/pom.xml b/dependencies/pom.xml
index e98ce5a..f0b9730 100755
--- a/dependencies/pom.xml
+++ b/dependencies/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
dependencies/server-all/pom.xml 2(+1 -1)
diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml
index 4bf1baa..4061b97 100755
--- a/dependencies/server-all/pom.xml
+++ b/dependencies/server-all/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
dependencies/server-min/pom.xml 2(+1 -1)
diff --git a/dependencies/server-min/pom.xml b/dependencies/server-min/pom.xml
index f4938b0..216bbad 100755
--- a/dependencies/server-min/pom.xml
+++ b/dependencies/server-min/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml b/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml
index bd92f05..f10d11a 100755
--- a/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml
+++ b/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml b/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml
index 004e6c1..2529bc4 100755
--- a/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml
+++ b/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml
@@ -25,7 +25,7 @@
<parent>
<artifactId>keycloak-as7-eap6-adapter-dist-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml b/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml
index 670fcd4..f2c5dac 100755
--- a/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml
+++ b/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-as7-eap6-adapter-dist-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/as7-eap6-adapter/pom.xml b/distribution/adapters/as7-eap6-adapter/pom.xml
index 6096915..e94db3a 100644
--- a/distribution/adapters/as7-eap6-adapter/pom.xml
+++ b/distribution/adapters/as7-eap6-adapter/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak AS7 / JBoss EAP 6 Adapter Distros</name>
diff --git a/distribution/adapters/fuse-adapter-zip/pom.xml b/distribution/adapters/fuse-adapter-zip/pom.xml
index 7bfd0d2..03db2b4 100644
--- a/distribution/adapters/fuse-adapter-zip/pom.xml
+++ b/distribution/adapters/fuse-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/jetty81-adapter-zip/pom.xml b/distribution/adapters/jetty81-adapter-zip/pom.xml
index 76713f4..5cfa198 100755
--- a/distribution/adapters/jetty81-adapter-zip/pom.xml
+++ b/distribution/adapters/jetty81-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/jetty91-adapter-zip/pom.xml b/distribution/adapters/jetty91-adapter-zip/pom.xml
index e83caa8..506bf22 100755
--- a/distribution/adapters/jetty91-adapter-zip/pom.xml
+++ b/distribution/adapters/jetty91-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/jetty92-adapter-zip/pom.xml b/distribution/adapters/jetty92-adapter-zip/pom.xml
index eb247cb..62c81b8 100755
--- a/distribution/adapters/jetty92-adapter-zip/pom.xml
+++ b/distribution/adapters/jetty92-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/jetty93-adapter-zip/pom.xml b/distribution/adapters/jetty93-adapter-zip/pom.xml
index c7ec2de..f42fcd9 100644
--- a/distribution/adapters/jetty93-adapter-zip/pom.xml
+++ b/distribution/adapters/jetty93-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/jetty94-adapter-zip/pom.xml b/distribution/adapters/jetty94-adapter-zip/pom.xml
index d776cb0..bd199c9 100644
--- a/distribution/adapters/jetty94-adapter-zip/pom.xml
+++ b/distribution/adapters/jetty94-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/js-adapter-zip/pom.xml b/distribution/adapters/js-adapter-zip/pom.xml
index 1a93bb4..be37dc8 100755
--- a/distribution/adapters/js-adapter-zip/pom.xml
+++ b/distribution/adapters/js-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/osgi/features/pom.xml b/distribution/adapters/osgi/features/pom.xml
index 8cef75b..36ef9f3 100755
--- a/distribution/adapters/osgi/features/pom.xml
+++ b/distribution/adapters/osgi/features/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<name>Keycloak OSGI Features</name>
diff --git a/distribution/adapters/osgi/jaas/pom.xml b/distribution/adapters/osgi/jaas/pom.xml
index 1c7182c..2994b3c 100755
--- a/distribution/adapters/osgi/jaas/pom.xml
+++ b/distribution/adapters/osgi/jaas/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<name>Keycloak OSGI JAAS Realm Configuration</name>
distribution/adapters/osgi/pom.xml 2(+1 -1)
diff --git a/distribution/adapters/osgi/pom.xml b/distribution/adapters/osgi/pom.xml
index 523e714..61b801a 100755
--- a/distribution/adapters/osgi/pom.xml
+++ b/distribution/adapters/osgi/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak OSGI Integration</name>
diff --git a/distribution/adapters/osgi/thirdparty/pom.xml b/distribution/adapters/osgi/thirdparty/pom.xml
index db04706..bf42fa8 100755
--- a/distribution/adapters/osgi/thirdparty/pom.xml
+++ b/distribution/adapters/osgi/thirdparty/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
distribution/adapters/pom.xml 2(+1 -1)
diff --git a/distribution/adapters/pom.xml b/distribution/adapters/pom.xml
index 2fc0f9c..6e4193f 100755
--- a/distribution/adapters/pom.xml
+++ b/distribution/adapters/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-distribution-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Adapters Distribution Parent</name>
diff --git a/distribution/adapters/tomcat6-adapter-zip/pom.xml b/distribution/adapters/tomcat6-adapter-zip/pom.xml
index 77dd6bc..1ba6502 100755
--- a/distribution/adapters/tomcat6-adapter-zip/pom.xml
+++ b/distribution/adapters/tomcat6-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/tomcat7-adapter-zip/pom.xml b/distribution/adapters/tomcat7-adapter-zip/pom.xml
index 5ccad77..391c642 100755
--- a/distribution/adapters/tomcat7-adapter-zip/pom.xml
+++ b/distribution/adapters/tomcat7-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/tomcat8-adapter-zip/pom.xml b/distribution/adapters/tomcat8-adapter-zip/pom.xml
index e02660f..d87f87b 100755
--- a/distribution/adapters/tomcat8-adapter-zip/pom.xml
+++ b/distribution/adapters/tomcat8-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/wf8-adapter/pom.xml b/distribution/adapters/wf8-adapter/pom.xml
index f975da8..05b72b5 100644
--- a/distribution/adapters/wf8-adapter/pom.xml
+++ b/distribution/adapters/wf8-adapter/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak Wildfly 8 Adapter</name>
diff --git a/distribution/adapters/wf8-adapter/wf8-adapter-zip/pom.xml b/distribution/adapters/wf8-adapter/wf8-adapter-zip/pom.xml
index fdb1c3f..a88f373 100755
--- a/distribution/adapters/wf8-adapter/wf8-adapter-zip/pom.xml
+++ b/distribution/adapters/wf8-adapter/wf8-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/wf8-adapter/wf8-modules/pom.xml b/distribution/adapters/wf8-adapter/wf8-modules/pom.xml
index b7cd807..88f191b 100755
--- a/distribution/adapters/wf8-adapter/wf8-modules/pom.xml
+++ b/distribution/adapters/wf8-adapter/wf8-modules/pom.xml
@@ -25,7 +25,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/adapters/wildfly-adapter/pom.xml b/distribution/adapters/wildfly-adapter/pom.xml
index 6090e9c..d116ab1 100644
--- a/distribution/adapters/wildfly-adapter/pom.xml
+++ b/distribution/adapters/wildfly-adapter/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-adapters-distribution-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-wildfly-adapter-dist</artifactId>
distribution/api-docs-dist/pom.xml 32(+24 -8)
diff --git a/distribution/api-docs-dist/pom.xml b/distribution/api-docs-dist/pom.xml
index a50916a..a13c521 100755
--- a/distribution/api-docs-dist/pom.xml
+++ b/distribution/api-docs-dist/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-distribution-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-api-docs-dist</artifactId>
@@ -63,13 +63,6 @@
</executions>
</plugin>
<plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-deploy-plugin</artifactId>
- <configuration>
- <skip>true</skip>
- </configuration>
- </plugin>
- <plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
@@ -96,4 +89,27 @@
</plugins>
</build>
+
+ <profiles>
+ <profile>
+ <id>community</id>
+ <activation>
+ <property>
+ <name>!product</name>
+ </property>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+
</project>
distribution/demo-dist/pom.xml 2(+1 -1)
diff --git a/distribution/demo-dist/pom.xml b/distribution/demo-dist/pom.xml
index c1668f3..95ae460 100755
--- a/distribution/demo-dist/pom.xml
+++ b/distribution/demo-dist/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-distribution-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-demo-dist</artifactId>
distribution/downloads/pom.xml 2(+1 -1)
diff --git a/distribution/downloads/pom.xml b/distribution/downloads/pom.xml
index eb22c9b..4c92119 100755
--- a/distribution/downloads/pom.xml
+++ b/distribution/downloads/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-distribution-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-dist-downloads</artifactId>
distribution/examples-dist/pom.xml 2(+1 -1)
diff --git a/distribution/examples-dist/pom.xml b/distribution/examples-dist/pom.xml
index ac57760..5db83ef 100755
--- a/distribution/examples-dist/pom.xml
+++ b/distribution/examples-dist/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-distribution-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-examples-dist</artifactId>
diff --git a/distribution/feature-packs/adapter-feature-pack/pom.xml b/distribution/feature-packs/adapter-feature-pack/pom.xml
index 0dbb9fc..6f4b77b 100755
--- a/distribution/feature-packs/adapter-feature-pack/pom.xml
+++ b/distribution/feature-packs/adapter-feature-pack/pom.xml
@@ -19,7 +19,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>feature-packs-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
distribution/feature-packs/pom.xml 2(+1 -1)
diff --git a/distribution/feature-packs/pom.xml b/distribution/feature-packs/pom.xml
index bf31d50..b32593d 100644
--- a/distribution/feature-packs/pom.xml
+++ b/distribution/feature-packs/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-distribution-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Feature Pack Builds</name>
diff --git a/distribution/feature-packs/server-feature-pack/pom.xml b/distribution/feature-packs/server-feature-pack/pom.xml
index 2d39ad1..e2f1f41 100644
--- a/distribution/feature-packs/server-feature-pack/pom.xml
+++ b/distribution/feature-packs/server-feature-pack/pom.xml
@@ -19,7 +19,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>feature-packs-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
distribution/pom.xml 2(+1 -1)
diff --git a/distribution/pom.xml b/distribution/pom.xml
index f8c0d2d..165323d 100755
--- a/distribution/pom.xml
+++ b/distribution/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
distribution/proxy-dist/pom.xml 2(+1 -1)
diff --git a/distribution/proxy-dist/pom.xml b/distribution/proxy-dist/pom.xml
index ee89fa7..79d4468 100755
--- a/distribution/proxy-dist/pom.xml
+++ b/distribution/proxy-dist/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-distribution-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-proxy-dist</artifactId>
diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml
index 935c85f..4165230 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/pom.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/pom.xml
index 98ee15f..f2650df 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/pom.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/pom.xml
@@ -25,7 +25,7 @@
<parent>
<artifactId>keycloak-saml-as7-eap6-adapter-dist-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml
index 4973aa1..ba66c82 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml
@@ -28,6 +28,7 @@
</resources>
<dependencies>
<module name="javax.api"/>
+ <module name="javax.xml.soap.api"/>
<module name="org.jboss.logging"/>
<module name="org.keycloak.keycloak-adapter-spi"/>
<module name="org.keycloak.keycloak-saml-core-public"/>
diff --git a/distribution/saml-adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml b/distribution/saml-adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml
index d0da2e4..54c249d 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-saml-as7-eap6-adapter-dist-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/distribution/saml-adapters/as7-eap6-adapter/pom.xml b/distribution/saml-adapters/as7-eap6-adapter/pom.xml
index 0a4d2cc..e1c378c 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/pom.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak SAML AS7 / JBoss EAP 6 Adapter Distros</name>
diff --git a/distribution/saml-adapters/jetty81-adapter-zip/pom.xml b/distribution/saml-adapters/jetty81-adapter-zip/pom.xml
index e74e126..8c8f99f 100755
--- a/distribution/saml-adapters/jetty81-adapter-zip/pom.xml
+++ b/distribution/saml-adapters/jetty81-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/saml-adapters/jetty92-adapter-zip/pom.xml b/distribution/saml-adapters/jetty92-adapter-zip/pom.xml
index c125082..f5c3481 100755
--- a/distribution/saml-adapters/jetty92-adapter-zip/pom.xml
+++ b/distribution/saml-adapters/jetty92-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/saml-adapters/jetty93-adapter-zip/pom.xml b/distribution/saml-adapters/jetty93-adapter-zip/pom.xml
index 6fe0cb5..e6ef28e 100644
--- a/distribution/saml-adapters/jetty93-adapter-zip/pom.xml
+++ b/distribution/saml-adapters/jetty93-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/saml-adapters/jetty94-adapter-zip/pom.xml b/distribution/saml-adapters/jetty94-adapter-zip/pom.xml
index 5e66e8f..8b86d43 100644
--- a/distribution/saml-adapters/jetty94-adapter-zip/pom.xml
+++ b/distribution/saml-adapters/jetty94-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
distribution/saml-adapters/pom.xml 2(+1 -1)
diff --git a/distribution/saml-adapters/pom.xml b/distribution/saml-adapters/pom.xml
index 4949548..81828d2 100755
--- a/distribution/saml-adapters/pom.xml
+++ b/distribution/saml-adapters/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-distribution-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>SAML Adapters Distribution Parent</name>
diff --git a/distribution/saml-adapters/tomcat6-adapter-zip/pom.xml b/distribution/saml-adapters/tomcat6-adapter-zip/pom.xml
index 62ef4ce..01cdab6 100755
--- a/distribution/saml-adapters/tomcat6-adapter-zip/pom.xml
+++ b/distribution/saml-adapters/tomcat6-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/saml-adapters/tomcat7-adapter-zip/pom.xml b/distribution/saml-adapters/tomcat7-adapter-zip/pom.xml
index d9eef4e..f4a5a2d 100755
--- a/distribution/saml-adapters/tomcat7-adapter-zip/pom.xml
+++ b/distribution/saml-adapters/tomcat7-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/saml-adapters/tomcat8-adapter-zip/pom.xml b/distribution/saml-adapters/tomcat8-adapter-zip/pom.xml
index be42cee..da7c218 100755
--- a/distribution/saml-adapters/tomcat8-adapter-zip/pom.xml
+++ b/distribution/saml-adapters/tomcat8-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/saml-adapters/wildfly-adapter/pom.xml b/distribution/saml-adapters/wildfly-adapter/pom.xml
index 6bec453..6d2a6e2 100755
--- a/distribution/saml-adapters/wildfly-adapter/pom.xml
+++ b/distribution/saml-adapters/wildfly-adapter/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak Wildfly SAML Adapter</name>
diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/pom.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/pom.xml
index a30ecef..1b80a59 100755
--- a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/pom.xml
+++ b/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml
index ec96595..e398bbb 100755
--- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml
+++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml
@@ -25,7 +25,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml
index e19e0f0..ef44695 100755
--- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml
+++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml
@@ -28,6 +28,7 @@
</resources>
<dependencies>
<module name="javax.api"/>
+ <module name="javax.xml.soap.api"/>
<module name="org.jboss.logging"/>
<module name="org.keycloak.keycloak-adapter-spi"/>
<module name="org.keycloak.keycloak-saml-adapter-api-public"/>
distribution/server-dist/pom.xml 14(+13 -1)
diff --git a/distribution/server-dist/pom.xml b/distribution/server-dist/pom.xml
index fd21630..6b425c0 100755
--- a/distribution/server-dist/pom.xml
+++ b/distribution/server-dist/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-distribution-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-server-dist</artifactId>
@@ -34,11 +34,23 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-feature-pack</artifactId>
<type>zip</type>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-cli-dist</artifactId>
<type>zip</type>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
</dependencies>
distribution/server-overlay/pom.xml 2(+1 -1)
diff --git a/distribution/server-overlay/pom.xml b/distribution/server-overlay/pom.xml
index d3310bb..f8a0c53 100755
--- a/distribution/server-overlay/pom.xml
+++ b/distribution/server-overlay/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-distribution-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-server-overlay</artifactId>
examples/admin-client/pom.xml 2(+1 -1)
diff --git a/examples/admin-client/pom.xml b/examples/admin-client/pom.xml
index e7339f7..c5ac43e 100755
--- a/examples/admin-client/pom.xml
+++ b/examples/admin-client/pom.xml
@@ -22,7 +22,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak Examples - Admin Client</name>
examples/authz/hello-world/pom.xml 2(+1 -1)
diff --git a/examples/authz/hello-world/pom.xml b/examples/authz/hello-world/pom.xml
index 50e5199..afdc3fb 100755
--- a/examples/authz/hello-world/pom.xml
+++ b/examples/authz/hello-world/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-example-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/examples/authz/hello-world-authz-service/pom.xml b/examples/authz/hello-world-authz-service/pom.xml
index 2067d82..26c1777 100755
--- a/examples/authz/hello-world-authz-service/pom.xml
+++ b/examples/authz/hello-world-authz-service/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-example-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/examples/authz/photoz/photoz-authz-policy/pom.xml b/examples/authz/photoz/photoz-authz-policy/pom.xml
index 8115179..08267aa 100755
--- a/examples/authz/photoz/photoz-authz-policy/pom.xml
+++ b/examples/authz/photoz/photoz-authz-policy/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-photoz-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/examples/authz/photoz/photoz-html5-client/pom.xml b/examples/authz/photoz/photoz-html5-client/pom.xml
index 09db1e5..5ff5fdb 100755
--- a/examples/authz/photoz/photoz-html5-client/pom.xml
+++ b/examples/authz/photoz/photoz-html5-client/pom.xml
@@ -5,7 +5,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-photoz-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/examples/authz/photoz/photoz-restful-api/pom.xml b/examples/authz/photoz/photoz-restful-api/pom.xml
index 918c258..94b73dc 100755
--- a/examples/authz/photoz/photoz-restful-api/pom.xml
+++ b/examples/authz/photoz/photoz-restful-api/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-photoz-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
index 129a11a..1fe6675 100644
--- a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
+++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
@@ -83,14 +83,14 @@ public class AlbumService {
@GET
@Produces("application/json")
public Response findAll() {
- return Response.ok(this.entityManager.createQuery("from Album where userId = '" + request.getUserPrincipal().getName() + "'").getResultList()).build();
+ return Response.ok(this.entityManager.createQuery("from Album where userId = :id").setParameter("id", request.getUserPrincipal().getName()).getResultList()).build();
}
@GET
@Path("{id}")
@Produces("application/json")
public Response findById(@PathParam("id") String id) {
- List result = this.entityManager.createQuery("from Album where id = " + id).getResultList();
+ List result = this.entityManager.createQuery("from Album where id = :id").setParameter("id", id).getResultList();
if (result.isEmpty()) {
return Response.status(Status.NOT_FOUND).build();
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java
index 92e300d..6259122 100644
--- a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java
+++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java
@@ -43,7 +43,7 @@ public class ProfileService {
@Produces("application/json")
public Response view(@Context HttpServletRequest request) {
Principal userPrincipal = request.getUserPrincipal();
- List albums = this.entityManager.createQuery("from Album where userId = '" + userPrincipal.getName() + "'").getResultList();
+ List albums = this.entityManager.createQuery("from Album where userId = :id").setParameter("id", userPrincipal.getName()).getResultList();
return Response.ok(new Profile(userPrincipal.getName(), albums.size())).build();
}
examples/authz/photoz/pom.xml 2(+1 -1)
diff --git a/examples/authz/photoz/pom.xml b/examples/authz/photoz/pom.xml
index a863cd2..cbaeb24 100755
--- a/examples/authz/photoz/pom.xml
+++ b/examples/authz/photoz/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-example-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
examples/authz/pom.xml 2(+1 -1)
diff --git a/examples/authz/pom.xml b/examples/authz/pom.xml
index 03012e7..06adb3c 100755
--- a/examples/authz/pom.xml
+++ b/examples/authz/pom.xml
@@ -6,7 +6,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
examples/authz/servlet-authz/pom.xml 2(+1 -1)
diff --git a/examples/authz/servlet-authz/pom.xml b/examples/authz/servlet-authz/pom.xml
index 68e672c..ffcf7f2 100755
--- a/examples/authz/servlet-authz/pom.xml
+++ b/examples/authz/servlet-authz/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-example-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
examples/basic-auth/pom.xml 2(+1 -1)
diff --git a/examples/basic-auth/pom.xml b/examples/basic-auth/pom.xml
index 0c12d2b..af19e13 100755
--- a/examples/basic-auth/pom.xml
+++ b/examples/basic-auth/pom.xml
@@ -23,7 +23,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak Examples - Basic Auth</name>
diff --git a/examples/broker/facebook-authentication/pom.xml b/examples/broker/facebook-authentication/pom.xml
index c0ef110..0fb71b4 100755
--- a/examples/broker/facebook-authentication/pom.xml
+++ b/examples/broker/facebook-authentication/pom.xml
@@ -23,7 +23,7 @@
<parent>
<artifactId>keycloak-examples-broker-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak Broker Examples - Facebook Authentication</name>
diff --git a/examples/broker/google-authentication/pom.xml b/examples/broker/google-authentication/pom.xml
index d29e41f..e89fbc5 100755
--- a/examples/broker/google-authentication/pom.xml
+++ b/examples/broker/google-authentication/pom.xml
@@ -23,7 +23,7 @@
<parent>
<artifactId>keycloak-examples-broker-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak Broker Examples - Google Authentication</name>
examples/broker/pom.xml 2(+1 -1)
diff --git a/examples/broker/pom.xml b/examples/broker/pom.xml
index aed134d..5797474 100755
--- a/examples/broker/pom.xml
+++ b/examples/broker/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Broker Examples</name>
diff --git a/examples/broker/saml-broker-authentication/pom.xml b/examples/broker/saml-broker-authentication/pom.xml
index eda4dd7..3226fbc 100755
--- a/examples/broker/saml-broker-authentication/pom.xml
+++ b/examples/broker/saml-broker-authentication/pom.xml
@@ -23,7 +23,7 @@
<parent>
<artifactId>keycloak-examples-broker-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak Broker Examples - SAML Identity Provider Brokering</name>
diff --git a/examples/broker/twitter-authentication/pom.xml b/examples/broker/twitter-authentication/pom.xml
index aa54475..188035d 100755
--- a/examples/broker/twitter-authentication/pom.xml
+++ b/examples/broker/twitter-authentication/pom.xml
@@ -23,7 +23,7 @@
<parent>
<artifactId>keycloak-examples-broker-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak Broker Examples - Twitter Authentication</name>
diff --git a/examples/cors/angular-product-app/pom.xml b/examples/cors/angular-product-app/pom.xml
index 1a2a669..28e65fc 100755
--- a/examples/cors/angular-product-app/pom.xml
+++ b/examples/cors/angular-product-app/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-cors-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/cors/database-service/pom.xml b/examples/cors/database-service/pom.xml
index 8e92987..80ccf11 100755
--- a/examples/cors/database-service/pom.xml
+++ b/examples/cors/database-service/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-cors-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
examples/cors/pom.xml 2(+1 -1)
diff --git a/examples/cors/pom.xml b/examples/cors/pom.xml
index 40b2ac8..3e4d71a 100755
--- a/examples/cors/pom.xml
+++ b/examples/cors/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak Examples - CORS</name>
diff --git a/examples/demo-template/admin-access-app/pom.xml b/examples/demo-template/admin-access-app/pom.xml
index 7e1f543..fd2565f 100755
--- a/examples/demo-template/admin-access-app/pom.xml
+++ b/examples/demo-template/admin-access-app/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/demo-template/angular-product-app/pom.xml b/examples/demo-template/angular-product-app/pom.xml
index b02bddf..06c4b2e 100755
--- a/examples/demo-template/angular-product-app/pom.xml
+++ b/examples/demo-template/angular-product-app/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/demo-template/customer-app/pom.xml b/examples/demo-template/customer-app/pom.xml
index 51cf448..7c59225 100755
--- a/examples/demo-template/customer-app/pom.xml
+++ b/examples/demo-template/customer-app/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/demo-template/customer-app-cli/pom.xml b/examples/demo-template/customer-app-cli/pom.xml
index eeebf5b..f58edd5 100755
--- a/examples/demo-template/customer-app-cli/pom.xml
+++ b/examples/demo-template/customer-app-cli/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/demo-template/customer-app-filter/pom.xml b/examples/demo-template/customer-app-filter/pom.xml
index 444395a..3b8146f 100755
--- a/examples/demo-template/customer-app-filter/pom.xml
+++ b/examples/demo-template/customer-app-filter/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/demo-template/customer-app-js/pom.xml b/examples/demo-template/customer-app-js/pom.xml
index 7e252c8..e5e6ca9 100755
--- a/examples/demo-template/customer-app-js/pom.xml
+++ b/examples/demo-template/customer-app-js/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/demo-template/database-service/pom.xml b/examples/demo-template/database-service/pom.xml
index 862041b..e4252a4 100755
--- a/examples/demo-template/database-service/pom.xml
+++ b/examples/demo-template/database-service/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/demo-template/example-ear/pom.xml b/examples/demo-template/example-ear/pom.xml
index 8a3d456..3c4d3a0 100755
--- a/examples/demo-template/example-ear/pom.xml
+++ b/examples/demo-template/example-ear/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/demo-template/offline-access-app/pom.xml b/examples/demo-template/offline-access-app/pom.xml
index d3f6b23..f2af2fb 100755
--- a/examples/demo-template/offline-access-app/pom.xml
+++ b/examples/demo-template/offline-access-app/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
examples/demo-template/pom.xml 2(+1 -1)
diff --git a/examples/demo-template/pom.xml b/examples/demo-template/pom.xml
index bd239fb..19f8b91 100755
--- a/examples/demo-template/pom.xml
+++ b/examples/demo-template/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Demo Examples</name>
diff --git a/examples/demo-template/product-app/pom.xml b/examples/demo-template/product-app/pom.xml
index 3667ab8..c565cee 100755
--- a/examples/demo-template/product-app/pom.xml
+++ b/examples/demo-template/product-app/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/demo-template/service-account/pom.xml b/examples/demo-template/service-account/pom.xml
index ca6152f..3f85266 100755
--- a/examples/demo-template/service-account/pom.xml
+++ b/examples/demo-template/service-account/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/demo-template/third-party/pom.xml b/examples/demo-template/third-party/pom.xml
index faefb5b..6aad96c 100755
--- a/examples/demo-template/third-party/pom.xml
+++ b/examples/demo-template/third-party/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/demo-template/third-party-cdi/pom.xml b/examples/demo-template/third-party-cdi/pom.xml
index bcbb86f..20a7e7e 100755
--- a/examples/demo-template/third-party-cdi/pom.xml
+++ b/examples/demo-template/third-party-cdi/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
examples/fuse/camel/pom.xml 2(+1 -1)
diff --git a/examples/fuse/camel/pom.xml b/examples/fuse/camel/pom.xml
index ec500e8..0ca3c4e 100755
--- a/examples/fuse/camel/pom.xml
+++ b/examples/fuse/camel/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-fuse-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/fuse/customer-app-fuse/pom.xml b/examples/fuse/customer-app-fuse/pom.xml
index 38691f2..3c4bfd4 100755
--- a/examples/fuse/customer-app-fuse/pom.xml
+++ b/examples/fuse/customer-app-fuse/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-fuse-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
examples/fuse/cxf-jaxrs/pom.xml 2(+1 -1)
diff --git a/examples/fuse/cxf-jaxrs/pom.xml b/examples/fuse/cxf-jaxrs/pom.xml
index 31feea3..9d3faa0 100755
--- a/examples/fuse/cxf-jaxrs/pom.xml
+++ b/examples/fuse/cxf-jaxrs/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-fuse-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
examples/fuse/cxf-jaxws/pom.xml 2(+1 -1)
diff --git a/examples/fuse/cxf-jaxws/pom.xml b/examples/fuse/cxf-jaxws/pom.xml
index 24b2fde..f53164c 100755
--- a/examples/fuse/cxf-jaxws/pom.xml
+++ b/examples/fuse/cxf-jaxws/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-fuse-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/fuse/external-config/pom.xml b/examples/fuse/external-config/pom.xml
index 7d79af2..3f9f36d 100755
--- a/examples/fuse/external-config/pom.xml
+++ b/examples/fuse/external-config/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-fuse-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak Examples - External Config</name>
examples/fuse/features/pom.xml 2(+1 -1)
diff --git a/examples/fuse/features/pom.xml b/examples/fuse/features/pom.xml
index eb1cb2b..a5fe72e 100755
--- a/examples/fuse/features/pom.xml
+++ b/examples/fuse/features/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-fuse-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
examples/fuse/pom.xml 2(+1 -1)
diff --git a/examples/fuse/pom.xml b/examples/fuse/pom.xml
index cfa9d2c..48e0125 100755
--- a/examples/fuse/pom.xml
+++ b/examples/fuse/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Fuse Examples</name>
diff --git a/examples/fuse/product-app-fuse/pom.xml b/examples/fuse/product-app-fuse/pom.xml
index a418925..e69578e 100755
--- a/examples/fuse/product-app-fuse/pom.xml
+++ b/examples/fuse/product-app-fuse/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-fuse-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
examples/js-console/pom.xml 2(+1 -1)
diff --git a/examples/js-console/pom.xml b/examples/js-console/pom.xml
index 00e0e6a..37a64e2 100755
--- a/examples/js-console/pom.xml
+++ b/examples/js-console/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
examples/kerberos/pom.xml 2(+1 -1)
diff --git a/examples/kerberos/pom.xml b/examples/kerberos/pom.xml
index 8687695..6d2d5b1 100755
--- a/examples/kerberos/pom.xml
+++ b/examples/kerberos/pom.xml
@@ -22,7 +22,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak Examples - Kerberos Credential Delegation</name>
examples/ldap/pom.xml 2(+1 -1)
diff --git a/examples/ldap/pom.xml b/examples/ldap/pom.xml
index aa50601..62c4d1b 100644
--- a/examples/ldap/pom.xml
+++ b/examples/ldap/pom.xml
@@ -22,7 +22,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
examples/multi-tenant/pom.xml 2(+1 -1)
diff --git a/examples/multi-tenant/pom.xml b/examples/multi-tenant/pom.xml
index 6978784..6c45be0 100755
--- a/examples/multi-tenant/pom.xml
+++ b/examples/multi-tenant/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak Examples - Multi Tenant</name>
examples/pom.xml 2(+1 -1)
diff --git a/examples/pom.xml b/examples/pom.xml
index 6de9216..0421e88 100755
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak Examples</name>
diff --git a/examples/providers/authenticator/pom.xml b/examples/providers/authenticator/pom.xml
index d3f6ffc..a4042c7 100755
--- a/examples/providers/authenticator/pom.xml
+++ b/examples/providers/authenticator/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-examples-providers-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Authenticator Example</name>
diff --git a/examples/providers/domain-extension/pom.xml b/examples/providers/domain-extension/pom.xml
index db892c8..8203215 100755
--- a/examples/providers/domain-extension/pom.xml
+++ b/examples/providers/domain-extension/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-examples-providers-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Domain Extension Example</name>
diff --git a/examples/providers/event-listener-sysout/pom.xml b/examples/providers/event-listener-sysout/pom.xml
index b2d919e..6448f1e 100755
--- a/examples/providers/event-listener-sysout/pom.xml
+++ b/examples/providers/event-listener-sysout/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-examples-providers-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Event Listener System.out Example</name>
diff --git a/examples/providers/event-store-mem/pom.xml b/examples/providers/event-store-mem/pom.xml
index 379da38..ab43145 100755
--- a/examples/providers/event-store-mem/pom.xml
+++ b/examples/providers/event-store-mem/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-examples-providers-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Event Store In-Mem Example</name>
examples/providers/pom.xml 2(+1 -1)
diff --git a/examples/providers/pom.xml b/examples/providers/pom.xml
index 0ea9100..a55f520 100755
--- a/examples/providers/pom.xml
+++ b/examples/providers/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Provider Examples</name>
examples/providers/rest/pom.xml 2(+1 -1)
diff --git a/examples/providers/rest/pom.xml b/examples/providers/rest/pom.xml
index 379954f..9570a74 100755
--- a/examples/providers/rest/pom.xml
+++ b/examples/providers/rest/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-examples-providers-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>REST Example</name>
diff --git a/examples/providers/user-storage-jpa/pom.xml b/examples/providers/user-storage-jpa/pom.xml
index 3bfc8a3..a1a5637 100755
--- a/examples/providers/user-storage-jpa/pom.xml
+++ b/examples/providers/user-storage-jpa/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-examples-providers-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>User Storage JPA Provider Exapmle</name>
diff --git a/examples/providers/user-storage-simple/pom.xml b/examples/providers/user-storage-simple/pom.xml
index 065f21b..36a41cb 100755
--- a/examples/providers/user-storage-simple/pom.xml
+++ b/examples/providers/user-storage-simple/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-examples-providers-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>UserStorageProvider Simple Example</name>
examples/saml/pom.xml 2(+1 -1)
diff --git a/examples/saml/pom.xml b/examples/saml/pom.xml
index 9aaf126..e69c6df 100755
--- a/examples/saml/pom.xml
+++ b/examples/saml/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>SAML Examples</name>
diff --git a/examples/saml/post-with-encryption/pom.xml b/examples/saml/post-with-encryption/pom.xml
index 947083d..d4bed04 100755
--- a/examples/saml/post-with-encryption/pom.xml
+++ b/examples/saml/post-with-encryption/pom.xml
@@ -22,7 +22,7 @@
<parent>
<artifactId>keycloak-examples-saml-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>saml-post-encryption</artifactId>
diff --git a/examples/saml/post-with-signature/pom.xml b/examples/saml/post-with-signature/pom.xml
index 1378997..02713e6 100755
--- a/examples/saml/post-with-signature/pom.xml
+++ b/examples/saml/post-with-signature/pom.xml
@@ -22,7 +22,7 @@
<parent>
<artifactId>keycloak-examples-saml-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>sales-post-sig</artifactId>
diff --git a/examples/saml/redirect-with-signature/pom.xml b/examples/saml/redirect-with-signature/pom.xml
index 41ae3ef..9f42085 100755
--- a/examples/saml/redirect-with-signature/pom.xml
+++ b/examples/saml/redirect-with-signature/pom.xml
@@ -22,7 +22,7 @@
<parent>
<artifactId>keycloak-examples-saml-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>saml-redirect-signatures</artifactId>
examples/saml/servlet-filter/pom.xml 2(+1 -1)
diff --git a/examples/saml/servlet-filter/pom.xml b/examples/saml/servlet-filter/pom.xml
index cdfcb25..e586f3e 100755
--- a/examples/saml/servlet-filter/pom.xml
+++ b/examples/saml/servlet-filter/pom.xml
@@ -22,7 +22,7 @@
<parent>
<artifactId>keycloak-examples-saml-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>saml-servlet-filter</artifactId>
examples/themes/pom.xml 2(+1 -1)
diff --git a/examples/themes/pom.xml b/examples/themes/pom.xml
index 8282f60..7d18fdf 100755
--- a/examples/themes/pom.xml
+++ b/examples/themes/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Themes Examples</name>
diff --git a/examples/themes/src/main/resources/theme/logo-example/admin/theme.properties b/examples/themes/src/main/resources/theme/logo-example/admin/theme.properties
index 3541fb4..7c933cf 100755
--- a/examples/themes/src/main/resources/theme/logo-example/admin/theme.properties
+++ b/examples/themes/src/main/resources/theme/logo-example/admin/theme.properties
@@ -17,4 +17,4 @@
parent=keycloak
import=common/keycloak
-styles=lib/patternfly/css/patternfly.css lib/select2-3.4.1/select2.css css/styles.css css/logo.css
\ No newline at end of file
+styles=lib/patternfly/css/patternfly.css node_modules/select2/select2.css css/styles.css css/logo.css
\ No newline at end of file
federation/kerberos/pom.xml 2(+1 -1)
diff --git a/federation/kerberos/pom.xml b/federation/kerberos/pom.xml
index 5d25b4a..6b026eb 100755
--- a/federation/kerberos/pom.xml
+++ b/federation/kerberos/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
federation/ldap/pom.xml 2(+1 -1)
diff --git a/federation/ldap/pom.xml b/federation/ldap/pom.xml
index da55bf0..4618792 100755
--- a/federation/ldap/pom.xml
+++ b/federation/ldap/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java
index 1f2473b..3cf91b9 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java
@@ -18,6 +18,7 @@
package org.keycloak.storage.ldap.mappers;
import org.jboss.logging.Logger;
+import org.keycloak.models.AbstractKeycloakTransaction;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
@@ -25,12 +26,10 @@ import org.keycloak.storage.ldap.idm.model.LDAPObject;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
-public class LDAPTransaction implements KeycloakTransaction {
+public class LDAPTransaction extends AbstractKeycloakTransaction {
public static final Logger logger = Logger.getLogger(LDAPTransaction.class);
- protected TransactionState state = TransactionState.NOT_STARTED;
-
private final LDAPStorageProvider ldapProvider;
private final LDAPObject ldapUser;
@@ -39,57 +38,21 @@ public class LDAPTransaction implements KeycloakTransaction {
this.ldapUser = ldapUser;
}
- @Override
- public void begin() {
- if (state != TransactionState.NOT_STARTED) {
- throw new IllegalStateException("Transaction already started");
- }
-
- state = TransactionState.STARTED;
- }
@Override
- public void commit() {
- if (state != TransactionState.STARTED) {
- throw new IllegalStateException("Transaction in illegal state for commit: " + state);
- }
-
+ protected void commitImpl() {
if (logger.isTraceEnabled()) {
logger.trace("Transaction commit! Updating LDAP attributes for object " + ldapUser.getDn().toString() + ", attributes: " + ldapUser.getAttributes());
}
ldapProvider.getLdapIdentityStore().update(ldapUser);
- state = TransactionState.FINISHED;
}
- @Override
- public void rollback() {
- if (state != TransactionState.STARTED && state != TransactionState.ROLLBACK_ONLY) {
- throw new IllegalStateException("Transaction in illegal state for rollback: " + state);
- }
-
- logger.warn("Transaction rollback! Ignoring LDAP updates for object " + ldapUser.getDn().toString());
- state = TransactionState.FINISHED;
- }
-
- @Override
- public void setRollbackOnly() {
- state = TransactionState.ROLLBACK_ONLY;
- }
-
- @Override
- public boolean getRollbackOnly() {
- return state == TransactionState.ROLLBACK_ONLY;
- }
@Override
- public boolean isActive() {
- return state == TransactionState.STARTED || state == TransactionState.ROLLBACK_ONLY;
+ protected void rollbackImpl() {
+ logger.warn("Transaction rollback! Ignoring LDAP updates for object " + ldapUser.getDn().toString());
}
-
- protected enum TransactionState {
- NOT_STARTED, STARTED, ROLLBACK_ONLY, FINISHED
- }
}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java
index 2bf88f2..09f4051 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java
@@ -41,7 +41,7 @@ public abstract class TxAwareLDAPUserModelDelegate extends UserModelDelegate {
protected void ensureTransactionStarted() {
LDAPTransaction transaction = provider.getUserManager().getTransaction(getId());
- if (transaction.state == LDAPTransaction.TransactionState.NOT_STARTED) {
+ if (transaction.getState() == LDAPTransaction.TransactionState.NOT_STARTED) {
if (logger.isTraceEnabled()) {
logger.trace("Starting and enlisting transaction for object " + ldapUser.getDn().toString());
}
federation/pom.xml 2(+1 -1)
diff --git a/federation/pom.xml b/federation/pom.xml
index dce02fb..9136832 100755
--- a/federation/pom.xml
+++ b/federation/pom.xml
@@ -22,7 +22,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
federation/sssd/pom.xml 2(+1 -1)
diff --git a/federation/sssd/pom.xml b/federation/sssd/pom.xml
index 55e028c..1e40781 100644
--- a/federation/sssd/pom.xml
+++ b/federation/sssd/pom.xml
@@ -4,7 +4,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
integration/admin-client/pom.xml 2(+1 -1)
diff --git a/integration/admin-client/pom.xml b/integration/admin-client/pom.xml
index d59b23e..ee006ef 100755
--- a/integration/admin-client/pom.xml
+++ b/integration/admin-client/pom.xml
@@ -22,7 +22,7 @@
<parent>
<artifactId>keycloak-integration-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
index cba7eb3..c6a1edb 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
@@ -38,6 +38,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
+import java.io.IOException;
import java.util.List;
import java.util.Map;
@@ -184,6 +185,12 @@ public interface RealmResource {
@QueryParam("bindDn") String bindDn, @QueryParam("bindCredential") String bindCredential,
@QueryParam("useTruststoreSpi") String useTruststoreSpi, @QueryParam("connectionTimeout") String connectionTimeout);
+ @Path("testSMTPConnection/{config}")
+ @POST
+ @NoCache
+ @Consumes(MediaType.APPLICATION_JSON)
+ Response testSMTPConnection(final @PathParam("config") String config) throws Exception;
+
@Path("clear-realm-cache")
@POST
void clearRealmCache();
diff --git a/integration/client-cli/admin-cli/pom.xml b/integration/client-cli/admin-cli/pom.xml
index 733ba06..10d629b 100755
--- a/integration/client-cli/admin-cli/pom.xml
+++ b/integration/client-cli/admin-cli/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-client-cli-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/integration/client-cli/client-cli-dist/pom.xml b/integration/client-cli/client-cli-dist/pom.xml
index 32572ce..38a1d56 100755
--- a/integration/client-cli/client-cli-dist/pom.xml
+++ b/integration/client-cli/client-cli-dist/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-client-cli-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-client-cli-dist</artifactId>
diff --git a/integration/client-cli/client-registration-cli/pom.xml b/integration/client-cli/client-registration-cli/pom.xml
index c194dde..6a76bf0 100755
--- a/integration/client-cli/client-registration-cli/pom.xml
+++ b/integration/client-cli/client-registration-cli/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-client-cli-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
integration/client-cli/pom.xml 2(+1 -1)
diff --git a/integration/client-cli/pom.xml b/integration/client-cli/pom.xml
index e4bf087..1ffbf10 100644
--- a/integration/client-cli/pom.xml
+++ b/integration/client-cli/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-integration-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak Client CLI</name>
diff --git a/integration/client-registration/pom.xml b/integration/client-registration/pom.xml
index 979b329..c8d2d22 100755
--- a/integration/client-registration/pom.xml
+++ b/integration/client-registration/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-integration-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
integration/pom.xml 2(+1 -1)
diff --git a/integration/pom.xml b/integration/pom.xml
index 11266cd..2c82980 100755
--- a/integration/pom.xml
+++ b/integration/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Keycloak Integration</name>
misc/keycloak-test-helper/pom.xml 2(+1 -1)
diff --git a/misc/keycloak-test-helper/pom.xml b/misc/keycloak-test-helper/pom.xml
index 5a9983c..8747ec1 100644
--- a/misc/keycloak-test-helper/pom.xml
+++ b/misc/keycloak-test-helper/pom.xml
@@ -6,7 +6,7 @@
<parent>
<artifactId>keycloak-misc-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-test-helper</artifactId>
misc/pom.xml 2(+1 -1)
diff --git a/misc/pom.xml b/misc/pom.xml
index b9c3b58..d65bbe7 100644
--- a/misc/pom.xml
+++ b/misc/pom.xml
@@ -3,7 +3,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak Misc</name>
<description/>
diff --git a/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml b/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml
index f48407f..2f55a9a 100644
--- a/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml
+++ b/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml
@@ -4,7 +4,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-spring-boot-starter</artifactId>
<name>Keycloak :: Spring :: Boot :: Default :: Starter</name>
misc/spring-boot-starter/pom.xml 4(+2 -2)
diff --git a/misc/spring-boot-starter/pom.xml b/misc/spring-boot-starter/pom.xml
index cbea839..70f7daf 100644
--- a/misc/spring-boot-starter/pom.xml
+++ b/misc/spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
<parent>
<artifactId>keycloak-misc-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter-parent</artifactId>
@@ -20,7 +20,7 @@
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
misc/Testsuite.md 9(+9 -0)
diff --git a/misc/Testsuite.md b/misc/Testsuite.md
index cb77ad7..7f5e036 100644
--- a/misc/Testsuite.md
+++ b/misc/Testsuite.md
@@ -29,6 +29,15 @@ When starting the server it can also import a realm from a json file:
mvn exec:java -Pkeycloak-server -Dimport=testrealm.json
+When starting the server, https transport can be set up by setting keystore containing the server certificate
+and https port, optionally setting the truststore.
+
+ mvn exec:java -Pkeycloak-server \
+ -Djavax.net.ssl.trustStore=/path/to/truststore.jks \
+ -Djavax.net.ssl.keyStore=/path/to/keystore.jks \
+ -Djavax.net.ssl.keyStorePassword=CHANGEME \
+ -Dkeycloak.port.https=8443
+
### Live edit of html and styles
The Keycloak test server can load resources directly from the filesystem instead of the classpath. This allows editing html, styles and updating images without restarting the server. To make the server use resources from the filesystem start with:
model/infinispan/pom.xml 2(+1 -1)
diff --git a/model/infinispan/pom.xml b/model/infinispan/pom.xml
index 8917daa..7d7dfe1 100755
--- a/model/infinispan/pom.xml
+++ b/model/infinispan/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java
index 17795ca..65ca09d 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java
@@ -52,6 +52,12 @@ abstract class CrossDCAwareCacheFactory {
// For cross-DC scenario, we need to return underlying remoteCache for atomic operations to work properly
RemoteStore remoteStore = remoteStores.iterator().next();
RemoteCache remoteCache = remoteStore.getRemoteCache();
+
+ if (remoteCache == null) {
+ String cacheName = remoteStore.getConfiguration().remoteCacheName();
+ throw new IllegalStateException("Remote cache '" + cacheName + "' is not available.");
+ }
+
return new RemoteCacheWrapperFactory(remoteCache);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java
index 5a4bdb7..bd23e90 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java
@@ -25,6 +25,11 @@ import org.keycloak.cluster.ExecutionResult;
import org.keycloak.common.util.Time;
import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
/**
@@ -43,11 +48,14 @@ public class InfinispanClusterProvider implements ClusterProvider {
private final CrossDCAwareCacheFactory crossDCAwareCacheFactory;
private final InfinispanNotificationsManager notificationsManager; // Just to extract notifications related stuff to separate class
- public InfinispanClusterProvider(int clusterStartupTime, String myAddress, CrossDCAwareCacheFactory crossDCAwareCacheFactory, InfinispanNotificationsManager notificationsManager) {
+ private final ExecutorService localExecutor;
+
+ public InfinispanClusterProvider(int clusterStartupTime, String myAddress, CrossDCAwareCacheFactory crossDCAwareCacheFactory, InfinispanNotificationsManager notificationsManager, ExecutorService localExecutor) {
this.myAddress = myAddress;
this.clusterStartupTime = clusterStartupTime;
this.crossDCAwareCacheFactory = crossDCAwareCacheFactory;
this.notificationsManager = notificationsManager;
+ this.localExecutor = localExecutor;
}
@@ -86,17 +94,44 @@ public class InfinispanClusterProvider implements ClusterProvider {
@Override
+ public Future<Boolean> executeIfNotExecutedAsync(String taskKey, int taskTimeoutInSeconds, Callable task) {
+ TaskCallback newCallback = new TaskCallback();
+ TaskCallback callback = this.notificationsManager.registerTaskCallback(TASK_KEY_PREFIX + taskKey, newCallback);
+
+ // We successfully submitted our task
+ if (newCallback == callback) {
+ Callable<Boolean> wrappedTask = () -> {
+ boolean executed = executeIfNotExecuted(taskKey, taskTimeoutInSeconds, task).isExecuted();
+
+ if (!executed) {
+ logger.infof("Task already in progress on other cluster node. Will wait until it's finished");
+ }
+
+ callback.getTaskCompletedLatch().await(taskTimeoutInSeconds, TimeUnit.SECONDS);
+ return callback.isSuccess();
+ };
+
+ Future<Boolean> future = localExecutor.submit(wrappedTask);
+ callback.setFuture(future);
+ } else {
+ logger.infof("Task already in progress on this cluster node. Will wait until it's finished");
+ }
+
+ return callback.getFuture();
+ }
+
+
+ @Override
public void registerListener(String taskKey, ClusterListener task) {
this.notificationsManager.registerListener(taskKey, task);
}
@Override
- public void notify(String taskKey, ClusterEvent event, boolean ignoreSender) {
- this.notificationsManager.notify(taskKey, event, ignoreSender);
+ public void notify(String taskKey, ClusterEvent event, boolean ignoreSender, DCNotify dcNotify) {
+ this.notificationsManager.notify(taskKey, event, ignoreSender, dcNotify);
}
-
private LockEntry createLockEntry() {
LockEntry lock = new LockEntry();
lock.setNode(myAddress);
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java
index a96621d..330de4f 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java
@@ -35,12 +35,15 @@ import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -62,17 +65,18 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
// Ensure that atomic operations (like putIfAbsent) must work correctly in any of: non-clustered, clustered or cross-Data-Center (cross-DC) setups
private CrossDCAwareCacheFactory crossDCAwareCacheFactory;
- private String myAddress;
-
private int clusterStartupTime;
// Just to extract notifications related stuff to separate class
private InfinispanNotificationsManager notificationsManager;
+ private ExecutorService localExecutor = Executors.newCachedThreadPool();
+
@Override
public ClusterProvider create(KeycloakSession session) {
lazyInit(session);
- return new InfinispanClusterProvider(clusterStartupTime, myAddress, crossDCAwareCacheFactory, notificationsManager);
+ String myAddress = InfinispanUtil.getMyAddress(session);
+ return new InfinispanClusterProvider(clusterStartupTime, myAddress, crossDCAwareCacheFactory, notificationsManager, localExecutor);
}
private void lazyInit(KeycloakSession session) {
@@ -83,33 +87,23 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
workCache = ispnConnections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
workCache.getCacheManager().addListener(new ViewChangeListener());
- initMyAddress();
- Set<RemoteStore> remoteStores = getRemoteStores();
+ // See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario
+ Set<RemoteStore> remoteStores = InfinispanUtil.getRemoteStores(workCache);
crossDCAwareCacheFactory = CrossDCAwareCacheFactory.getFactory(workCache, remoteStores);
clusterStartupTime = initClusterStartupTime(session);
- notificationsManager = InfinispanNotificationsManager.create(workCache, myAddress, remoteStores);
+ String myAddress = InfinispanUtil.getMyAddress(session);
+ String mySite = InfinispanUtil.getMySite(session);
+
+ notificationsManager = InfinispanNotificationsManager.create(workCache, myAddress, mySite, remoteStores);
}
}
}
}
- // See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario
- private Set<RemoteStore> getRemoteStores() {
- return workCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class);
- }
-
-
- protected void initMyAddress() {
- Transport transport = workCache.getCacheManager().getTransport();
- this.myAddress = transport == null ? HostUtils.getHostName() + "-" + workCache.hashCode() : transport.getAddress().toString();
- logger.debugf("My address: %s", this.myAddress);
- }
-
-
protected int initClusterStartupTime(KeycloakSession session) {
Integer existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY);
if (existingClusterStartTime != null) {
@@ -201,6 +195,10 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
if (logger.isTraceEnabled()) {
logger.tracef("Removing task %s due it's node left cluster", rem);
}
+
+ // If we have task in progress, it needs to be notified
+ notificationsManager.taskFinished(rem, false);
+
cache.remove(rem);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
index fa73420..0c5e6e9 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
@@ -20,32 +20,35 @@ package org.keycloak.cluster.infinispan;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved;
import org.infinispan.client.hotrod.annotation.ClientListener;
import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
-import org.infinispan.client.hotrod.event.ClientEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent;
import org.infinispan.context.Flag;
-import org.infinispan.marshall.core.MarshalledEntry;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
-import org.infinispan.persistence.manager.PersistenceManager;
+import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
import org.infinispan.persistence.remote.RemoteStore;
-import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
-import org.keycloak.common.util.HostUtils;
-import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.common.util.ConcurrentMultivaluedHashMap;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
/**
* Impl for sending infinispan messages across cluster and listening to them
@@ -56,37 +59,52 @@ public class InfinispanNotificationsManager {
protected static final Logger logger = Logger.getLogger(InfinispanNotificationsManager.class);
- private final MultivaluedHashMap<String, ClusterListener> listeners = new MultivaluedHashMap<>();
+ private final ConcurrentMultivaluedHashMap<String, ClusterListener> listeners = new ConcurrentMultivaluedHashMap<>();
+
+ private final ConcurrentMap<String, TaskCallback> taskCallbacks = new ConcurrentHashMap<>();
private final Cache<String, Serializable> workCache;
+ private final RemoteCache workRemoteCache;
+
private final String myAddress;
+ private final String mySite;
+
- protected InfinispanNotificationsManager(Cache<String, Serializable> workCache, String myAddress) {
+ protected InfinispanNotificationsManager(Cache<String, Serializable> workCache, RemoteCache workRemoteCache, String myAddress, String mySite) {
this.workCache = workCache;
+ this.workRemoteCache = workRemoteCache;
this.myAddress = myAddress;
+ this.mySite = mySite;
}
// Create and init manager including all listeners etc
- public static InfinispanNotificationsManager create(Cache<String, Serializable> workCache, String myAddress, Set<RemoteStore> remoteStores) {
- InfinispanNotificationsManager manager = new InfinispanNotificationsManager(workCache, myAddress);
+ public static InfinispanNotificationsManager create(Cache<String, Serializable> workCache, String myAddress, String mySite, Set<RemoteStore> remoteStores) {
+ RemoteCache workRemoteCache = null;
- // We need CacheEntryListener just if we don't have remoteStore. With remoteStore will be all cluster nodes notified anyway from HotRod listener
- if (remoteStores.isEmpty()) {
- workCache.addListener(manager.new CacheEntryListener());
-
- logger.debugf("Added listener for infinispan cache: %s", workCache.getName());
- } else {
- for (RemoteStore remoteStore : remoteStores) {
- RemoteCache<Object, Object> remoteCache = remoteStore.getRemoteCache();
- remoteCache.addClientListener(manager.new HotRodListener(remoteCache));
+ if (!remoteStores.isEmpty()) {
+ RemoteStore remoteStore = remoteStores.iterator().next();
+ workRemoteCache = remoteStore.getRemoteCache();
- logger.debugf("Added listener for HotRod remoteStore cache: %s", remoteCache.getName());
+ if (mySite == null) {
+ throw new IllegalStateException("Multiple datacenters available, but site name is not configured! Check your configuration");
}
}
+ InfinispanNotificationsManager manager = new InfinispanNotificationsManager(workCache, workRemoteCache, myAddress, mySite);
+
+ // We need CacheEntryListener for communication within current DC
+ workCache.addListener(manager.new CacheEntryListener());
+ logger.debugf("Added listener for infinispan cache: %s", workCache.getName());
+
+ // Added listener for remoteCache to notify other DCs
+ if (workRemoteCache != null) {
+ workRemoteCache.addClientListener(manager.new HotRodListener(workRemoteCache));
+ logger.debugf("Added listener for HotRod remoteStore cache: %s", workRemoteCache.getName());
+ }
+
return manager;
}
@@ -96,19 +114,41 @@ public class InfinispanNotificationsManager {
}
- void notify(String taskKey, ClusterEvent event, boolean ignoreSender) {
+ TaskCallback registerTaskCallback(String taskKey, TaskCallback callback) {
+ TaskCallback existing = taskCallbacks.putIfAbsent(taskKey, callback);
+
+ if (existing != null) {
+ return existing;
+ } else {
+ return callback;
+ }
+ }
+
+
+ void notify(String taskKey, ClusterEvent event, boolean ignoreSender, ClusterProvider.DCNotify dcNotify) {
WrapperClusterEvent wrappedEvent = new WrapperClusterEvent();
+ wrappedEvent.setEventKey(taskKey);
wrappedEvent.setDelegateEvent(event);
wrappedEvent.setIgnoreSender(ignoreSender);
+ wrappedEvent.setIgnoreSenderSite(dcNotify == ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC);
wrappedEvent.setSender(myAddress);
+ wrappedEvent.setSenderSite(mySite);
+
+ String eventKey = UUID.randomUUID().toString();
if (logger.isTraceEnabled()) {
- logger.tracef("Sending event %s: %s", taskKey, event);
+ logger.tracef("Sending event with key %s: %s", eventKey, event);
}
- // Put the value to the cache to notify listeners on all the nodes
- workCache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
- .put(taskKey, wrappedEvent, 120, TimeUnit.SECONDS);
+ if (dcNotify == ClusterProvider.DCNotify.LOCAL_DC_ONLY || workRemoteCache == null) {
+ // Just put it to workCache, but skip notifying remoteCache
+ workCache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES, Flag.SKIP_CACHE_STORE)
+ .put(eventKey, wrappedEvent, 120, TimeUnit.SECONDS);
+ } else {
+ // Add directly to remoteCache. Will notify remote listeners on all nodes in all DCs
+ RemoteCache remoteCache = InfinispanUtil.getRemoteCache(workCache);
+ remoteCache.put(eventKey, wrappedEvent, 120, TimeUnit.SECONDS);
+ }
}
@@ -124,6 +164,12 @@ public class InfinispanNotificationsManager {
public void cacheEntryModified(CacheEntryModifiedEvent<String, Serializable> event) {
eventReceived(event.getKey(), event.getValue());
}
+
+ @CacheEntryRemoved
+ public void cacheEntryRemoved(CacheEntryRemovedEvent<String, Serializable> event) {
+ taskFinished(event.getKey(), true);
+ }
+
}
@@ -150,6 +196,14 @@ public class InfinispanNotificationsManager {
hotrodEventReceived(key);
}
+
+ @ClientCacheEntryRemoved
+ public void removed(ClientCacheEntryRemovedEvent event) {
+ String key = event.getKey().toString();
+ taskFinished(key, true);
+ }
+
+
private void hotrodEventReceived(String key) {
// TODO: Look at CacheEventConverter stuff to possibly include value in the event and avoid additional remoteCache request
Object value = workCache.get(key);
@@ -160,6 +214,9 @@ public class InfinispanNotificationsManager {
private void eventReceived(String key, Serializable obj) {
if (!(obj instanceof WrapperClusterEvent)) {
+ if (obj == null) {
+ logger.warnf("Event object wasn't available in remote cache after event was received. Event key: %s", key);
+ }
return;
}
@@ -171,24 +228,39 @@ public class InfinispanNotificationsManager {
}
}
+ if (event.isIgnoreSenderSite()) {
+ if (this.mySite == null || this.mySite.equals(event.getSenderSite())) {
+ return;
+ }
+ }
+
+ String eventKey = event.getEventKey();
+
if (logger.isTraceEnabled()) {
- logger.tracef("Received event %s: %s", key, event);
+ logger.tracef("Received event: %s", event);
}
ClusterEvent wrappedEvent = event.getDelegateEvent();
- List<ClusterListener> myListeners = listeners.get(key);
+ List<ClusterListener> myListeners = listeners.get(eventKey);
if (myListeners != null) {
for (ClusterListener listener : myListeners) {
listener.eventReceived(wrappedEvent);
}
}
+ }
- myListeners = listeners.get(ClusterProvider.ALL);
- if (myListeners != null) {
- for (ClusterListener listener : myListeners) {
- listener.eventReceived(wrappedEvent);
+
+ void taskFinished(String taskKey, boolean success) {
+ TaskCallback callback = taskCallbacks.remove(taskKey);
+
+ if (callback != null) {
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Finished task '%s' with '%b'", taskKey, success);
}
+ callback.setSuccess(success);
+ callback.getTaskCompletedLatch().countDown();
}
+
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/TaskCallback.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/TaskCallback.java
new file mode 100644
index 0000000..028d743
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/TaskCallback.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.cluster.infinispan;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.jboss.logging.Logger;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+class TaskCallback {
+
+ protected static final Logger logger = Logger.getLogger(TaskCallback.class);
+
+ static final int LATCH_TIMEOUT_MS = 10000;
+
+ private volatile boolean success;
+
+ private volatile Future<Boolean> future;
+
+ private final CountDownLatch taskCompletedLatch = new CountDownLatch(1);
+ private final CountDownLatch futureAvailableLatch = new CountDownLatch(1);
+
+
+ public void setSuccess(boolean success) {
+ this.success = success;
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public void setFuture(Future<Boolean> future) {
+ this.future = future;
+ this.futureAvailableLatch.countDown();
+ }
+
+
+ public Future<Boolean> getFuture() {
+ try {
+ this.futureAvailableLatch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ie) {
+ logger.error("Interrupted thread!");
+ Thread.currentThread().interrupt();
+ }
+
+ return future;
+ }
+
+
+ public CountDownLatch getTaskCompletedLatch() {
+ return taskCompletedLatch;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java
index b03dd70..0e58275 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java
@@ -24,10 +24,21 @@ import org.keycloak.cluster.ClusterEvent;
*/
public class WrapperClusterEvent implements ClusterEvent {
- private String sender; // will be null in non-clustered environment
+ private String eventKey;
+ private String sender;
+ private String senderSite;
private boolean ignoreSender;
+ private boolean ignoreSenderSite;
private ClusterEvent delegateEvent;
+ public String getEventKey() {
+ return eventKey;
+ }
+
+ public void setEventKey(String eventKey) {
+ this.eventKey = eventKey;
+ }
+
public String getSender() {
return sender;
}
@@ -36,6 +47,14 @@ public class WrapperClusterEvent implements ClusterEvent {
this.sender = sender;
}
+ public String getSenderSite() {
+ return senderSite;
+ }
+
+ public void setSenderSite(String senderSite) {
+ this.senderSite = senderSite;
+ }
+
public boolean isIgnoreSender() {
return ignoreSender;
}
@@ -44,6 +63,14 @@ public class WrapperClusterEvent implements ClusterEvent {
this.ignoreSender = ignoreSender;
}
+ public boolean isIgnoreSenderSite() {
+ return ignoreSenderSite;
+ }
+
+ public void setIgnoreSenderSite(boolean ignoreSenderSite) {
+ this.ignoreSenderSite = ignoreSenderSite;
+ }
+
public ClusterEvent getDelegateEvent() {
return delegateEvent;
}
@@ -54,6 +81,6 @@ public class WrapperClusterEvent implements ClusterEvent {
@Override
public String toString() {
- return String.format("WrapperClusterEvent [ sender=%s, delegateEvent=%s ]", sender, delegateEvent.toString());
+ return String.format("WrapperClusterEvent [ eventKey=%s, sender=%s, senderSite=%s, delegateEvent=%s ]", eventKey, sender, senderSite, delegateEvent.toString());
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java
index 71a2eba..d95e4a4 100644
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java
@@ -26,9 +26,13 @@ import org.infinispan.manager.EmbeddedCacheManager;
public class DefaultInfinispanConnectionProvider implements InfinispanConnectionProvider {
private EmbeddedCacheManager cacheManager;
+ private final String siteName;
+ private final String nodeName;
- public DefaultInfinispanConnectionProvider(EmbeddedCacheManager cacheManager) {
+ public DefaultInfinispanConnectionProvider(EmbeddedCacheManager cacheManager, String nodeName, String siteName) {
this.cacheManager = cacheManager;
+ this.nodeName = nodeName;
+ this.siteName = siteName;
}
@Override
@@ -37,6 +41,16 @@ public class DefaultInfinispanConnectionProvider implements InfinispanConnection
}
@Override
+ public String getNodeName() {
+ return nodeName;
+ }
+
+ @Override
+ public String getSiteName() {
+ return siteName;
+ }
+
+ @Override
public void close() {
}
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
index a9df047..86f6074 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
@@ -17,6 +17,7 @@
package org.keycloak.connections.infinispan;
+import java.security.SecureRandom;
import java.util.concurrent.TimeUnit;
import org.infinispan.commons.util.FileLookup;
@@ -30,6 +31,7 @@ import org.infinispan.eviction.EvictionType;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
+import org.infinispan.remoting.transport.Transport;
import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.infinispan.transaction.LockingMode;
import org.infinispan.transaction.TransactionMode;
@@ -38,8 +40,12 @@ import org.jboss.logging.Logger;
import org.jgroups.JChannel;
import org.keycloak.Config;
import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory;
+import org.keycloak.common.util.HostUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.sessions.infinispan.remotestore.KcRemoteStoreConfigurationBuilder;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+import org.keycloak.models.utils.KeycloakModelUtils;
import javax.naming.InitialContext;
@@ -56,11 +62,15 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
protected boolean containerManaged;
+ private String nodeName;
+
+ private String siteName;
+
@Override
public InfinispanConnectionProvider create(KeycloakSession session) {
lazyInit();
- return new DefaultInfinispanConnectionProvider(cacheManager);
+ return new DefaultInfinispanConnectionProvider(cacheManager, nodeName, siteName);
}
@Override
@@ -96,6 +106,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
} else {
initEmbedded();
}
+
+ logger.infof("Node name: %s, Site name: %s", nodeName, siteName);
}
}
}
@@ -134,7 +146,20 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, getRevisionCacheConfig(authzRevisionsMaxEntries));
cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, true);
-
+ Transport transport = cacheManager.getTransport();
+ if (transport != null) {
+ this.nodeName = transport.getAddress().toString();
+ this.siteName = cacheManager.getCacheManagerConfiguration().transport().siteId();
+ if (this.siteName == null) {
+ this.siteName = System.getProperty(InfinispanConnectionProvider.JBOSS_SITE_NAME);
+ }
+ } else {
+ this.nodeName = System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME);
+ this.siteName = System.getProperty(InfinispanConnectionProvider.JBOSS_SITE_NAME);
+ }
+ if (this.nodeName == null || this.nodeName.equals("localhost")) {
+ this.nodeName = generateNodeName();
+ }
logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup);
} catch (Exception e) {
@@ -152,13 +177,27 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
boolean async = config.getBoolean("async", false);
boolean allowDuplicateJMXDomains = config.getBoolean("allowDuplicateJMXDomains", true);
+ this.nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
+ if (this.nodeName != null && this.nodeName.isEmpty()) {
+ this.nodeName = null;
+ }
+
+ this.siteName = config.get("siteName", System.getProperty(InfinispanConnectionProvider.JBOSS_SITE_NAME));
+ if (this.siteName != null && this.siteName.isEmpty()) {
+ this.siteName = null;
+ }
+
if (clustered) {
- String nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR));
- configureTransport(gcb, nodeName, jgroupsUdpMcastAddr);
+ configureTransport(gcb, nodeName, siteName, jgroupsUdpMcastAddr);
gcb.globalJmxStatistics()
.jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName);
+ } else {
+ if (nodeName == null) {
+ nodeName = generateNodeName();
+ }
}
+
gcb.globalJmxStatistics()
.allowDuplicateDomains(allowDuplicateJMXDomains)
.enable();
@@ -166,6 +205,10 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager = new DefaultCacheManager(gcb.build());
containerManaged = false;
+ if (cacheManager.getTransport() != null) {
+ nodeName = cacheManager.getTransport().getAddress().toString();
+ }
+
logger.debug("Started embedded Infinispan cache container");
ConfigurationBuilder modelCacheConfigBuilder = new ConfigurationBuilder();
@@ -198,11 +241,29 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
.build();
}
+ // Base configuration doesn't contain any remote stores
+ Configuration sessionCacheConfigurationBase = sessionConfigBuilder.build();
+
+ boolean jdgEnabled = config.getBoolean("remoteStoreEnabled", false);
+
+ if (jdgEnabled) {
+ sessionConfigBuilder = new ConfigurationBuilder();
+ sessionConfigBuilder.read(sessionCacheConfigurationBase);
+ configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.SESSION_CACHE_NAME, KcRemoteStoreConfigurationBuilder.class);
+ }
Configuration sessionCacheConfiguration = sessionConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCacheConfiguration);
+
+ if (jdgEnabled) {
+ sessionConfigBuilder = new ConfigurationBuilder();
+ sessionConfigBuilder.read(sessionCacheConfigurationBase);
+ configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, KcRemoteStoreConfigurationBuilder.class);
+ }
+ sessionCacheConfiguration = sessionConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration);
- cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfiguration);
- cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, sessionCacheConfiguration);
+
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfigurationBase);
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, sessionCacheConfigurationBase);
// Retrieve caches to enforce rebalance
cacheManager.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME, true);
@@ -215,9 +276,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
replicationConfigBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC);
}
- boolean jdgEnabled = config.getBoolean("remoteStoreEnabled", false);
if (jdgEnabled) {
- configureRemoteCacheStore(replicationConfigBuilder, async);
+ configureRemoteCacheStore(replicationConfigBuilder, async, InfinispanConnectionProvider.WORK_CACHE_NAME, RemoteStoreConfigurationBuilder.class);
}
Configuration replicationEvictionCacheConfiguration = replicationConfigBuilder.build();
@@ -267,6 +327,10 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, true);
}
+ protected String generateNodeName() {
+ return InfinispanConnectionProvider.NODE_PREFIX + new SecureRandom().nextInt(1000000);
+ }
+
private Configuration getRevisionCacheConfig(long maxEntries) {
ConfigurationBuilder cb = new ConfigurationBuilder();
cb.invocationBatching().enable().transaction().transactionMode(TransactionMode.TRANSACTIONAL);
@@ -281,19 +345,19 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
}
// Used for cross-data centers scenario. Usually integration with external JDG server, which itself handles communication between DCs.
- private void configureRemoteCacheStore(ConfigurationBuilder builder, boolean async) {
+ private void configureRemoteCacheStore(ConfigurationBuilder builder, boolean async, String cacheName, Class<? extends RemoteStoreConfigurationBuilder> configBuilderClass) {
String jdgServer = config.get("remoteStoreServer", "localhost");
Integer jdgPort = config.getInt("remoteStorePort", 11222);
builder.persistence()
.passivation(false)
- .addStore(RemoteStoreConfigurationBuilder.class)
+ .addStore(configBuilderClass)
.fetchPersistentState(false)
.ignoreModifications(false)
.purgeOnStartup(false)
.preload(false)
.shared(true)
- .remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME)
+ .remoteCacheName(cacheName)
.rawValues(true)
.forceReturnValues(false)
.marshaller(KeycloakHotRodMarshallerFactory.class.getName())
@@ -355,7 +419,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
private static final Object CHANNEL_INIT_SYNCHRONIZER = new Object();
- protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName, String jgroupsUdpMcastAddr) {
+ protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName, String siteName, String jgroupsUdpMcastAddr) {
if (nodeName == null) {
gcb.transport().defaultTransport();
} else {
@@ -376,6 +440,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
gcb.transport()
.nodeName(nodeName)
+ .siteId(siteName)
.transport(transport)
.globalJmxStatistics()
.jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName)
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
index e8cdbf6..9c3d437 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
@@ -55,8 +55,25 @@ public interface InfinispanConnectionProvider extends Provider {
String JBOSS_NODE_NAME = "jboss.node.name";
String JGROUPS_UDP_MCAST_ADDR = "jgroups.udp.mcast_addr";
+ // TODO This property is not in Wildfly. Check if corresponding property in Wildfly exists
+ String JBOSS_SITE_NAME = "jboss.site.name";
+
String JMX_DOMAIN = "jboss.datagrid-infinispan";
+ // Constant used as the prefix of the current node if "jboss.node.name" is not configured
+ String NODE_PREFIX = "node_";
+
<K, V> Cache<K, V> getCache(String name);
+ /**
+ * @return Address of current node in cluster. In non-cluster environment, it returns some other non-null value (eg. hostname with some random value like "host-123456" )
+ */
+ String getNodeName();
+
+ /**
+ *
+ * @return siteName or null if we're not in environment with multiple sites (data centers)
+ */
+ String getSiteName();
+
}
diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java
index b5f48cd..52a509e 100644
--- a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java
@@ -69,7 +69,7 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
public void clearCache() {
keys.clear();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.notify(InfinispanPublicKeyStorageProviderFactory.KEYS_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
+ cluster.notify(InfinispanPublicKeyStorageProviderFactory.KEYS_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true, ClusterProvider.DCNotify.ALL_DCS);
}
@@ -122,7 +122,7 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
for (String cacheKey : invalidations) {
keys.remove(cacheKey);
- cluster.notify(cacheKey, PublicKeyStorageInvalidationEvent.create(cacheKey), true);
+ cluster.notify(InfinispanPublicKeyStorageProviderFactory.PUBLIC_KEY_STORAGE_INVALIDATION_EVENT, PublicKeyStorageInvalidationEvent.create(cacheKey), true, ClusterProvider.DCNotify.ALL_DCS);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java
index 42a73fc..e8872a7 100644
--- a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java
@@ -50,6 +50,8 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
public static final String KEYS_CLEAR_CACHE_EVENTS = "KEYS_CLEAR_CACHE_EVENTS";
+ public static final String PUBLIC_KEY_STORAGE_INVALIDATION_EVENT = "PUBLIC_KEY_STORAGE_INVALIDATION_EVENT";
+
private volatile Cache<String, PublicKeysEntry> keysCache;
private final Map<String, FutureTask<PublicKeysEntry>> tasksInProgress = new ConcurrentHashMap<>();
@@ -69,12 +71,10 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
this.keysCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
+ cluster.registerListener(PUBLIC_KEY_STORAGE_INVALIDATION_EVENT, (ClusterEvent event) -> {
- if (event instanceof PublicKeyStorageInvalidationEvent) {
- PublicKeyStorageInvalidationEvent invalidationEvent = (PublicKeyStorageInvalidationEvent) event;
- keysCache.remove(invalidationEvent.getCacheKey());
- }
+ PublicKeyStorageInvalidationEvent invalidationEvent = (PublicKeyStorageInvalidationEvent) event;
+ keysCache.remove(invalidationEvent.getCacheKey());
});
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java
index c74e4fe..8a9dd05 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java
@@ -41,6 +41,7 @@ public class InfinispanCacheStoreFactoryProviderFactory implements CachedStorePr
private static final Logger log = Logger.getLogger(InfinispanCacheStoreFactoryProviderFactory.class);
public static final String AUTHORIZATION_CLEAR_CACHE_EVENTS = "AUTHORIZATION_CLEAR_CACHE_EVENTS";
+ public static final String AUTHORIZATION_INVALIDATION_EVENTS = "AUTHORIZATION_INVALIDATION_EVENTS";
protected volatile StoreFactoryCacheManager storeCache;
@@ -59,11 +60,11 @@ public class InfinispanCacheStoreFactoryProviderFactory implements CachedStorePr
storeCache = new StoreFactoryCacheManager(cache, revisions);
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
- if (event instanceof InvalidationEvent) {
- InvalidationEvent invalidationEvent = (InvalidationEvent) event;
- storeCache.invalidationEventReceived(invalidationEvent);
- }
+ cluster.registerListener(AUTHORIZATION_INVALIDATION_EVENTS, (ClusterEvent event) -> {
+
+ InvalidationEvent invalidationEvent = (InvalidationEvent) event;
+ storeCache.invalidationEventReceived(invalidationEvent);
+
});
cluster.registerListener(AUTHORIZATION_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> storeCache.clear());
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
index 10be78d..a169235 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
@@ -216,7 +216,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
cache.invalidateObject(id);
}
- cache.sendInvalidationEvents(session, invalidationEvents);
+ cache.sendInvalidationEvents(session, invalidationEvents, InfinispanCacheStoreFactoryProviderFactory.AUTHORIZATION_INVALIDATION_EVENTS);
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
index 4480f7a..9a0839f 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
@@ -198,22 +198,15 @@ public abstract class CacheManager {
}
- public void sendInvalidationEvents(KeycloakSession session, Collection<InvalidationEvent> invalidationEvents) {
+ public void sendInvalidationEvents(KeycloakSession session, Collection<InvalidationEvent> invalidationEvents, String eventKey) {
ClusterProvider clusterProvider = session.getProvider(ClusterProvider.class);
// Maybe add InvalidationEvent, which will be collection of all invalidationEvents? That will reduce cluster traffic even more.
for (InvalidationEvent event : invalidationEvents) {
- clusterProvider.notify(generateEventId(event), event, true);
+ clusterProvider.notify(eventKey, event, true, ClusterProvider.DCNotify.ALL_DCS);
}
}
- protected String generateEventId(InvalidationEvent event) {
- return new StringBuilder(event.getId())
- .append("_")
- .append(event.hashCode())
- .toString();
- }
-
public void invalidationEventReceived(InvalidationEvent event) {
Set<String> invalidations = new HashSet<>();
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
index 3668d97..160fee5 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
@@ -117,6 +117,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected AuthenticationFlowModel directGrantFlow;
protected AuthenticationFlowModel resetCredentialsFlow;
protected AuthenticationFlowModel clientAuthenticationFlow;
+ protected AuthenticationFlowModel dockerAuthenticationFlow;
protected boolean eventsEnabled;
protected long eventsExpiration;
@@ -252,6 +253,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
directGrantFlow = model.getDirectGrantFlow();
resetCredentialsFlow = model.getResetCredentialsFlow();
clientAuthenticationFlow = model.getClientAuthenticationFlow();
+ dockerAuthenticationFlow = model.getDockerAuthenticationFlow();
for (ComponentModel component : model.getComponents()) {
componentsByParentAndType.add(component.getParentId() + component.getProviderType(), component);
@@ -547,6 +549,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return clientAuthenticationFlow;
}
+ public AuthenticationFlowModel getDockerAuthenticationFlow() {
+ return dockerAuthenticationFlow;
+ }
+
public List<String> getDefaultGroups() {
return defaultGroups;
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java
index c2ad8ce..ef2ce2b 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java
@@ -38,6 +38,7 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa
private static final Logger log = Logger.getLogger(InfinispanCacheRealmProviderFactory.class);
public static final String REALM_CLEAR_CACHE_EVENTS = "REALM_CLEAR_CACHE_EVENTS";
+ public static final String REALM_INVALIDATION_EVENTS = "REALM_INVALIDATION_EVENTS";
protected volatile RealmCacheManager realmCache;
@@ -56,12 +57,11 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa
realmCache = new RealmCacheManager(cache, revisions);
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
+ cluster.registerListener(REALM_INVALIDATION_EVENTS, (ClusterEvent event) -> {
+
+ InvalidationEvent invalidationEvent = (InvalidationEvent) event;
+ realmCache.invalidationEventReceived(invalidationEvent);
- if (event instanceof InvalidationEvent) {
- InvalidationEvent invalidationEvent = (InvalidationEvent) event;
- realmCache.invalidationEventReceived(invalidationEvent);
- }
});
cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java
index e8c2ba1..4d0f445 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java
@@ -37,6 +37,7 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact
private static final Logger log = Logger.getLogger(InfinispanUserCacheProviderFactory.class);
public static final String USER_CLEAR_CACHE_EVENTS = "USER_CLEAR_CACHE_EVENTS";
+ public static final String USER_INVALIDATION_EVENTS = "USER_INVALIDATION_EVENTS";
protected volatile UserCacheManager userCache;
@@ -58,12 +59,10 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
+ cluster.registerListener(USER_INVALIDATION_EVENTS, (ClusterEvent event) -> {
- if (event instanceof InvalidationEvent) {
- InvalidationEvent invalidationEvent = (InvalidationEvent) event;
- userCache.invalidationEventReceived(invalidationEvent);
- }
+ InvalidationEvent invalidationEvent = (InvalidationEvent) event;
+ userCache.invalidationEventReceived(invalidationEvent);
});
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index 53effda..af7159c 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -1019,6 +1019,18 @@ public class RealmAdapter implements CachedRealmModel {
}
@Override
+ public AuthenticationFlowModel getDockerAuthenticationFlow() {
+ if (isUpdated()) return updated.getDockerAuthenticationFlow();
+ return cached.getDockerAuthenticationFlow();
+ }
+
+ @Override
+ public void setDockerAuthenticationFlow(final AuthenticationFlowModel flow) {
+ getDelegateForUpdate();
+ updated.setDockerAuthenticationFlow(flow);
+ }
+
+ @Override
public List<AuthenticationFlowModel> getAuthenticationFlows() {
if (isUpdated()) return updated.getAuthenticationFlows();
return cached.getAuthenticationFlowList();
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java
index b01dbab..ed5db84 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java
@@ -95,11 +95,9 @@ public class RealmCacheManager extends CacheManager {
@Override
protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
- if (event instanceof RealmCacheInvalidationEvent) {
- invalidations.add(event.getId());
+ invalidations.add(event.getId());
- ((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations);
- }
+ ((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
index f9bf0d7..77f8981 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
@@ -132,7 +132,7 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void clear() {
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), false);
+ cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), false, ClusterProvider.DCNotify.ALL_DCS);
}
@Override
@@ -264,7 +264,7 @@ public class RealmCacheSession implements CacheRealmProvider {
cache.invalidateObject(id);
}
- cache.sendInvalidationEvents(session, invalidationEvents);
+ cache.sendInvalidationEvents(session, invalidationEvents, InfinispanCacheRealmProviderFactory.REALM_INVALIDATION_EVENTS);
}
private KeycloakTransaction getPrepareTransaction() {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java
index e949314..9126b2f 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java
@@ -95,9 +95,7 @@ public class UserCacheManager extends CacheManager {
@Override
protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
- if (event instanceof UserCacheInvalidationEvent) {
- ((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations);
- }
+ ((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations);
}
public void invalidateRealmUsers(String realm, Set<String> invalidations) {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
index ef19b84..0d971f7 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
@@ -90,7 +90,7 @@ public class UserCacheSession implements UserCache {
public void clear() {
cache.clear();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
+ cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true, ClusterProvider.DCNotify.ALL_DCS);
}
public UserProvider getDelegate() {
@@ -129,7 +129,7 @@ public class UserCacheSession implements UserCache {
cache.invalidateObject(invalidation);
}
- cache.sendInvalidationEvents(session, invalidationEvents);
+ cache.sendInvalidationEvents(session, invalidationEvents, InfinispanUserCacheProviderFactory.USER_INVALIDATION_EVENTS);
}
private KeycloakTransaction getTransaction() {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java
index 7772bc2..be086b8 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java
@@ -22,13 +22,17 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Set;
-import org.infinispan.Cache;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
+import org.keycloak.models.sessions.infinispan.changes.UserSessionClientSessionUpdateTask;
+import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
-import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
/**
@@ -39,19 +43,20 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
private final AuthenticatedClientSessionEntity entity;
private final ClientModel client;
private final InfinispanUserSessionProvider provider;
- private final Cache<String, SessionEntity> cache;
+ private final InfinispanChangelogBasedTransaction updateTx;
private UserSessionAdapter userSession;
- public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionAdapter userSession, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache) {
+ public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionAdapter userSession,
+ InfinispanUserSessionProvider provider, InfinispanChangelogBasedTransaction updateTx) {
this.provider = provider;
this.entity = entity;
this.client = client;
- this.cache = cache;
+ this.updateTx = updateTx;
this.userSession = userSession;
}
- private void update() {
- provider.getTx().replace(cache, userSession.getEntity().getId(), userSession.getEntity());
+ private void update(UserSessionUpdateTask task) {
+ updateTx.addTask(userSession.getId(), task);
}
@@ -62,15 +67,27 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
// Dettach userSession
if (userSession == null) {
- if (sessionEntity.getAuthenticatedClientSessions() != null) {
- sessionEntity.getAuthenticatedClientSessions().remove(clientUUID);
- update();
- this.userSession = null;
- }
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity sessionEntity) {
+ sessionEntity.getAuthenticatedClientSessions().remove(clientUUID);
+ }
+
+ };
+ update(task);
+ this.userSession = null;
} else {
this.userSession = (UserSessionAdapter) userSession;
- sessionEntity.getAuthenticatedClientSessions().put(clientUUID, entity);
- update();
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity sessionEntity) {
+ sessionEntity.getAuthenticatedClientSessions().put(clientUUID, entity);
+ }
+
+ };
+ update(task);
}
}
@@ -86,8 +103,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public void setRedirectUri(String uri) {
- entity.setRedirectUri(uri);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.setRedirectUri(uri);
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -112,8 +137,22 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public void setTimestamp(int timestamp) {
- entity.setTimestamp(timestamp);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.setTimestamp(timestamp);
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
+ // We usually update lastSessionRefresh at the same time. That would handle it.
+ return CrossDCMessageStatus.NOT_NEEDED;
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -123,8 +162,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public void setAction(String action) {
- entity.setAction(action);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.setAction(action);
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -134,8 +181,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public void setProtocol(String method) {
- entity.setAuthMethod(method);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.setAuthMethod(method);
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -145,8 +200,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public void setRoles(Set<String> roles) {
- entity.setRoles(roles);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.setRoles(roles); // TODO not thread-safe. But we will remove setRoles anyway...?
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -156,35 +219,54 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public void setProtocolMappers(Set<String> protocolMappers) {
- entity.setProtocolMappers(protocolMappers);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.setProtocolMappers(protocolMappers); // TODO not thread-safe. But we will remove setProtocolMappers anyway...?
+ }
+
+ };
+
+ update(task);
}
@Override
public String getNote(String name) {
- return entity.getNotes()==null ? null : entity.getNotes().get(name);
+ return entity.getNotes().get(name);
}
@Override
public void setNote(String name, String value) {
- if (entity.getNotes() == null) {
- entity.setNotes(new HashMap<>());
- }
- entity.getNotes().put(name, value);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.getNotes().put(name, value);
+ }
+
+ };
+
+ update(task);
}
@Override
public void removeNote(String name) {
- if (entity.getNotes() != null) {
- entity.getNotes().remove(name);
- update();
- }
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.getNotes().remove(name);
+ }
+
+ };
+
+ update(task);
}
@Override
public Map<String, String> getNotes() {
- if (entity.getNotes() == null || entity.getNotes().isEmpty()) return Collections.emptyMap();
+ if (entity.getNotes().isEmpty()) return Collections.emptyMap();
Map<String, String> copy = new HashMap<>();
copy.putAll(entity.getNotes());
return copy;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java
new file mode 100644
index 0000000..e9b3288
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan;
+
+import org.infinispan.AdvancedCache;
+import org.infinispan.Cache;
+import org.infinispan.context.Flag;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class CacheDecorators {
+
+ public static <K, V> AdvancedCache<K, V> localCache(Cache<K, V> cache) {
+ return cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL);
+ }
+
+ public static <K, V> AdvancedCache<K, V> skipCacheLoaders(Cache<K, V> cache) {
+ return cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE);
+ }
+
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java
new file mode 100644
index 0000000..43bb2b3
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.changes;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.infinispan.Cache;
+import org.infinispan.context.Flag;
+import org.jboss.logging.Logger;
+import org.keycloak.models.AbstractKeycloakTransaction;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extends AbstractKeycloakTransaction {
+
+ public static final Logger logger = Logger.getLogger(InfinispanChangelogBasedTransaction.class);
+
+ private final KeycloakSession kcSession;
+ private final String cacheName;
+ private final Cache<String, SessionEntityWrapper<S>> cache;
+ private final RemoteCacheInvoker remoteCacheInvoker;
+
+ private final Map<String, SessionUpdatesList<S>> updates = new HashMap<>();
+
+ public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, String cacheName, Cache<String, SessionEntityWrapper<S>> cache, RemoteCacheInvoker remoteCacheInvoker) {
+ this.kcSession = kcSession;
+ this.cacheName = cacheName;
+ this.cache = cache;
+ this.remoteCacheInvoker = remoteCacheInvoker;
+ }
+
+
+ public void addTask(String key, SessionUpdateTask<S> task) {
+ SessionUpdatesList<S> myUpdates = updates.get(key);
+ if (myUpdates == null) {
+ // Lookup entity from cache
+ SessionEntityWrapper<S> wrappedEntity = cache.get(key);
+ if (wrappedEntity == null) {
+ logger.warnf("Not present cache item for key %s", key);
+ return;
+ }
+
+ RealmModel realm = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealm());
+
+ myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
+ updates.put(key, myUpdates);
+ }
+
+ // Run the update now, so reader in same transaction can see it (TODO: Rollback may not work correctly. See if it's an issue..)
+ task.runUpdate(myUpdates.getEntityWrapper().getEntity());
+ myUpdates.add(task);
+ }
+
+
+ // Create entity and new version for it
+ public void addTask(String key, SessionUpdateTask<S> task, S entity) {
+ if (entity == null) {
+ throw new IllegalArgumentException("Null entity not allowed");
+ }
+
+ RealmModel realm = kcSession.realms().getRealm(entity.getRealm());
+ SessionEntityWrapper<S> wrappedEntity = new SessionEntityWrapper<>(entity);
+ SessionUpdatesList<S> myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
+ updates.put(key, myUpdates);
+
+ // Run the update now, so reader in same transaction can see it
+ task.runUpdate(entity);
+ myUpdates.add(task);
+ }
+
+
+ public void reloadEntityInCurrentTransaction(RealmModel realm, String key, SessionEntityWrapper<S> entity) {
+ if (entity == null) {
+ throw new IllegalArgumentException("Null entity not allowed");
+ }
+
+ SessionEntityWrapper<S> latestEntity = cache.get(key);
+ if (latestEntity == null) {
+ return;
+ }
+
+ SessionUpdatesList<S> newUpdates = new SessionUpdatesList<>(realm, latestEntity);
+
+ SessionUpdatesList<S> existingUpdates = updates.get(key);
+ if (existingUpdates != null) {
+ newUpdates.setUpdateTasks(existingUpdates.getUpdateTasks());
+ }
+
+ updates.put(key, newUpdates);
+ }
+
+
+ public SessionEntityWrapper<S> get(String key) {
+ SessionUpdatesList<S> myUpdates = updates.get(key);
+ if (myUpdates == null) {
+ SessionEntityWrapper<S> wrappedEntity = cache.get(key);
+ if (wrappedEntity == null) {
+ return null;
+ }
+
+ RealmModel realm = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealm());
+
+ myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
+ updates.put(key, myUpdates);
+
+ return wrappedEntity;
+ } else {
+ S entity = myUpdates.getEntityWrapper().getEntity();
+
+ // If entity is scheduled for remove, we don't return it.
+ boolean scheduledForRemove = myUpdates.getUpdateTasks().stream().filter((SessionUpdateTask task) -> {
+
+ return task.getOperation(entity) == SessionUpdateTask.CacheOperation.REMOVE;
+
+ }).findFirst().isPresent();
+
+ return scheduledForRemove ? null : myUpdates.getEntityWrapper();
+ }
+ }
+
+
+ @Override
+ protected void commitImpl() {
+ for (Map.Entry<String, SessionUpdatesList<S>> entry : updates.entrySet()) {
+ SessionUpdatesList<S> sessionUpdates = entry.getValue();
+ SessionEntityWrapper<S> sessionWrapper = sessionUpdates.getEntityWrapper();
+
+ RealmModel realm = sessionUpdates.getRealm();
+
+ MergedUpdate<S> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper);
+
+ if (merged != null) {
+ // Now run the operation in our cluster
+ runOperationInCluster(entry.getKey(), merged, sessionWrapper);
+
+ // Check if we need to send message to second DC
+ remoteCacheInvoker.runTask(kcSession, realm, cacheName, entry.getKey(), merged, sessionWrapper);
+ }
+ }
+ }
+
+
+ private void runOperationInCluster(String key, MergedUpdate<S> task, SessionEntityWrapper<S> sessionWrapper) {
+ S session = sessionWrapper.getEntity();
+ SessionUpdateTask.CacheOperation operation = task.getOperation(session);
+
+ // Don't need to run update of underlying entity. Local updates were already run
+ //task.runUpdate(session);
+
+ switch (operation) {
+ case REMOVE:
+ // Just remove it
+ cache
+ .getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
+ .remove(key);
+ break;
+ case ADD:
+ cache
+ .getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
+ .put(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS);
+ break;
+ case ADD_IF_ABSENT:
+ SessionEntityWrapper existing = cache.putIfAbsent(key, sessionWrapper);
+ if (existing != null) {
+ throw new IllegalStateException("There is already existing value in cache for key " + key);
+ }
+ break;
+ case REPLACE:
+ replace(key, task, sessionWrapper);
+ break;
+ default:
+ throw new IllegalStateException("Unsupported state " + operation);
+ }
+
+ }
+
+
+ private void replace(String key, MergedUpdate<S> task, SessionEntityWrapper<S> oldVersionEntity) {
+ boolean replaced = false;
+ S session = oldVersionEntity.getEntity();
+
+ while (!replaced) {
+ SessionEntityWrapper<S> newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata());
+
+ // Atomic cluster-aware replace
+ replaced = cache.replace(key, oldVersionEntity, newVersionEntity);
+
+ // Replace fail. Need to load latest entity from cache, apply updates again and try to replace in cache again
+ if (!replaced) {
+ logger.debugf("Replace failed for entity: %s . Will try again", key);
+
+ oldVersionEntity = cache.get(key);
+
+ if (oldVersionEntity == null) {
+ logger.debugf("Entity %s not found. Maybe removed in the meantime. Replace task will be ignored", key);
+ return;
+ }
+
+ session = oldVersionEntity.getEntity();
+
+ task.runUpdate(session);
+ } else {
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Replace SUCCESS for entity: %s . old version: %d, new version: %d", key, oldVersionEntity.getVersion(), newVersionEntity.getVersion());
+ }
+ }
+ }
+
+ }
+
+
+ @Override
+ protected void rollbackImpl() {
+ }
+
+ private SessionEntityWrapper<S> generateNewVersionAndWrapEntity(S entity, Map<String, String> localMetadata) {
+ return new SessionEntityWrapper<>(localMetadata, entity);
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java
new file mode 100644
index 0000000..1f24f84
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.changes;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
+
+ private List<SessionUpdateTask<S>> childUpdates = new LinkedList<>();
+ private CacheOperation operation;
+ private CrossDCMessageStatus crossDCMessageStatus;
+
+
+ public MergedUpdate(CacheOperation operation, CrossDCMessageStatus crossDCMessageStatus) {
+ this.operation = operation;
+ this.crossDCMessageStatus = crossDCMessageStatus;
+ }
+
+ @Override
+ public void runUpdate(S session) {
+ for (SessionUpdateTask<S> child : childUpdates) {
+ child.runUpdate(session);
+ }
+ }
+
+ @Override
+ public CacheOperation getOperation(S session) {
+ return operation;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<S> sessionWrapper) {
+ return crossDCMessageStatus;
+ }
+
+
+ public static <S extends SessionEntity> MergedUpdate<S> computeUpdate(List<SessionUpdateTask<S>> childUpdates, SessionEntityWrapper<S> sessionWrapper) {
+ if (childUpdates == null || childUpdates.isEmpty()) {
+ return null;
+ }
+
+ MergedUpdate<S> result = null;
+ S session = sessionWrapper.getEntity();
+ for (SessionUpdateTask<S> child : childUpdates) {
+ if (result == null) {
+ result = new MergedUpdate<>(child.getOperation(session), child.getCrossDCMessageStatus(sessionWrapper));
+ result.childUpdates.add(child);
+ } else {
+
+ // Merge the operations. REMOVE is special case as other operations are not needed then.
+ CacheOperation mergedOp = result.getOperation(session).merge(child.getOperation(session), session);
+ if (mergedOp == CacheOperation.REMOVE) {
+ result = new MergedUpdate<>(child.getOperation(session), child.getCrossDCMessageStatus(sessionWrapper));
+ result.childUpdates.add(child);
+ return result;
+ }
+
+ result.operation = mergedOp;
+
+ // Check if we need to send message to other DCs and how critical it is
+ CrossDCMessageStatus currentDCStatus = result.getCrossDCMessageStatus(sessionWrapper);
+
+ // Optimization. If we already have SYNC, we don't need to retrieve childDCStatus
+ if (currentDCStatus != CrossDCMessageStatus.SYNC) {
+ CrossDCMessageStatus childDCStatus = child.getCrossDCMessageStatus(sessionWrapper);
+ result.crossDCMessageStatus = currentDCStatus.merge(childDCStatus);
+ }
+
+ // Finally add another update to the result
+ result.childUpdates.add(child);
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "MergedUpdate" + childUpdates;
+ }
+
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java
new file mode 100644
index 0000000..ca21487
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.changes;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.infinispan.commons.marshall.Externalizer;
+import org.infinispan.commons.marshall.MarshallUtil;
+import org.infinispan.commons.marshall.SerializeWith;
+import org.keycloak.models.sessions.infinispan.changes.sessions.SessionData;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@SerializeWith(SessionEntityWrapper.ExternalizerImpl.class)
+public class SessionEntityWrapper<S extends SessionEntity> {
+
+ private UUID version;
+ private final S entity;
+ private final Map<String, String> localMetadata;
+
+
+ protected SessionEntityWrapper(UUID version, Map<String, String> localMetadata, S entity) {
+ if (version == null) {
+ throw new IllegalArgumentException("Version UUID can't be null");
+ }
+
+ this.version = version;
+ this.localMetadata = localMetadata;
+ this.entity = entity;
+ }
+
+ public SessionEntityWrapper(Map<String, String> localMetadata, S entity) {
+ this(UUID.randomUUID(),localMetadata, entity);
+ }
+
+ public SessionEntityWrapper(S entity) {
+ this(new ConcurrentHashMap<>(), entity);
+ }
+
+
+ public UUID getVersion() {
+ return version;
+ }
+
+ public void setVersion(UUID version) {
+ this.version = version;
+ }
+
+
+ public S getEntity() {
+ return entity;
+ }
+
+ public String getLocalMetadataNote(String key) {
+ return localMetadata.get(key);
+ }
+
+ public void putLocalMetadataNote(String key, String value) {
+ localMetadata.put(key, value);
+ }
+
+ public Integer getLocalMetadataNoteInt(String key) {
+ String note = getLocalMetadataNote(key);
+ return note==null ? null : Integer.parseInt(note);
+ }
+
+ public void putLocalMetadataNoteInt(String key, int value) {
+ localMetadata.put(key, String.valueOf(value));
+ }
+
+ public Map<String, String> getLocalMetadata() {
+ return localMetadata;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SessionEntityWrapper)) return false;
+
+ SessionEntityWrapper that = (SessionEntityWrapper) o;
+
+ if (!Objects.equals(version, that.version)) {
+ return false;
+ }
+
+ return Objects.equals(entity, that.entity);
+ }
+
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(version) * 17
+ + Objects.hashCode(entity);
+ }
+
+ @Override
+ public String toString() {
+ return "SessionEntityWrapper{" + "version=" + version + ", entity=" + entity + ", localMetadata=" + localMetadata + '}';
+ }
+
+ public static class ExternalizerImpl implements Externalizer<SessionEntityWrapper> {
+
+
+ @Override
+ public void writeObject(ObjectOutput output, SessionEntityWrapper obj) throws IOException {
+ MarshallUtil.marshallUUID(obj.version, output, false);
+ MarshallUtil.marshallMap(obj.localMetadata, output);
+ output.writeObject(obj.getEntity());
+ }
+
+
+ @Override
+ public SessionEntityWrapper readObject(ObjectInput input) throws IOException, ClassNotFoundException {
+ UUID objVersion = MarshallUtil.unmarshallUUID(input, false);
+
+ Map<String, String> localMetadata = MarshallUtil.unmarshallMap(input, new MarshallUtil.MapBuilder<String, String, Map<String, String>>() {
+
+ @Override
+ public Map<String, String> build(int size) {
+ return new ConcurrentHashMap<>(size);
+ }
+
+ });
+
+ SessionEntity entity = (SessionEntity) input.readObject();
+
+ return new SessionEntityWrapper<>(objVersion, localMetadata, entity);
+ }
+
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java
new file mode 100644
index 0000000..f9adf9b
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.changes.sessions;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LastSessionRefreshChecker {
+
+ public static final Logger logger = Logger.getLogger(LastSessionRefreshChecker.class);
+
+ private final LastSessionRefreshStore store;
+ private final LastSessionRefreshStore offlineStore;
+
+
+ public LastSessionRefreshChecker(LastSessionRefreshStore store, LastSessionRefreshStore offlineStore) {
+ this.store = store;
+ this.offlineStore = offlineStore;
+ }
+
+
+ public SessionUpdateTask.CrossDCMessageStatus getCrossDCMessageStatus(KeycloakSession kcSession, RealmModel realm, SessionEntityWrapper<UserSessionEntity> sessionWrapper, boolean offline, int newLastSessionRefresh) {
+ // revokeRefreshToken always writes everything to remoteCache immediately
+ if (realm.isRevokeRefreshToken()) {
+ return SessionUpdateTask.CrossDCMessageStatus.SYNC;
+ }
+
+ // We're likely not in cross-dc environment. Doesn't matter what we return
+ LastSessionRefreshStore storeToUse = offline ? offlineStore : store;
+ if (storeToUse == null) {
+ return SessionUpdateTask.CrossDCMessageStatus.SYNC;
+ }
+
+ Boolean ignoreRemoteCacheUpdate = (Boolean) kcSession.getAttribute(LastSessionRefreshListener.IGNORE_REMOTE_CACHE_UPDATE);
+ if (ignoreRemoteCacheUpdate != null && ignoreRemoteCacheUpdate) {
+ return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED;
+ }
+
+ Integer lsrr = sessionWrapper.getLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE);
+ if (lsrr == null) {
+ logger.warnf("Not available lsrr note on user session %s.", sessionWrapper.getEntity().getId());
+ return SessionUpdateTask.CrossDCMessageStatus.SYNC;
+ }
+
+ int idleTimeout = offline ? realm.getOfflineSessionIdleTimeout() : realm.getSsoSessionIdleTimeout();
+
+ if (lsrr + (idleTimeout / 2) <= newLastSessionRefresh) {
+ logger.debugf("We are going to write remotely. Remote last session refresh: %d, New last session refresh: %d", (int) lsrr, newLastSessionRefresh);
+ return SessionUpdateTask.CrossDCMessageStatus.SYNC;
+ }
+
+ logger.debugf("Skip writing last session refresh to the remoteCache. Session %s newLastSessionRefresh %d", sessionWrapper.getEntity().getId(), newLastSessionRefresh);
+
+ storeToUse.putLastSessionRefresh(kcSession, sessionWrapper.getEntity().getId(), realm.getId(), newLastSessionRefresh);
+
+ return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED;
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshEvent.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshEvent.java
new file mode 100644
index 0000000..7d2af5f
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshEvent.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.changes.sessions;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.infinispan.commons.marshall.Externalizer;
+import org.infinispan.commons.marshall.MarshallUtil;
+import org.infinispan.commons.marshall.SerializeWith;
+import org.keycloak.cluster.ClusterEvent;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@SerializeWith(LastSessionRefreshEvent.ExternalizerImpl.class)
+public class LastSessionRefreshEvent implements ClusterEvent {
+
+ private final Map<String, SessionData> lastSessionRefreshes;
+
+ public LastSessionRefreshEvent(Map<String, SessionData> lastSessionRefreshes) {
+ this.lastSessionRefreshes = lastSessionRefreshes;
+ }
+
+ public Map<String, SessionData> getLastSessionRefreshes() {
+ return lastSessionRefreshes;
+ }
+
+
+ public static class ExternalizerImpl implements Externalizer<LastSessionRefreshEvent> {
+
+
+ @Override
+ public void writeObject(ObjectOutput output, LastSessionRefreshEvent obj) throws IOException {
+ MarshallUtil.marshallMap(obj.lastSessionRefreshes, output);
+ }
+
+
+ @Override
+ public LastSessionRefreshEvent readObject(ObjectInput input) throws IOException, ClassNotFoundException {
+ Map<String, SessionData> map = MarshallUtil.unmarshallMap(input, new MarshallUtil.MapBuilder<String, SessionData, Map<String, SessionData>>() {
+
+ @Override
+ public Map<String, SessionData> build(int size) {
+ return new HashMap<>(size);
+ }
+
+ });
+
+ LastSessionRefreshEvent event = new LastSessionRefreshEvent(map);
+ return event;
+ }
+
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java
new file mode 100644
index 0000000..1bc151f
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.changes.sessions;
+
+import java.util.Map;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.event.ClientEvent;
+import org.jboss.logging.Logger;
+import org.keycloak.cluster.ClusterEvent;
+import org.keycloak.cluster.ClusterListener;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LastSessionRefreshListener implements ClusterListener {
+
+ public static final Logger logger = Logger.getLogger(LastSessionRefreshListener.class);
+
+ public static final String IGNORE_REMOTE_CACHE_UPDATE = "IGNORE_REMOTE_CACHE_UPDATE";
+
+ private final boolean offline;
+
+ private final KeycloakSessionFactory sessionFactory;
+ private final Cache<String, SessionEntityWrapper> cache;
+ private final boolean distributed;
+ private final String myAddress;
+
+ public LastSessionRefreshListener(KeycloakSession session, Cache<String, SessionEntityWrapper> cache, boolean offline) {
+ this.sessionFactory = session.getKeycloakSessionFactory();
+ this.cache = cache;
+ this.offline = offline;
+
+ this.distributed = InfinispanUtil.isDistributedCache(cache);
+ if (this.distributed) {
+ this.myAddress = InfinispanUtil.getMyAddress(session);
+ } else {
+ this.myAddress = null;
+ }
+ }
+
+ @Override
+ public void eventReceived(ClusterEvent event) {
+ Map<String, SessionData> lastSessionRefreshes = ((LastSessionRefreshEvent) event).getLastSessionRefreshes();
+
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Received refreshes. Offline %b, refreshes: %s", offline, lastSessionRefreshes);
+ }
+
+ lastSessionRefreshes.entrySet().stream().forEach((entry) -> {
+ String sessionId = entry.getKey();
+ String realmId = entry.getValue().getRealmId();
+ int lastSessionRefresh = entry.getValue().getLastSessionRefresh();
+
+ // All nodes will receive the message. So ensure that each node updates just lastSessionRefreshes owned by him.
+ if (shouldUpdateLocalCache(sessionId)) {
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, (kcSession) -> {
+
+ RealmModel realm = kcSession.realms().getRealm(realmId);
+ UserSessionModel userSession = kcSession.sessions().getUserSession(realm, sessionId);
+ if (userSession == null) {
+ logger.debugf("User session %s not available on node %s", sessionId, myAddress);
+ } else {
+ // Update just if lastSessionRefresh from event is bigger than ours
+ if (lastSessionRefresh > userSession.getLastSessionRefresh()) {
+
+ // Ensure that remoteCache won't be updated due to this
+ kcSession.setAttribute(IGNORE_REMOTE_CACHE_UPDATE, true);
+
+ userSession.setLastSessionRefresh(lastSessionRefresh);
+ }
+ }
+ });
+ }
+
+ });
+ }
+
+
+ // For distributed caches, ensure that local modification is executed just on owner
+ protected boolean shouldUpdateLocalCache(String key) {
+ if (!distributed) {
+ return true;
+ } else {
+ String keyAddress = InfinispanUtil.getKeyPrimaryOwnerAddress(cache, key);
+ return myAddress.equals(keyAddress);
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStore.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStore.java
new file mode 100644
index 0000000..c50bcf1
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStore.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.changes.sessions;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jboss.logging.Logger;
+import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * Tracks the queue of lastSessionRefreshes, which were updated on this host. Those will be sent to the second DC in bulk, so second DC can update
+ * lastSessionRefreshes on it's side. Message is sent either periodically or if there are lots of stored lastSessionRefreshes.
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LastSessionRefreshStore {
+
+ protected static final Logger logger = Logger.getLogger(LastSessionRefreshStore.class);
+
+ private final int maxIntervalBetweenMessagesSeconds;
+ private final int maxCount;
+ private final String eventKey;
+
+ private volatile Map<String, SessionData> lastSessionRefreshes = new ConcurrentHashMap<>();
+
+ private volatile int lastRun = Time.currentTime();
+
+
+ protected LastSessionRefreshStore(int maxIntervalBetweenMessagesSeconds, int maxCount, String eventKey) {
+ this.maxIntervalBetweenMessagesSeconds = maxIntervalBetweenMessagesSeconds;
+ this.maxCount = maxCount;
+ this.eventKey = eventKey;
+ }
+
+
+ public void putLastSessionRefresh(KeycloakSession kcSession, String sessionId, String realmId, int lastSessionRefresh) {
+ lastSessionRefreshes.put(sessionId, new SessionData(realmId, lastSessionRefresh));
+
+ // Assume that lastSessionRefresh is same or close to current time
+ checkSendingMessage(kcSession, lastSessionRefresh);
+ }
+
+
+ void checkSendingMessage(KeycloakSession kcSession, int currentTime) {
+ if (lastSessionRefreshes.size() >= maxCount || lastRun + maxIntervalBetweenMessagesSeconds <= currentTime) {
+ Map<String, SessionData> refreshesToSend = prepareSendingMessage(currentTime);
+
+ // Sending message doesn't need to be synchronized
+ if (refreshesToSend != null) {
+ sendMessage(kcSession, refreshesToSend);
+ }
+ }
+ }
+
+
+ // synchronized manipulation with internal object instances. Will return map if message should be sent. Otherwise return null
+ private synchronized Map<String, SessionData> prepareSendingMessage(int currentTime) {
+ if (lastSessionRefreshes.size() >= maxCount || lastRun + maxIntervalBetweenMessagesSeconds <= currentTime) {
+ // Create new map instance, so that new writers will use that one
+ Map<String, SessionData> copiedRefreshesToSend = lastSessionRefreshes;
+ lastSessionRefreshes = new ConcurrentHashMap<>();
+ lastRun = currentTime;
+
+ return copiedRefreshesToSend;
+ } else {
+ return null;
+ }
+ }
+
+
+ protected void sendMessage(KeycloakSession kcSession, Map<String, SessionData> refreshesToSend) {
+ LastSessionRefreshEvent event = new LastSessionRefreshEvent(refreshesToSend);
+
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Sending lastSessionRefreshes for key '%s'. Refreshes: %s", eventKey, event.getLastSessionRefreshes().toString());
+ }
+
+ // Don't notify local DC about the lastSessionRefreshes. They were processed here already
+ ClusterProvider cluster = kcSession.getProvider(ClusterProvider.class);
+ cluster.notify(eventKey, event, true, ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC);
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStoreFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStoreFactory.java
new file mode 100644
index 0000000..9c38251
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStoreFactory.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.changes.sessions;
+
+import org.infinispan.Cache;
+import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import org.keycloak.timer.TimerProvider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LastSessionRefreshStoreFactory {
+
+ // Timer interval. The store will be checked every 5 seconds whether the message with stored lastSessionRefreshes
+ public static final long DEFAULT_TIMER_INTERVAL_MS = 5000;
+
+ // Max interval between messages. It means that when message is sent to second DC, then another message will be sent at least after 60 seconds.
+ public static final int DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS = 60;
+
+ // Max count of lastSessionRefreshes. It count of lastSessionRefreshes reach this value, the message is sent to second DC
+ public static final int DEFAULT_MAX_COUNT = 100;
+
+
+ public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache<String, SessionEntityWrapper> cache, boolean offline) {
+ return createAndInit(kcSession, cache, DEFAULT_TIMER_INTERVAL_MS, DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS, DEFAULT_MAX_COUNT, offline);
+ }
+
+
+ public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache<String, SessionEntityWrapper> cache, long timerIntervalMs, int maxIntervalBetweenMessagesSeconds, int maxCount, boolean offline) {
+ String eventKey = offline ? "lastSessionRefreshes-offline" : "lastSessionRefreshes";
+ LastSessionRefreshStore store = createStoreInstance(maxIntervalBetweenMessagesSeconds, maxCount, eventKey);
+
+ // Register listener
+ ClusterProvider cluster = kcSession.getProvider(ClusterProvider.class);
+ cluster.registerListener(eventKey, new LastSessionRefreshListener(kcSession, cache, offline));
+
+ // Setup periodic timer check
+ TimerProvider timer = kcSession.getProvider(TimerProvider.class);
+ timer.scheduleTask((KeycloakSession keycloakSession) -> {
+
+ store.checkSendingMessage(keycloakSession, Time.currentTime());
+
+ }, timerIntervalMs, eventKey);
+
+ return store;
+ }
+
+
+ protected LastSessionRefreshStore createStoreInstance(int maxIntervalBetweenMessagesSeconds, int maxCount, String eventKey) {
+ return new LastSessionRefreshStore(maxIntervalBetweenMessagesSeconds, maxCount, eventKey);
+ }
+
+
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/SessionData.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/SessionData.java
new file mode 100644
index 0000000..5f78eda
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/SessionData.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.changes.sessions;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+
+import org.infinispan.commons.marshall.Externalizer;
+import org.infinispan.commons.marshall.MarshallUtil;
+import org.infinispan.commons.marshall.SerializeWith;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@SerializeWith(SessionData.ExternalizerImpl.class)
+public class SessionData {
+
+ private final String realmId;
+ private final int lastSessionRefresh;
+
+ public SessionData(String realmId, int lastSessionRefresh) {
+ this.realmId = realmId;
+ this.lastSessionRefresh = lastSessionRefresh;
+ }
+
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public int getLastSessionRefresh() {
+ return lastSessionRefresh;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("realmId: %s, lastSessionRefresh: %d", realmId, lastSessionRefresh);
+ }
+
+ public static class ExternalizerImpl implements Externalizer<SessionData> {
+
+
+ @Override
+ public void writeObject(ObjectOutput output, SessionData obj) throws IOException {
+ MarshallUtil.marshallString(obj.realmId, output);
+ MarshallUtil.marshallInt(output, obj.lastSessionRefresh);
+ }
+
+
+ @Override
+ public SessionData readObject(ObjectInput input) throws IOException, ClassNotFoundException {
+ String realmId = MarshallUtil.unmarshallString(input);
+ int lastSessionRefresh = MarshallUtil.unmarshallInt(input);
+
+ return new SessionData(realmId, lastSessionRefresh);
+ }
+
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java
new file mode 100644
index 0000000..66f88ba
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.changes;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface SessionUpdateTask<S extends SessionEntity> {
+
+ void runUpdate(S entity);
+
+ CacheOperation getOperation(S entity);
+
+ CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<S> sessionWrapper);
+
+ default long getLifespanMs() {
+ return -1;
+ }
+
+
+ enum CacheOperation {
+
+ ADD,
+ ADD_IF_ABSENT, // ADD_IF_ABSENT throws an exception if there is existing value
+ REMOVE,
+ REPLACE;
+
+ CacheOperation merge(CacheOperation other, SessionEntity entity) {
+ if (this == REMOVE || other == REMOVE) {
+ return REMOVE;
+ }
+
+ if (this == ADD | this == ADD_IF_ABSENT) {
+ if (other == ADD | other == ADD_IF_ABSENT) {
+ throw new IllegalStateException("Illegal state. Task already in progress for session " + entity.getId());
+ }
+
+ return this;
+ }
+
+ // Lowest priority
+ return REPLACE;
+ }
+ }
+
+
+ enum CrossDCMessageStatus {
+ SYNC,
+ //ASYNC,
+ // QUEUE,
+ NOT_NEEDED;
+
+
+ CrossDCMessageStatus merge(CrossDCMessageStatus other) {
+ if (this == SYNC || other == SYNC) {
+ return SYNC;
+ }
+
+ /*if (this == ASYNC || other == ASYNC) {
+ return ASYNC;
+ }*/
+
+ return NOT_NEEDED;
+ }
+
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionClientSessionUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionClientSessionUpdateTask.java
new file mode 100644
index 0000000..56e0403
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionClientSessionUpdateTask.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.changes;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+
+/**
+ * Task for create or update AuthenticatedClientSessionEntity within userSession
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class UserSessionClientSessionUpdateTask extends UserSessionUpdateTask {
+
+ public static final Logger logger = Logger.getLogger(UserSessionClientSessionUpdateTask.class);
+
+ private final String clientUUID;
+
+ public UserSessionClientSessionUpdateTask(String clientUUID) {
+ this.clientUUID = clientUUID;
+ }
+
+ @Override
+ public void runUpdate(UserSessionEntity userSession) {
+ AuthenticatedClientSessionEntity clientSession = userSession.getAuthenticatedClientSessions().get(clientUUID);
+ if (clientSession == null) {
+ logger.warnf("Not found authenticated client session entity for client %s in userSession %s", clientUUID, userSession.getId());
+ return;
+ }
+
+ runClientSessionUpdate(clientSession);
+ }
+
+ protected abstract void runClientSessionUpdate(AuthenticatedClientSessionEntity entity);
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java
new file mode 100644
index 0000000..4fd4bbe
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.changes;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class UserSessionUpdateTask implements SessionUpdateTask<UserSessionEntity> {
+
+ @Override
+ public CacheOperation getOperation(UserSessionEntity session) {
+ return CacheOperation.REPLACE;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
+ return CrossDCMessageStatus.SYNC;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java
index 3641d5f..f67d736 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java
@@ -17,13 +17,24 @@
package org.keycloak.models.sessions.infinispan.entities;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.infinispan.commons.marshall.Externalizer;
+import org.infinispan.commons.marshall.MarshallUtil;
+import org.infinispan.commons.marshall.SerializeWith;
+import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
/**
+ *
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
+@SerializeWith(AuthenticatedClientSessionEntity.ExternalizerImpl.class)
public class AuthenticatedClientSessionEntity implements Serializable {
private String authMethod;
@@ -33,7 +44,7 @@ public class AuthenticatedClientSessionEntity implements Serializable {
private Set<String> roles;
private Set<String> protocolMappers;
- private Map<String, String> notes;
+ private Map<String, String> notes = new ConcurrentHashMap<>();
public String getAuthMethod() {
return authMethod;
@@ -91,4 +102,46 @@ public class AuthenticatedClientSessionEntity implements Serializable {
this.notes = notes;
}
+
+ public static class ExternalizerImpl implements Externalizer<AuthenticatedClientSessionEntity> {
+
+ @Override
+ public void writeObject(ObjectOutput output, AuthenticatedClientSessionEntity session) throws IOException {
+ MarshallUtil.marshallString(session.getAuthMethod(), output);
+ MarshallUtil.marshallString(session.getRedirectUri(), output);
+ MarshallUtil.marshallInt(output, session.getTimestamp());
+ MarshallUtil.marshallString(session.getAction(), output);
+
+ Map<String, String> notes = session.getNotes();
+ KeycloakMarshallUtil.writeMap(notes, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, output);
+
+ KeycloakMarshallUtil.writeCollection(session.getProtocolMappers(), KeycloakMarshallUtil.STRING_EXT, output);
+ KeycloakMarshallUtil.writeCollection(session.getRoles(), KeycloakMarshallUtil.STRING_EXT, output);
+ }
+
+
+ @Override
+ public AuthenticatedClientSessionEntity readObject(ObjectInput input) throws IOException, ClassNotFoundException {
+ AuthenticatedClientSessionEntity sessionEntity = new AuthenticatedClientSessionEntity();
+
+ sessionEntity.setAuthMethod(MarshallUtil.unmarshallString(input));
+ sessionEntity.setRedirectUri(MarshallUtil.unmarshallString(input));
+ sessionEntity.setTimestamp(MarshallUtil.unmarshallInt(input));
+ sessionEntity.setAction(MarshallUtil.unmarshallString(input));
+
+ Map<String, String> notes = KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT,
+ new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>());
+ sessionEntity.setNotes(notes);
+
+ Set<String> protocolMappers = KeycloakMarshallUtil.readCollection(input, KeycloakMarshallUtil.STRING_EXT, new KeycloakMarshallUtil.HashSetBuilder<>());
+ sessionEntity.setProtocolMappers(protocolMappers);
+
+ Set<String> roles = KeycloakMarshallUtil.readCollection(input, KeycloakMarshallUtil.STRING_EXT, new KeycloakMarshallUtil.HashSetBuilder<>());
+ sessionEntity.setRoles(roles);
+
+ return sessionEntity;
+ }
+
+ }
+
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java
index 8aeb79e..25ac2a4 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java
@@ -19,10 +19,12 @@ package org.keycloak.models.sessions.infinispan.entities;
import java.io.Serializable;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class SessionEntity implements Serializable {
+public abstract class SessionEntity implements Serializable {
private String id;
@@ -60,4 +62,10 @@ public class SessionEntity implements Serializable {
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
+
+
+ public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) {
+ throw new IllegalStateException("Not yet implemented");
+ };
+
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
index 3c4746d..5d0edb0 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
@@ -17,18 +17,32 @@
package org.keycloak.models.sessions.infinispan.entities;
+import org.infinispan.commons.marshall.Externalizer;
+import org.infinispan.commons.marshall.MarshallUtil;
+import org.infinispan.commons.marshall.SerializeWith;
+import org.jboss.logging.Logger;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
import java.util.Map;
-import java.util.Set;
+import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArraySet;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
+@SerializeWith(UserSessionEntity.ExternalizerImpl.class)
public class UserSessionEntity extends SessionEntity {
+ public static final Logger logger = Logger.getLogger(UserSessionEntity.class);
+
+ // Metadata attribute, which contains the lastSessionRefresh available on remoteCache. Used in decide whether we need to write to remoteCache (DC) or not
+ public static final String LAST_SESSION_REFRESH_REMOTE = "lsrr";
+
private String user;
private String brokerSessionId;
@@ -147,4 +161,120 @@ public class UserSessionEntity extends SessionEntity {
public void setBrokerUserId(String brokerUserId) {
this.brokerUserId = brokerUserId;
}
+
+ @Override
+ public String toString() {
+ return String.format("UserSessionEntity [id=%s, realm=%s, lastSessionRefresh=%d, clients=%s]", getId(), getRealm(), getLastSessionRefresh(),
+ new TreeSet(this.authenticatedClientSessions.keySet()));
+ }
+
+ @Override
+ public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) {
+ int lsrRemote = getLastSessionRefresh();
+
+ SessionEntityWrapper entityWrapper;
+ if (localEntityWrapper == null) {
+ entityWrapper = new SessionEntityWrapper<>(this);
+ } else {
+ UserSessionEntity localUserSession = (UserSessionEntity) localEntityWrapper.getEntity();
+
+ // local lastSessionRefresh should always contain the bigger
+ if (lsrRemote < localUserSession.getLastSessionRefresh()) {
+ setLastSessionRefresh(localUserSession.getLastSessionRefresh());
+ }
+
+ entityWrapper = new SessionEntityWrapper<>(localEntityWrapper.getLocalMetadata(), this);
+ }
+
+ entityWrapper.putLocalMetadataNoteInt(LAST_SESSION_REFRESH_REMOTE, lsrRemote);
+
+ logger.debugf("Updating session entity. lastSessionRefresh=%d, lastSessionRefreshRemote=%d", getLastSessionRefresh(), lsrRemote);
+
+ return entityWrapper;
+ }
+
+
+ public static class ExternalizerImpl implements Externalizer<UserSessionEntity> {
+
+ private static final int VERSION_1 = 1;
+
+ @Override
+ public void writeObject(ObjectOutput output, UserSessionEntity session) throws IOException {
+ output.writeByte(VERSION_1);
+
+ MarshallUtil.marshallString(session.getAuthMethod(), output);
+ MarshallUtil.marshallString(session.getBrokerSessionId(), output);
+ MarshallUtil.marshallString(session.getBrokerUserId(), output);
+ MarshallUtil.marshallString(session.getId(), output);
+ MarshallUtil.marshallString(session.getIpAddress(), output);
+ MarshallUtil.marshallString(session.getLoginUsername(), output);
+ MarshallUtil.marshallString(session.getRealm(), output);
+ MarshallUtil.marshallString(session.getUser(), output);
+
+ MarshallUtil.marshallInt(output, session.getLastSessionRefresh());
+ MarshallUtil.marshallInt(output, session.getStarted());
+ output.writeBoolean(session.isRememberMe());
+
+ int state = session.getState() == null ? 0 :
+ ((session.getState() == UserSessionModel.State.LOGGED_IN) ? 1 : (session.getState() == UserSessionModel.State.LOGGED_OUT ? 2 : 3));
+ output.writeInt(state);
+
+ Map<String, String> notes = session.getNotes();
+ KeycloakMarshallUtil.writeMap(notes, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, output);
+
+ Map<String, AuthenticatedClientSessionEntity> authSessions = session.getAuthenticatedClientSessions();
+ KeycloakMarshallUtil.writeMap(authSessions, KeycloakMarshallUtil.STRING_EXT, new AuthenticatedClientSessionEntity.ExternalizerImpl(), output);
+ }
+
+
+ @Override
+ public UserSessionEntity readObject(ObjectInput input) throws IOException, ClassNotFoundException {
+ switch (input.readByte()) {
+ case VERSION_1:
+ return readObjectVersion1(input);
+ default:
+ throw new IOException("Unknown version");
+ }
+ }
+
+ public UserSessionEntity readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
+ UserSessionEntity sessionEntity = new UserSessionEntity();
+
+ sessionEntity.setAuthMethod(MarshallUtil.unmarshallString(input));
+ sessionEntity.setBrokerSessionId(MarshallUtil.unmarshallString(input));
+ sessionEntity.setBrokerUserId(MarshallUtil.unmarshallString(input));
+ sessionEntity.setId(MarshallUtil.unmarshallString(input));
+ sessionEntity.setIpAddress(MarshallUtil.unmarshallString(input));
+ sessionEntity.setLoginUsername(MarshallUtil.unmarshallString(input));
+ sessionEntity.setRealm(MarshallUtil.unmarshallString(input));
+ sessionEntity.setUser(MarshallUtil.unmarshallString(input));
+
+ sessionEntity.setLastSessionRefresh(MarshallUtil.unmarshallInt(input));
+ sessionEntity.setStarted(MarshallUtil.unmarshallInt(input));
+ sessionEntity.setRememberMe(input.readBoolean());
+
+ int state = input.readInt();
+ switch(state) {
+ case 1: sessionEntity.setState(UserSessionModel.State.LOGGED_IN);
+ break;
+ case 2: sessionEntity.setState(UserSessionModel.State.LOGGED_OUT);
+ break;
+ case 3: sessionEntity.setState(UserSessionModel.State.LOGGING_OUT);
+ break;
+ default:
+ sessionEntity.setState(null);
+ }
+
+ Map<String, String> notes = KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT,
+ new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>());
+ sessionEntity.setNotes(notes);
+
+ Map<String, AuthenticatedClientSessionEntity> authSessions = KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, new AuthenticatedClientSessionEntity.ExternalizerImpl(),
+ new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>());
+ sessionEntity.setAuthenticatedClientSessions(authSessions);
+
+ return sessionEntity;
+ }
+
+ }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractAuthSessionClusterListener.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractAuthSessionClusterListener.java
new file mode 100644
index 0000000..18461e0
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractAuthSessionClusterListener.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.events;
+
+import org.jboss.logging.Logger;
+import org.keycloak.cluster.ClusterEvent;
+import org.keycloak.cluster.ClusterListener;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProvider;
+import org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.sessions.AuthenticationSessionProvider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class AbstractAuthSessionClusterListener <SE extends SessionClusterEvent> implements ClusterListener {
+
+ private static final Logger log = Logger.getLogger(AbstractAuthSessionClusterListener.class);
+
+ private final KeycloakSessionFactory sessionFactory;
+
+ public AbstractAuthSessionClusterListener(KeycloakSessionFactory sessionFactory) {
+ this.sessionFactory = sessionFactory;
+ }
+
+
+ @Override
+ public void eventReceived(ClusterEvent event) {
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, (KeycloakSession session) -> {
+ InfinispanAuthenticationSessionProvider provider = (InfinispanAuthenticationSessionProvider) session.getProvider(AuthenticationSessionProvider.class,
+ InfinispanAuthenticationSessionProviderFactory.PROVIDER_ID);
+ SE sessionEvent = (SE) event;
+
+ if (!provider.getCache().getStatus().allowInvocations()) {
+ log.debugf("Cache in state '%s' doesn't allow invocations", provider.getCache().getStatus());
+ return;
+ }
+
+ log.debugf("Received authentication session event '%s'", sessionEvent.toString());
+
+ eventReceived(session, provider, sessionEvent);
+
+ });
+ }
+
+ protected abstract void eventReceived(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, SE sessionEvent);
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractUserSessionClusterListener.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractUserSessionClusterListener.java
new file mode 100644
index 0000000..70d94f0
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractUserSessionClusterListener.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.events;
+
+import org.jboss.logging.Logger;
+import org.keycloak.cluster.ClusterEvent;
+import org.keycloak.cluster.ClusterListener;
+import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionProvider;
+import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider;
+import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class AbstractUserSessionClusterListener<SE extends SessionClusterEvent> implements ClusterListener {
+
+ private static final Logger log = Logger.getLogger(AbstractUserSessionClusterListener.class);
+
+ private final KeycloakSessionFactory sessionFactory;
+
+ public AbstractUserSessionClusterListener(KeycloakSessionFactory sessionFactory) {
+ this.sessionFactory = sessionFactory;
+ }
+
+
+ @Override
+ public void eventReceived(ClusterEvent event) {
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, (KeycloakSession session) -> {
+ InfinispanUserSessionProvider provider = (InfinispanUserSessionProvider) session.getProvider(UserSessionProvider.class, InfinispanUserSessionProviderFactory.PROVIDER_ID);
+ SE sessionEvent = (SE) event;
+
+ boolean shouldResendEvent = shouldResendEvent(session, sessionEvent);
+
+ if (log.isDebugEnabled()) {
+ log.debugf("Received user session event '%s'. Should resend event: %b", sessionEvent.toString(), shouldResendEvent);
+ }
+
+ eventReceived(session, provider, sessionEvent);
+
+ if (shouldResendEvent) {
+ session.getProvider(ClusterProvider.class).notify(sessionEvent.getEventKey(), event, true, ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC);
+ }
+
+ });
+ }
+
+ protected abstract void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, SE sessionEvent);
+
+
+ private boolean shouldResendEvent(KeycloakSession session, SessionClusterEvent event) {
+ if (!event.isResendingEvent()) {
+ return false;
+ }
+
+ // Just the initiator will re-send the event after receiving it
+ String myNode = InfinispanUtil.getMyAddress(session);
+ String mySite = InfinispanUtil.getMySite(session);
+ return (event.getNodeId() != null && event.getNodeId().equals(myNode) && event.getSiteId() != null && event.getSiteId().equals(mySite));
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/ClientRemovedSessionEvent.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/ClientRemovedSessionEvent.java
new file mode 100644
index 0000000..8dbad8c
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/ClientRemovedSessionEvent.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.events;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientRemovedSessionEvent extends SessionClusterEvent {
+
+ private String clientUuid;
+
+ public static ClientRemovedSessionEvent create(KeycloakSession session, String eventKey, String realmId, boolean resendingEvent, String clientUuid) {
+ ClientRemovedSessionEvent event = ClientRemovedSessionEvent.createEvent(ClientRemovedSessionEvent.class, eventKey, session, realmId, resendingEvent);
+ event.clientUuid = clientUuid;
+ return event;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ClientRemovedSessionEvent [ realmId=%s , clientUuid=%s ]", getRealmId(), clientUuid);
+ }
+
+ public String getClientUuid() {
+ return clientUuid;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RemoveUserSessionsEvent.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RemoveUserSessionsEvent.java
new file mode 100644
index 0000000..968ff86
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RemoveUserSessionsEvent.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.events;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RemoveUserSessionsEvent extends SessionClusterEvent {
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionClusterEvent.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionClusterEvent.java
new file mode 100644
index 0000000..118eb53
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionClusterEvent.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.events;
+
+import org.keycloak.cluster.ClusterEvent;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class SessionClusterEvent implements ClusterEvent {
+
+ private String realmId;
+ private String eventKey;
+ private boolean resendingEvent;
+ private String siteId;
+ private String nodeId;
+
+
+ public static <T extends SessionClusterEvent> T createEvent(Class<T> eventClass, String eventKey, KeycloakSession session, String realmId, boolean resendingEvent) {
+ try {
+ T event = eventClass.newInstance();
+ event.setData(session, eventKey, realmId, resendingEvent);
+ return event;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ void setData(KeycloakSession session, String eventKey, String realmId, boolean resendingEvent) {
+ this.realmId = realmId;
+ this.eventKey = eventKey;
+ this.resendingEvent = resendingEvent;
+ this.siteId = InfinispanUtil.getMySite(session);
+ this.nodeId = InfinispanUtil.getMyAddress(session);
+ }
+
+
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public String getEventKey() {
+ return eventKey;
+ }
+
+ public boolean isResendingEvent() {
+ return resendingEvent;
+ }
+
+ public String getSiteId() {
+ return siteId;
+ }
+
+ public String getNodeId() {
+ return nodeId;
+ }
+
+ @Override
+ public String toString() {
+ String simpleClassName = getClass().getSimpleName();
+ return String.format("%s [ realmId=%s ]", simpleClassName, realmId);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionEventsSenderTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionEventsSenderTransaction.java
new file mode 100644
index 0000000..f98bbe6
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionEventsSenderTransaction.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.events;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.models.AbstractKeycloakTransaction;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * Postpone sending notifications of session events to the commit of Keycloak transaction
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class SessionEventsSenderTransaction extends AbstractKeycloakTransaction {
+
+ private final KeycloakSession session;
+
+ private final List<DCEventContext> sessionEvents = new LinkedList<>();
+
+ public SessionEventsSenderTransaction(KeycloakSession session) {
+ this.session = session;
+ }
+
+ public void addEvent(SessionClusterEvent event, ClusterProvider.DCNotify dcNotify) {
+ sessionEvents.add(new DCEventContext(dcNotify, event));
+ }
+
+
+ @Override
+ protected void commitImpl() {
+ ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+
+ // TODO bulk notify (send whole list instead of separate events?)
+ for (DCEventContext entry : sessionEvents) {
+ cluster.notify(entry.event.getEventKey(), entry.event, false, entry.dcNotify);
+ }
+ }
+
+
+ @Override
+ protected void rollbackImpl() {
+
+ }
+
+
+ private class DCEventContext {
+ private final ClusterProvider.DCNotify dcNotify;
+ private final SessionClusterEvent event;
+
+ DCEventContext(ClusterProvider.DCNotify dcNotify, SessionClusterEvent event) {
+ this.dcNotify = dcNotify;
+ this.event = event;
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java
index b4689aa..192c964 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java
@@ -61,10 +61,6 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi
this.tx.put(actionKeyCache, tokenKey, tokenValue, key.getExpiration() - Time.currentTime(), TimeUnit.SECONDS);
}
- private static String generateActionTokenEventId() {
- return InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS + "/" + UUID.randomUUID();
- }
-
@Override
public ActionTokenValueModel get(ActionTokenKeyModel actionTokenKey) {
if (actionTokenKey == null || actionTokenKey.getUserId() == null || actionTokenKey.getActionId() == null) {
@@ -98,6 +94,6 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi
}
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- this.tx.notify(cluster, generateActionTokenEventId(), new RemoveActionTokensSpecificEvent(userId, actionId), false);
+ this.tx.notify(cluster, InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS, new RemoveActionTokensSpecificEvent(userId, actionId), false);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java
index 95ee903..e4f3bd0 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java
@@ -70,24 +70,24 @@ public class InfinispanActionTokenStoreProviderFactory implements ActionTokenSto
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.registerListener(ClusterProvider.ALL, event -> {
- if (event instanceof RemoveActionTokensSpecificEvent) {
- RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event;
+ cluster.registerListener(ACTION_TOKEN_EVENTS, event -> {
- LOG.debugf("[%s] Removing token invalidation for user+action: userId=%s, actionId=%s", cacheAddress, e.getUserId(), e.getActionId());
+ RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event;
- AdvancedCache<ActionTokenReducedKey, ActionTokenValueEntity> localCache = cache
- .getAdvancedCache()
- .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD);
+ LOG.debugf("[%s] Removing token invalidation for user+action: userId=%s, actionId=%s", cacheAddress, e.getUserId(), e.getActionId());
- List<ActionTokenReducedKey> toRemove = localCache
- .keySet()
- .stream()
- .filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId()))
- .collect(Collectors.toList());
+ AdvancedCache<ActionTokenReducedKey, ActionTokenValueEntity> localCache = cache
+ .getAdvancedCache()
+ .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD);
+
+ List<ActionTokenReducedKey> toRemove = localCache
+ .keySet()
+ .stream()
+ .filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId()))
+ .collect(Collectors.toList());
+
+ toRemove.forEach(localCache::remove);
- toRemove.forEach(localCache::remove);
- }
});
LOG.debugf("[%s] Registered cluster listeners", cacheAddress);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
index 5991f98..15a37cc 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
@@ -30,6 +30,9 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
+import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
+import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
+import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmInfoUtil;
@@ -46,13 +49,17 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
private final KeycloakSession session;
private final Cache<String, AuthenticationSessionEntity> cache;
protected final InfinispanKeycloakTransaction tx;
+ protected final SessionEventsSenderTransaction clusterEventsSenderTx;
public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache<String, AuthenticationSessionEntity> cache) {
this.session = session;
this.cache = cache;
this.tx = new InfinispanKeycloakTransaction();
+ this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
+
session.getTransactionManager().enlistAfterCompletion(tx);
+ session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
}
@Override
@@ -109,37 +116,67 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
- Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
- .entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId()).expired(expired)).iterator();
+ Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
+ .entrySet()
+ .stream()
+ .filter(AuthenticationSessionPredicate.create(realm.getId()).expired(expired))
+ .iterator();
int counter = 0;
while (itr.hasNext()) {
counter++;
AuthenticationSessionEntity entity = itr.next().getValue();
- tx.remove(cache, entity.getId());
+ tx.remove(CacheDecorators.localCache(cache), entity.getId());
}
- log.debugf("Removed %d expired user sessions for realm '%s'", counter, realm.getName());
+ log.debugf("Removed %d expired authentication sessions for realm '%s'", counter, realm.getName());
}
- // TODO: Should likely listen to "RealmRemovedEvent" received from cluster and clean just local sessions
+
@Override
public void onRealmRemoved(RealmModel realm) {
- Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = cache.entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId())).iterator();
+ // Send message to all DCs. The remoteCache will notify client listeners on all DCs for remove authentication sessions
+ clusterEventsSenderTx.addEvent(
+ RealmRemovedSessionEvent.createEvent(RealmRemovedSessionEvent.class, InfinispanAuthenticationSessionProviderFactory.REALM_REMOVED_AUTHSESSION_EVENT, session, realm.getId(), false),
+ ClusterProvider.DCNotify.ALL_DCS);
+ }
+
+ protected void onRealmRemovedEvent(String realmId) {
+ Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
+ .entrySet()
+ .stream()
+ .filter(AuthenticationSessionPredicate.create(realmId))
+ .iterator();
+
while (itr.hasNext()) {
- cache.remove(itr.next().getKey());
+ CacheDecorators.localCache(cache)
+ .remove(itr.next().getKey());
}
}
- // TODO: Should likely listen to "ClientRemovedEvent" received from cluster and clean just local sessions
+
@Override
public void onClientRemoved(RealmModel realm, ClientModel client) {
- Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = cache.entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId()).client(client.getId())).iterator();
+ // Send message to all DCs. The remoteCache will notify client listeners on all DCs for remove authentication sessions of this client
+ clusterEventsSenderTx.addEvent(
+ ClientRemovedSessionEvent.create(session, InfinispanAuthenticationSessionProviderFactory.CLIENT_REMOVED_AUTHSESSION_EVENT, realm.getId(), false, client.getId()),
+ ClusterProvider.DCNotify.ALL_DCS);
+ }
+
+ protected void onClientRemovedEvent(String realmId, String clientUuid) {
+ Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
+ .entrySet()
+ .stream()
+ .filter(AuthenticationSessionPredicate.create(realmId).client(clientUuid))
+ .iterator();
+
while (itr.hasNext()) {
- cache.remove(itr.next().getKey());
+ CacheDecorators.localCache(cache)
+ .remove(itr.next().getKey());
}
}
+
@Override
public void updateNonlocalSessionAuthNotes(String authSessionId, Map<String, String> authNotesFragment) {
if (authSessionId == null) {
@@ -150,7 +187,8 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
cluster.notify(
InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS,
AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, authNotesFragment),
- true
+ true,
+ ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC
);
}
@@ -159,4 +197,7 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
}
+ public Cache<String, AuthenticationSessionEntity> getCache() {
+ return cache;
+ }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
index a9589cc..04e1dc8 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
@@ -26,6 +26,12 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
+import org.keycloak.models.sessions.infinispan.events.AbstractAuthSessionClusterListener;
+import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
+import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
+import org.keycloak.models.utils.PostMigrationEvent;
+import org.keycloak.provider.ProviderEvent;
+import org.keycloak.provider.ProviderEventListener;
import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.sessions.AuthenticationSessionProviderFactory;
import java.util.Map;
@@ -42,13 +48,59 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
private volatile Cache<String, AuthenticationSessionEntity> authSessionsCache;
+ public static final String PROVIDER_ID = "infinispan";
+
public static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS";
+ public static final String REALM_REMOVED_AUTHSESSION_EVENT = "REALM_REMOVED_EVENT_AUTHSESSIONS";
+
+ public static final String CLIENT_REMOVED_AUTHSESSION_EVENT = "CLIENT_REMOVED_SESSION_AUTHSESSIONS";
+
@Override
public void init(Config.Scope config) {
}
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ factory.register(new ProviderEventListener() {
+
+ @Override
+ public void onEvent(ProviderEvent event) {
+ if (event instanceof PostMigrationEvent) {
+ registerClusterListeners(((PostMigrationEvent) event).getSession());
+ }
+ }
+ });
+ }
+
+
+ protected void registerClusterListeners(KeycloakSession session) {
+ KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+ ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+
+ cluster.registerListener(REALM_REMOVED_AUTHSESSION_EVENT, new AbstractAuthSessionClusterListener<RealmRemovedSessionEvent>(sessionFactory) {
+
+ @Override
+ protected void eventReceived(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, RealmRemovedSessionEvent sessionEvent) {
+ provider.onRealmRemovedEvent(sessionEvent.getRealmId());
+ }
+
+ });
+
+ cluster.registerListener(CLIENT_REMOVED_AUTHSESSION_EVENT, new AbstractAuthSessionClusterListener<ClientRemovedSessionEvent>(sessionFactory) {
+
+ @Override
+ protected void eventReceived(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, ClientRemovedSessionEvent sessionEvent) {
+ provider.onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid());
+ }
+ });
+
+ log.debug("Registered cluster listeners");
+ }
+
+
@Override
public AuthenticationSessionProvider create(KeycloakSession session) {
lazyInit(session);
@@ -99,15 +151,11 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
}
@Override
- public void postInit(KeycloakSessionFactory factory) {
- }
-
- @Override
public void close() {
}
@Override
public String getId() {
- return "infinispan";
+ return PROVIDER_ID;
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java
index 5471184..959223c 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java
@@ -155,7 +155,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
theTaskKey = taskKey + "-" + (i++);
}
- tasks.put(taskKey, () -> clusterProvider.notify(taskKey, event, ignoreSender));
+ tasks.put(taskKey, () -> clusterProvider.notify(taskKey, event, ignoreSender, ClusterProvider.DCNotify.ALL_DCS));
}
public <K, V> void remove(Cache<K, V> cache, K key) {
@@ -168,7 +168,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
// This is for possibility to lookup for session by id, which was created in this transaction
public <K, V> V get(Cache<K, V> cache, K key) {
Object taskKey = getTaskKey(cache, key);
- CacheTask<V> current = tasks.get(taskKey);
+ CacheTask current = tasks.get(taskKey);
if (current != null) {
if (current instanceof CacheTaskWithValue) {
return ((CacheTaskWithValue<V>) current).getValue();
@@ -190,11 +190,11 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
}
}
- public interface CacheTask<V> {
+ public interface CacheTask {
void execute();
}
- public abstract class CacheTaskWithValue<V> implements CacheTask<V> {
+ public abstract class CacheTaskWithValue<V> implements CacheTask {
protected V value;
public CacheTaskWithValue(V value) {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java
index b8e6a71..2477b69 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java
@@ -21,6 +21,7 @@ import org.keycloak.Config;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.sessions.StickySessionEncoderProvider;
import org.keycloak.sessions.StickySessionEncoderProviderFactory;
@@ -29,16 +30,22 @@ import org.keycloak.sessions.StickySessionEncoderProviderFactory;
*/
public class InfinispanStickySessionEncoderProviderFactory implements StickySessionEncoderProviderFactory {
- private String myNodeName;
@Override
public StickySessionEncoderProvider create(KeycloakSession session) {
+ String myNodeName = InfinispanUtil.getMyAddress(session);
+
+ if (myNodeName != null && myNodeName.startsWith(InfinispanConnectionProvider.NODE_PREFIX)) {
+
+ // Node name was randomly generated. We won't use anything for sticky sessions in this case
+ myNodeName = null;
+ }
+
return new InfinispanStickySessionEncoderProvider(session, myNodeName);
}
@Override
public void init(Config.Scope config) {
- myNodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
}
@Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 202a051..c1a8ed6 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -18,10 +18,12 @@
package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache;
-import org.infinispan.CacheStream;
+import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.context.Flag;
import org.jboss.logging.Logger;
+import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.util.Time;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
@@ -31,19 +33,27 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.session.UserSessionPersisterProvider;
+import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStore;
+import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
+import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
-import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
+import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
+import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
+import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
+import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
import org.keycloak.models.sessions.infinispan.stream.Comparators;
import org.keycloak.models.sessions.infinispan.stream.Mappers;
import org.keycloak.models.sessions.infinispan.stream.SessionPredicate;
import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate;
import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
-import java.util.Collection;
-import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
@@ -51,7 +61,6 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Predicate;
-import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
@@ -62,31 +71,71 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
private static final Logger log = Logger.getLogger(InfinispanUserSessionProvider.class);
protected final KeycloakSession session;
- protected final Cache<String, SessionEntity> sessionCache;
- protected final Cache<String, SessionEntity> offlineSessionCache;
+
+ protected final Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache;
+ protected final Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache;
protected final Cache<LoginFailureKey, LoginFailureEntity> loginFailureCache;
+
+ protected final InfinispanChangelogBasedTransaction<UserSessionEntity> sessionTx;
+ protected final InfinispanChangelogBasedTransaction<UserSessionEntity> offlineSessionTx;
protected final InfinispanKeycloakTransaction tx;
- public InfinispanUserSessionProvider(KeycloakSession session, Cache<String, SessionEntity> sessionCache, Cache<String, SessionEntity> offlineSessionCache,
+ protected final SessionEventsSenderTransaction clusterEventsSenderTx;
+
+ protected final LastSessionRefreshStore lastSessionRefreshStore;
+ protected final LastSessionRefreshStore offlineLastSessionRefreshStore;
+
+ public InfinispanUserSessionProvider(KeycloakSession session,
+ RemoteCacheInvoker remoteCacheInvoker,
+ LastSessionRefreshStore lastSessionRefreshStore,
+ LastSessionRefreshStore offlineLastSessionRefreshStore,
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache,
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache,
Cache<LoginFailureKey, LoginFailureEntity> loginFailureCache) {
this.session = session;
+
this.sessionCache = sessionCache;
this.offlineSessionCache = offlineSessionCache;
this.loginFailureCache = loginFailureCache;
+
+ this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCache, remoteCacheInvoker);
+ this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, offlineSessionCache, remoteCacheInvoker);
+
this.tx = new InfinispanKeycloakTransaction();
+ this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
+
+ this.lastSessionRefreshStore = lastSessionRefreshStore;
+ this.offlineLastSessionRefreshStore = offlineLastSessionRefreshStore;
+
session.getTransactionManager().enlistAfterCompletion(tx);
+ session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
+ session.getTransactionManager().enlistAfterCompletion(sessionTx);
+ session.getTransactionManager().enlistAfterCompletion(offlineSessionTx);
}
- protected Cache<String, SessionEntity> getCache(boolean offline) {
+ protected Cache<String, SessionEntityWrapper<UserSessionEntity>> getCache(boolean offline) {
return offline ? offlineSessionCache : sessionCache;
}
+ protected InfinispanChangelogBasedTransaction<UserSessionEntity> getTransaction(boolean offline) {
+ return offline ? offlineSessionTx : sessionTx;
+ }
+
+ protected LastSessionRefreshStore getLastSessionRefreshStore() {
+ return lastSessionRefreshStore;
+ }
+
+ protected LastSessionRefreshStore getOfflineLastSessionRefreshStore() {
+ return offlineLastSessionRefreshStore;
+ }
+
@Override
public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) {
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
- AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, sessionCache);
+ InfinispanChangelogBasedTransaction<UserSessionEntity> updateTx = getTransaction(false);
+ AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, updateTx);
adapter.setUserSession(userSession);
return adapter;
}
@@ -95,10 +144,28 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
UserSessionEntity entity = new UserSessionEntity();
entity.setId(id);
-
updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
- tx.putIfAbsent(sessionCache, id, entity);
+ SessionUpdateTask<UserSessionEntity> createSessionTask = new SessionUpdateTask<UserSessionEntity>() {
+
+ @Override
+ public void runUpdate(UserSessionEntity session) {
+
+ }
+
+ @Override
+ public CacheOperation getOperation(UserSessionEntity session) {
+ return CacheOperation.ADD_IF_ABSENT;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
+ return CrossDCMessageStatus.SYNC;
+ }
+
+ };
+
+ sessionTx.addTask(id, createSessionTask, entity);
return wrap(realm, entity, false);
}
@@ -121,31 +188,43 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
+
@Override
public UserSessionModel getUserSession(RealmModel realm, String id) {
return getUserSession(realm, id, false);
}
protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) {
- Cache<String, SessionEntity> cache = getCache(offline);
- UserSessionEntity entity = (UserSessionEntity) tx.get(cache, id); // Chance created in this transaction
-
- if (entity == null) {
- entity = (UserSessionEntity) cache.get(id);
- }
-
+ UserSessionEntity entity = getUserSessionEntity(id, offline);
return wrap(realm, entity, offline);
}
- protected List<UserSessionModel> getUserSessions(RealmModel realm, Predicate<Map.Entry<String, SessionEntity>> predicate, boolean offline) {
- CacheStream<Map.Entry<String, SessionEntity>> cacheStream = getCache(offline).entrySet().stream();
- Iterator<Map.Entry<String, SessionEntity>> itr = cacheStream.filter(predicate).iterator();
- List<UserSessionModel> sessions = new LinkedList<>();
+ private UserSessionEntity getUserSessionEntity(String id, boolean offline) {
+ InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+ SessionEntityWrapper<UserSessionEntity> entityWrapper = tx.get(id);
+ return entityWrapper==null ? null : entityWrapper.getEntity();
+ }
+
+
+ protected List<UserSessionModel> getUserSessions(RealmModel realm, Predicate<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>> predicate, boolean offline) {
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
+
+ cache = CacheDecorators.skipCacheLoaders(cache);
+
+ Stream<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>> cacheStream = cache.entrySet().stream();
+
+ List<UserSessionModel> resultSessions = new LinkedList<>();
+
+ Iterator<UserSessionEntity> itr = cacheStream.filter(predicate)
+ .map(Mappers.userSessionEntity())
+ .iterator();
+
while (itr.hasNext()) {
- UserSessionEntity e = (UserSessionEntity) itr.next().getValue();
- sessions.add(wrap(realm, e, offline));
+ UserSessionEntity userSessionEntity = itr.next();
+ resultSessions.add(wrap(realm, userSessionEntity, offline));
}
- return sessions;
+
+ return resultSessions;
}
@Override
@@ -175,65 +254,107 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
protected List<UserSessionModel> getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) {
- final Cache<String, SessionEntity> cache = getCache(offline);
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
+
+ cache = CacheDecorators.skipCacheLoaders(cache);
Stream<UserSessionEntity> stream = cache.entrySet().stream()
.filter(UserSessionPredicate.create(realm.getId()).client(client.getId()))
.map(Mappers.userSessionEntity())
.sorted(Comparators.userSessionLastSessionRefresh());
- // Doesn't work due to ISPN-6575 . TODO Fix once infinispan upgraded to 8.2.2.Final or 9.0
-// if (firstResult > 0) {
-// stream = stream.skip(firstResult);
-// }
-//
-// if (maxResults > 0) {
-// stream = stream.limit(maxResults);
-// }
-//
-// List<UserSessionEntity> entities = stream.collect(Collectors.toList());
-
-
- // Workaround for ISPN-6575 TODO Fix once infinispan upgraded to 8.2.2.Final or 9.0 and replace with the more effective code above
- if (firstResult < 0) {
- firstResult = 0;
+ if (firstResult > 0) {
+ stream = stream.skip(firstResult);
}
- if (maxResults < 0) {
- maxResults = Integer.MAX_VALUE;
+
+ if (maxResults > 0) {
+ stream = stream.limit(maxResults);
}
- int count = firstResult + maxResults;
- if (count > 0) {
- stream = stream.limit(count);
+ final List<UserSessionModel> sessions = new LinkedList<>();
+ Iterator<UserSessionEntity> itr = stream.iterator();
+
+ while (itr.hasNext()) {
+ UserSessionEntity userSessionEntity = itr.next();
+ sessions.add(wrap(realm, userSessionEntity, offline));
}
- List<UserSessionEntity> entities = stream.collect(Collectors.toList());
- if (firstResult > entities.size()) {
- return Collections.emptyList();
+
+ return sessions;
+ }
+
+
+ @Override
+ public UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline, Predicate<UserSessionModel> predicate) {
+ UserSessionModel userSession = getUserSession(realm, id, offline);
+ if (userSession == null) {
+ return null;
}
- maxResults = Math.min(maxResults, entities.size() - firstResult);
- entities = entities.subList(firstResult, firstResult + maxResults);
+ // We have userSession, which passes predicate. No need for remote lookup.
+ if (predicate.test(userSession)) {
+ log.debugf("getUserSessionWithPredicate(%s): found in local cache", id);
+ return userSession;
+ }
+ // Try lookup userSession from remoteCache
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
+ RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache);
- final List<UserSessionModel> sessions = new LinkedList<>();
- entities.stream().forEach(new Consumer<UserSessionEntity>() {
- @Override
- public void accept(UserSessionEntity userSessionEntity) {
- sessions.add(wrap(realm, userSessionEntity, offline));
+ if (remoteCache != null) {
+ UserSessionEntity remoteSessionEntity = (UserSessionEntity) remoteCache.get(id);
+ if (remoteSessionEntity != null) {
+ log.debugf("getUserSessionWithPredicate(%s): remote cache contains session entity %s", id, remoteSessionEntity);
+
+ UserSessionModel remoteSessionAdapter = wrap(realm, remoteSessionEntity, offline);
+ if (predicate.test(remoteSessionAdapter)) {
+
+ InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+
+ // Remote entity contains our predicate. Update local cache with the remote entity
+ SessionEntityWrapper<UserSessionEntity> sessionWrapper = remoteSessionEntity.mergeRemoteEntityWithLocalEntity(tx.get(id));
+
+ // Replace entity just in ispn cache. Skip remoteStore
+ cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
+ .replace(id, sessionWrapper);
+
+ tx.reloadEntityInCurrentTransaction(realm, id, sessionWrapper);
+
+ // Recursion. We should have it locally now
+ return getUserSessionWithPredicate(realm, id, offline, predicate);
+ } else {
+ log.debugf("getUserSessionWithPredicate(%s): found, but predicate doesn't pass", id);
+
+ return null;
+ }
+ } else {
+ log.debugf("getUserSessionWithPredicate(%s): not found", id);
+
+ // Session not available on remoteCache. Was already removed there. So removing locally too.
+ // TODO: Can be optimized to skip calling remoteCache.remove
+ removeUserSession(realm, userSession);
+
+ return null;
}
- });
+ } else {
- return sessions;
+ log.debugf("getUserSessionWithPredicate(%s): remote cache not available", id);
+
+ return null;
+ }
}
+
@Override
public long getActiveUserSessions(RealmModel realm, ClientModel client) {
return getUserSessionsCount(realm, client, false);
}
protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) {
- return getCache(offline).entrySet().stream()
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
+ cache = CacheDecorators.skipCacheLoaders(cache);
+
+ return cache.entrySet().stream()
.filter(UserSessionPredicate.create(realm.getId()).client(client.getId()))
.count();
}
@@ -242,7 +363,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public void removeUserSession(RealmModel realm, UserSessionModel session) {
UserSessionEntity entity = getUserSessionEntity(session, false);
if (entity != null) {
- removeUserSession(realm, entity, false);
+ removeUserSession(entity, false);
}
}
@@ -252,12 +373,15 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
protected void removeUserSessions(RealmModel realm, UserModel user, boolean offline) {
- Cache<String, SessionEntity> cache = getCache(offline);
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
+
+ cache = CacheDecorators.skipCacheLoaders(cache);
+
+ Iterator<UserSessionEntity> itr = cache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).map(Mappers.userSessionEntity()).iterator();
- Iterator<SessionEntity> itr = cache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).map(Mappers.sessionEntity()).iterator();
while (itr.hasNext()) {
- UserSessionEntity userSessionEntity = (UserSessionEntity) itr.next();
- removeUserSession(realm, userSessionEntity, offline);
+ UserSessionEntity userSessionEntity = itr.next();
+ removeUserSession(userSessionEntity, offline);
}
}
@@ -273,17 +397,30 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout();
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
- Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
- .entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh)).iterator();
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(sessionCache);
- int counter = 0;
- while (itr.hasNext()) {
- counter++;
- UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
- tx.remove(sessionCache, entity.getId());
- }
+ int[] counter = { 0 };
- log.debugf("Removed %d expired user sessions for realm '%s'", counter, realm.getName());
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
+
+ // Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
+ localCacheStoreIgnore
+ .entrySet()
+ .stream()
+ .filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh))
+ .map(Mappers.sessionId())
+ .forEach(new Consumer<String>() {
+
+ @Override
+ public void accept(String sessionId) {
+ counter[0]++;
+ tx.remove(localCache, sessionId);
+ }
+
+ });
+
+
+ log.debugf("Removed %d expired user sessions for realm '%s'", counter[0], realm.getName());
}
private void removeExpiredOfflineUserSessions(RealmModel realm) {
@@ -291,38 +428,72 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(offlineSessionCache);
+
UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline);
- Iterator<Map.Entry<String, SessionEntity>> itr = offlineSessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
- .entrySet().stream().filter(predicate).iterator();
- int counter = 0;
- while (itr.hasNext()) {
- counter++;
- UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
- tx.remove(offlineSessionCache, entity.getId());
+ final int[] counter = { 0 };
- persister.removeUserSession(entity.getId(), true);
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
- for (String clientUUID : entity.getAuthenticatedClientSessions().keySet()) {
- persister.removeClientSession(entity.getId(), clientUUID, true);
- }
- }
+ // Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
+ localCacheStoreIgnore
+ .entrySet()
+ .stream()
+ .filter(predicate)
+ .map(Mappers.userSessionEntity())
+ .forEach(new Consumer<UserSessionEntity>() {
+
+ @Override
+ public void accept(UserSessionEntity userSessionEntity) {
+ counter[0]++;
+ tx.remove(localCache, userSessionEntity.getId());
+
+ // TODO:mposolda can be likely optimized to delete all expired at one step
+ persister.removeUserSession( userSessionEntity.getId(), true);
+
+ // TODO can be likely optimized to delete all at one step
+ for (String clientUUID : userSessionEntity.getAuthenticatedClientSessions().keySet()) {
+ persister.removeClientSession(userSessionEntity.getId(), clientUUID, true);
+ }
+ }
+ });
log.debugf("Removed %d expired offline user sessions for realm '%s'", counter, realm.getName());
}
@Override
public void removeUserSessions(RealmModel realm) {
- removeUserSessions(realm, false);
+ // Don't send message to all DCs, just to all cluster nodes in current DC. The remoteCache will notify client listeners for removed userSessions.
+ clusterEventsSenderTx.addEvent(
+ RemoveUserSessionsEvent.createEvent(RemoveUserSessionsEvent.class, InfinispanUserSessionProviderFactory.REMOVE_USER_SESSIONS_EVENT, session, realm.getId(), true),
+ ClusterProvider.DCNotify.LOCAL_DC_ONLY);
}
- protected void removeUserSessions(RealmModel realm, boolean offline) {
- Cache<String, SessionEntity> cache = getCache(offline);
+ protected void onRemoveUserSessionsEvent(String realmId) {
+ removeLocalUserSessions(realmId, false);
+ }
- Iterator<String> itr = cache.entrySet().stream().filter(SessionPredicate.create(realm.getId())).map(Mappers.sessionId()).iterator();
- while (itr.hasNext()) {
- cache.remove(itr.next());
- }
+ private void removeLocalUserSessions(String realmId, boolean offline) {
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(cache);
+
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
+
+ localCacheStoreIgnore
+ .entrySet()
+ .stream()
+ .filter(SessionPredicate.create(realmId))
+ .map(Mappers.sessionId())
+ .forEach(new Consumer<String>() {
+
+ @Override
+ public void accept(String sessionId) {
+ // Remove session from remoteCache too
+ localCache.remove(sessionId);
+ }
+
+ });
}
@Override
@@ -348,22 +519,53 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public void removeAllUserLoginFailures(RealmModel realm) {
- Iterator<LoginFailureKey> itr = loginFailureCache.entrySet().stream().filter(UserLoginFailurePredicate.create(realm.getId())).map(Mappers.loginFailureId()).iterator();
- while (itr.hasNext()) {
- LoginFailureKey key = itr.next();
- tx.remove(loginFailureCache, key);
- }
+ clusterEventsSenderTx.addEvent(
+ RemoveAllUserLoginFailuresEvent.createEvent(RemoveAllUserLoginFailuresEvent.class, InfinispanUserSessionProviderFactory.REMOVE_ALL_LOGIN_FAILURES_EVENT, session, realm.getId(), true),
+ ClusterProvider.DCNotify.LOCAL_DC_ONLY);
+ }
+
+ protected void onRemoveAllUserLoginFailuresEvent(String realmId) {
+ removeAllLocalUserLoginFailuresEvent(realmId);
+ }
+
+ private void removeAllLocalUserLoginFailuresEvent(String realmId) {
+ Cache<LoginFailureKey, LoginFailureEntity> localCache = CacheDecorators.localCache(loginFailureCache);
+
+ Cache<LoginFailureKey, LoginFailureEntity> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
+
+ localCacheStoreIgnore
+ .entrySet()
+ .stream()
+ .filter(UserLoginFailurePredicate.create(realmId))
+ .map(Mappers.loginFailureId())
+ .forEach(loginFailureKey -> {
+ // Remove loginFailure from remoteCache too
+ localCache.remove(loginFailureKey);
+ });
}
@Override
public void onRealmRemoved(RealmModel realm) {
- removeUserSessions(realm, true);
- removeUserSessions(realm, false);
- removeAllUserLoginFailures(realm);
+ // Don't send message to all DCs, just to all cluster nodes in current DC. The remoteCache will notify client listeners for removed userSessions.
+ clusterEventsSenderTx.addEvent(
+ RealmRemovedSessionEvent.createEvent(RealmRemovedSessionEvent.class, InfinispanUserSessionProviderFactory.REALM_REMOVED_SESSION_EVENT, session, realm.getId(), true),
+ ClusterProvider.DCNotify.LOCAL_DC_ONLY);
+ }
+
+ protected void onRealmRemovedEvent(String realmId) {
+ removeLocalUserSessions(realmId, true);
+ removeLocalUserSessions(realmId, false);
+ removeAllLocalUserLoginFailuresEvent(realmId);
}
@Override
public void onClientRemoved(RealmModel realm, ClientModel client) {
+// clusterEventsSenderTx.addEvent(
+// ClientRemovedSessionEvent.createEvent(ClientRemovedSessionEvent.class, InfinispanUserSessionProviderFactory.CLIENT_REMOVED_SESSION_EVENT, session, realm.getId(), true),
+// ClusterProvider.DCNotify.LOCAL_DC_ONLY);
+ }
+
+ protected void onClientRemovedEvent(String realmId, String clientUuid) {
// Nothing for now. userSession.getAuthenticatedClientSessions() will check lazily if particular client exists and update userSession on-the-fly.
}
@@ -380,10 +582,29 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public void close() {
}
- protected void removeUserSession(RealmModel realm, UserSessionEntity sessionEntity, boolean offline) {
- Cache<String, SessionEntity> cache = getCache(offline);
+ protected void removeUserSession(UserSessionEntity sessionEntity, boolean offline) {
+ InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
- tx.remove(cache, sessionEntity.getId());
+ SessionUpdateTask<UserSessionEntity> removeTask = new SessionUpdateTask<UserSessionEntity>() {
+
+ @Override
+ public void runUpdate(UserSessionEntity entity) {
+
+ }
+
+ @Override
+ public CacheOperation getOperation(UserSessionEntity entity) {
+ return CacheOperation.REMOVE;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
+ return CrossDCMessageStatus.SYNC;
+ }
+
+ };
+
+ tx.addTask(sessionEntity.getId(), removeTask);
}
InfinispanKeycloakTransaction getTx() {
@@ -391,16 +612,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) {
- Cache<String, SessionEntity> cache = getCache(offline);
- return entity != null ? new UserSessionAdapter(session, this, cache, realm, entity, offline) : null;
- }
-
- List<UserSessionModel> wrapUserSessions(RealmModel realm, Collection<UserSessionEntity> entities, boolean offline) {
- List<UserSessionModel> models = new LinkedList<>();
- for (UserSessionEntity e : entities) {
- models.add(wrap(realm, e, offline));
- }
- return models;
+ InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+ return entity != null ? new UserSessionAdapter(session, this, tx, realm, entity, offline) : null;
}
UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
@@ -411,8 +624,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
if (userSession instanceof UserSessionAdapter) {
return ((UserSessionAdapter) userSession).getEntity();
} else {
- Cache<String, SessionEntity> cache = getCache(offline);
- return cache != null ? (UserSessionEntity) cache.get(userSession.getId()) : null;
+ return getUserSessionEntity(userSession.getId(), offline);
}
}
@@ -438,7 +650,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public void removeOfflineUserSession(RealmModel realm, UserSessionModel userSession) {
UserSessionEntity userSessionEntity = getUserSessionEntity(userSession, true);
if (userSessionEntity != null) {
- removeUserSession(realm, userSessionEntity, true);
+ removeUserSession(userSessionEntity, true);
}
}
@@ -449,7 +661,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
UserSessionAdapter userSessionAdapter = (offlineUserSession instanceof UserSessionAdapter) ? (UserSessionAdapter) offlineUserSession :
getOfflineUserSession(offlineUserSession.getRealm(), offlineUserSession.getId());
- AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession);
+ AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, getTransaction(true));
// update timestamp to current time
offlineClientSession.setTimestamp(Time.currentTime());
@@ -459,12 +671,18 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public List<UserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user) {
- Iterator<Map.Entry<String, SessionEntity>> itr = offlineSessionCache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).iterator();
List<UserSessionModel> userSessions = new LinkedList<>();
- while(itr.hasNext()) {
- UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
- UserSessionModel userSession = wrap(realm, entity, true);
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = CacheDecorators.skipCacheLoaders(offlineSessionCache);
+
+ Iterator<UserSessionEntity> itr = cache.entrySet().stream()
+ .filter(UserSessionPredicate.create(realm.getId()).user(user.getId()))
+ .map(Mappers.userSessionEntity())
+ .iterator();
+
+ while (itr.hasNext()) {
+ UserSessionEntity userSessionEntity = itr.next();
+ UserSessionModel userSession = wrap(realm, userSessionEntity, true);
userSessions.add(userSession);
}
@@ -492,7 +710,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
entity.setBrokerUserId(userSession.getBrokerUserId());
entity.setIpAddress(userSession.getIpAddress());
entity.setLoginUsername(userSession.getLoginUsername());
- entity.setNotes(userSession.getNotes()== null ? new ConcurrentHashMap<>() : userSession.getNotes());
+ entity.setNotes(userSession.getNotes() == null ? new ConcurrentHashMap<>() : userSession.getNotes());
entity.setAuthenticatedClientSessions(new ConcurrentHashMap<>());
entity.setRememberMe(userSession.isRememberMe());
entity.setState(userSession.getState());
@@ -502,14 +720,34 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
- Cache<String, SessionEntity> cache = getCache(offline);
- tx.put(cache, userSession.getId(), entity);
+ InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+
+ SessionUpdateTask importTask = new SessionUpdateTask<UserSessionEntity>() {
+
+ @Override
+ public void runUpdate(UserSessionEntity session) {
+
+ }
+
+ @Override
+ public CacheOperation getOperation(UserSessionEntity session) {
+ return CacheOperation.ADD_IF_ABSENT;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
+ return CrossDCMessageStatus.SYNC;
+ }
+
+ };
+ tx.addTask(userSession.getId(), importTask, entity);
+
UserSessionAdapter importedSession = wrap(userSession.getRealm(), entity, offline);
// Handle client sessions
if (importAuthenticatedClientSessions) {
for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
- importClientSession(importedSession, clientSession);
+ importClientSession(importedSession, clientSession, tx);
}
}
@@ -517,25 +755,46 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
- private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter importedUserSession, AuthenticatedClientSessionModel clientSession) {
+ private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter importedUserSession, AuthenticatedClientSessionModel clientSession,
+ InfinispanChangelogBasedTransaction<UserSessionEntity> updateTx) {
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
entity.setAction(clientSession.getAction());
entity.setAuthMethod(clientSession.getProtocol());
- entity.setNotes(clientSession.getNotes());
+ entity.setNotes(clientSession.getNotes() == null ? new ConcurrentHashMap<>() : clientSession.getNotes());
entity.setProtocolMappers(clientSession.getProtocolMappers());
entity.setRedirectUri(clientSession.getRedirectUri());
entity.setRoles(clientSession.getRoles());
entity.setTimestamp(clientSession.getTimestamp());
+
Map<String, AuthenticatedClientSessionEntity> clientSessions = importedUserSession.getEntity().getAuthenticatedClientSessions();
clientSessions.put(clientSession.getClient().getId(), entity);
- importedUserSession.update();
+ SessionUpdateTask importTask = new SessionUpdateTask<UserSessionEntity>() {
+
+ @Override
+ public void runUpdate(UserSessionEntity session) {
+ Map<String, AuthenticatedClientSessionEntity> clientSessions = session.getAuthenticatedClientSessions();
+ clientSessions.put(clientSession.getClient().getId(), entity);
+ }
+
+ @Override
+ public CacheOperation getOperation(UserSessionEntity session) {
+ return CacheOperation.REPLACE;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
+ return CrossDCMessageStatus.SYNC;
+ }
+
+ };
+ updateTx.addTask(importedUserSession.getId(), importTask);
- return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, importedUserSession.getCache());
+ return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, updateTx);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
index 663a4b2..110a812 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
@@ -18,41 +18,76 @@
package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.persistence.remote.RemoteStore;
import org.jboss.logging.Logger;
import org.keycloak.Config;
+import org.keycloak.cluster.ClusterProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UserSessionProviderFactory;
+import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStore;
+import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStoreFactory;
+import org.keycloak.models.sessions.infinispan.initializer.BaseCacheInitializer;
+import org.keycloak.models.sessions.infinispan.initializer.CacheInitializer;
+import org.keycloak.models.sessions.infinispan.initializer.DBLockBasedCacheInitializer;
+import org.keycloak.models.sessions.infinispan.initializer.SingleWorkerCacheInitializer;
+import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
-import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
-import org.keycloak.models.sessions.infinispan.initializer.InfinispanUserSessionInitializer;
-import org.keycloak.models.sessions.infinispan.initializer.OfflineUserSessionLoader;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import org.keycloak.models.sessions.infinispan.events.AbstractUserSessionClusterListener;
+import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
+import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
+import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
+import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
+import org.keycloak.models.sessions.infinispan.initializer.InfinispanCacheInitializer;
+import org.keycloak.models.sessions.infinispan.initializer.OfflinePersistentUserSessionLoader;
+import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionListener;
+import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionsLoader;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.provider.ProviderEventListener;
import java.io.Serializable;
+import java.util.Set;
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class);
+ public static final String PROVIDER_ID = "infinispan";
+
+ public static final String REALM_REMOVED_SESSION_EVENT = "REALM_REMOVED_EVENT_SESSIONS";
+
+ public static final String CLIENT_REMOVED_SESSION_EVENT = "CLIENT_REMOVED_SESSION_SESSIONS";
+
+ public static final String REMOVE_USER_SESSIONS_EVENT = "REMOVE_USER_SESSIONS_EVENT";
+
+ public static final String REMOVE_ALL_LOGIN_FAILURES_EVENT = "REMOVE_ALL_LOGIN_FAILURES_EVENT";
+
private Config.Scope config;
+ private RemoteCacheInvoker remoteCacheInvoker;
+ private LastSessionRefreshStore lastSessionRefreshStore;
+ private LastSessionRefreshStore offlineLastSessionRefreshStore;
+
@Override
public InfinispanUserSessionProvider create(KeycloakSession session) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
- Cache<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
- Cache<String, SessionEntity> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
Cache<LoginFailureKey, LoginFailureEntity> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
- return new InfinispanUserSessionProvider(session, cache, offlineSessionsCache, loginFailures);
+ return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore, cache, offlineSessionsCache, loginFailures);
}
@Override
@@ -62,18 +97,19 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
@Override
public void postInit(final KeycloakSessionFactory factory) {
- // Max count of worker errors. Initialization will end with exception when this number is reached
- final int maxErrors = config.getInt("maxErrors", 20);
-
- // Count of sessions to be computed in each segment
- final int sessionsPerSegment = config.getInt("sessionsPerSegment", 100);
factory.register(new ProviderEventListener() {
@Override
public void onEvent(ProviderEvent event) {
if (event instanceof PostMigrationEvent) {
- loadPersistentSessions(factory, maxErrors, sessionsPerSegment);
+ KeycloakSession session = ((PostMigrationEvent) event).getSession();
+
+ checkRemoteCaches(session);
+ loadPersistentSessions(factory, getMaxErrors(), getSessionsPerSegment());
+ registerClusterListeners(session);
+ loadSessionsFromRemoteCaches(session);
+
} else if (event instanceof UserModel.UserRemovedEvent) {
UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event;
@@ -84,35 +120,169 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
});
}
+ // Max count of worker errors. Initialization will end with exception when this number is reached
+ private int getMaxErrors() {
+ return config.getInt("maxErrors", 20);
+ }
+
+ // Count of sessions to be computed in each segment
+ private int getSessionsPerSegment() {
+ return config.getInt("sessionsPerSegment", 100);
+ }
+
@Override
public void loadPersistentSessions(final KeycloakSessionFactory sessionFactory, final int maxErrors, final int sessionsPerSegment) {
- log.debug("Start pre-loading userSessions and clientSessions from persistent storage");
+ log.debug("Start pre-loading userSessions from persistent storage");
+
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
+ Cache<String, Serializable> workCache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+
+ InfinispanCacheInitializer ispnInitializer = new InfinispanCacheInitializer(sessionFactory, workCache, new OfflinePersistentUserSessionLoader(), "offlineUserSessions", sessionsPerSegment, maxErrors);
+
+ // DB-lock to ensure that persistent sessions are loaded from DB just on one DC. The other DCs will load them from remote cache.
+ CacheInitializer initializer = new DBLockBasedCacheInitializer(session, ispnInitializer);
+
+ initializer.initCache();
+ initializer.loadSessions();
+ }
+
+ });
+
+ log.debug("Pre-loading userSessions from persistent storage finished");
+ }
+
+
+ protected void registerClusterListeners(KeycloakSession session) {
+ KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+ ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+
+ cluster.registerListener(REALM_REMOVED_SESSION_EVENT, new AbstractUserSessionClusterListener<RealmRemovedSessionEvent>(sessionFactory) {
+
+ @Override
+ protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RealmRemovedSessionEvent sessionEvent) {
+ provider.onRealmRemovedEvent(sessionEvent.getRealmId());
+ }
+
+ });
+
+ cluster.registerListener(CLIENT_REMOVED_SESSION_EVENT, new AbstractUserSessionClusterListener<ClientRemovedSessionEvent>(sessionFactory) {
+
+ @Override
+ protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, ClientRemovedSessionEvent sessionEvent) {
+ provider.onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid());
+ }
+
+ });
+
+ cluster.registerListener(REMOVE_USER_SESSIONS_EVENT, new AbstractUserSessionClusterListener<RemoveUserSessionsEvent>(sessionFactory) {
+
+ @Override
+ protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RemoveUserSessionsEvent sessionEvent) {
+ provider.onRemoveUserSessionsEvent(sessionEvent.getRealmId());
+ }
+
+ });
+
+ cluster.registerListener(REMOVE_ALL_LOGIN_FAILURES_EVENT, new AbstractUserSessionClusterListener<RemoveAllUserLoginFailuresEvent>(sessionFactory) {
+
+ @Override
+ protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RemoveAllUserLoginFailuresEvent sessionEvent) {
+ provider.onRemoveAllUserLoginFailuresEvent(sessionEvent.getRealmId());
+ }
+
+ });
+
+ log.debug("Registered cluster listeners");
+ }
+
+
+ protected void checkRemoteCaches(KeycloakSession session) {
+ this.remoteCacheInvoker = new RemoteCacheInvoker();
+
+ InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class);
+
+ Cache sessionsCache = ispn.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+ boolean sessionsRemoteCache = checkRemoteCache(session, sessionsCache, (RealmModel realm) -> {
+ return realm.getSsoSessionIdleTimeout() * 1000;
+ });
+
+ if (sessionsRemoteCache) {
+ lastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, sessionsCache, false);
+ }
+
+
+ Cache offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
+ boolean offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> {
+ return realm.getOfflineSessionIdleTimeout() * 1000;
+ });
+
+ if (offlineSessionsRemoteCache) {
+ offlineLastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true);
+ }
+ }
+
+ private boolean checkRemoteCache(KeycloakSession session, Cache ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader) {
+ Set<RemoteStore> remoteStores = InfinispanUtil.getRemoteStores(ispnCache);
+
+ if (remoteStores.isEmpty()) {
+ log.debugf("No remote store configured for cache '%s'", ispnCache.getName());
+ return false;
+ } else {
+ log.infof("Remote store configured for cache '%s'", ispnCache.getName());
+
+ RemoteCache remoteCache = remoteStores.iterator().next().getRemoteCache();
+
+ remoteCacheInvoker.addRemoteCache(ispnCache.getName(), remoteCache, maxIdleLoader);
+
+ RemoteCacheSessionListener hotrodListener = RemoteCacheSessionListener.createListener(session, ispnCache, remoteCache);
+ remoteCache.addClientListener(hotrodListener);
+ return true;
+ }
+ }
+
+
+ private void loadSessionsFromRemoteCaches(KeycloakSession session) {
+ for (String cacheName : remoteCacheInvoker.getRemoteCacheNames()) {
+ loadSessionsFromRemoteCache(session.getKeycloakSessionFactory(), cacheName, getMaxErrors());
+ }
+ }
+
+
+ private void loadSessionsFromRemoteCache(final KeycloakSessionFactory sessionFactory, String cacheName, final int maxErrors) {
+ log.debugf("Check pre-loading userSessions from remote cache '%s'", cacheName);
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
- Cache<String, Serializable> cache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+ Cache<String, Serializable> workCache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+
+ // Use limit for sessionsPerSegment as RemoteCache bulk load doesn't have support for pagination :/
+ BaseCacheInitializer initializer = new SingleWorkerCacheInitializer(session, workCache, new RemoteCacheSessionsLoader(cacheName), "remoteCacheLoad::" + cacheName);
- InfinispanUserSessionInitializer initializer = new InfinispanUserSessionInitializer(sessionFactory, cache, new OfflineUserSessionLoader(), maxErrors, sessionsPerSegment, "offlineUserSessions");
initializer.initCache();
- initializer.loadPersistentSessions();
+ initializer.loadSessions();
}
});
- log.debug("Pre-loading userSessions and clientSessions from persistent storage finished");
+ log.debugf("Pre-loading userSessions from remote cache '%s' finished", cacheName);
}
+
@Override
public void close() {
}
@Override
public String getId() {
- return "infinispan";
+ return PROVIDER_ID;
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/BaseCacheInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/BaseCacheInitializer.java
new file mode 100644
index 0000000..43788d0
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/BaseCacheInitializer.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.initializer;
+
+import java.io.Serializable;
+
+import org.infinispan.Cache;
+import org.infinispan.context.Flag;
+import org.infinispan.lifecycle.ComponentStatus;
+import org.infinispan.remoting.transport.Transport;
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class BaseCacheInitializer extends CacheInitializer {
+
+ private static final String STATE_KEY_PREFIX = "distributed::";
+
+ private static final Logger log = Logger.getLogger(BaseCacheInitializer.class);
+
+ protected final KeycloakSessionFactory sessionFactory;
+ protected final Cache<String, Serializable> workCache;
+ protected final SessionLoader sessionLoader;
+ protected final int sessionsPerSegment;
+ protected final String stateKey;
+
+ public BaseCacheInitializer(KeycloakSessionFactory sessionFactory, Cache<String, Serializable> workCache, SessionLoader sessionLoader, String stateKeySuffix, int sessionsPerSegment) {
+ this.sessionFactory = sessionFactory;
+ this.workCache = workCache;
+ this.sessionLoader = sessionLoader;
+ this.sessionsPerSegment = sessionsPerSegment;
+ this.stateKey = STATE_KEY_PREFIX + stateKeySuffix;
+ }
+
+
+ @Override
+ protected boolean isFinished() {
+ // Check if we should skipLoadingSessions. This can happen if someone else already did the task (For example in cross-dc environment, it was done by different DC)
+ boolean isFinishedAlready = this.sessionLoader.isFinished(this);
+ if (isFinishedAlready) {
+ return true;
+ }
+
+ InitializerState state = getStateFromCache();
+ return state != null && state.isFinished();
+ }
+
+
+ @Override
+ protected boolean isCoordinator() {
+ Transport transport = workCache.getCacheManager().getTransport();
+ return transport == null || transport.isCoordinator();
+ }
+
+
+ protected InitializerState getOrCreateInitializerState() {
+ InitializerState state = getStateFromCache();
+ if (state == null) {
+ final int[] count = new int[1];
+
+ // Rather use separate transactions for update and counting
+
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+ @Override
+ public void run(KeycloakSession session) {
+ sessionLoader.init(session);
+ }
+
+ });
+
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+ @Override
+ public void run(KeycloakSession session) {
+ count[0] = sessionLoader.getSessionsCount(session);
+ }
+
+ });
+
+ state = new InitializerState();
+ state.init(count[0], sessionsPerSegment);
+ saveStateToCache(state);
+ }
+ return state;
+
+ }
+
+
+ private InitializerState getStateFromCache() {
+ // We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
+ return (InitializerState) workCache.getAdvancedCache()
+ .withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
+ .get(stateKey);
+ }
+
+
+ protected void saveStateToCache(final InitializerState state) {
+
+ // 3 attempts to send the message (it may fail if some node fails in the meantime)
+ retry(3, new Runnable() {
+
+ @Override
+ public void run() {
+
+ // Save this synchronously to ensure all nodes read correct state
+ // We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
+ BaseCacheInitializer.this.workCache.getAdvancedCache().
+ withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS, Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
+ .put(stateKey, state);
+ }
+
+ });
+ }
+
+
+ private void retry(int retry, Runnable runnable) {
+ while (true) {
+ try {
+ runnable.run();
+ return;
+ } catch (RuntimeException e) {
+ ComponentStatus status = workCache.getStatus();
+ if (status.isStopping() || status.isTerminated()) {
+ log.warn("Failed to put initializerState to the cache. Cache is already terminating");
+ log.debug(e.getMessage(), e);
+ return;
+ }
+ retry--;
+ if (retry == 0) {
+ throw e;
+ }
+ }
+ }
+ }
+
+
+ public Cache<String, Serializable> getWorkCache() {
+ return workCache;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/CacheInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/CacheInitializer.java
new file mode 100644
index 0000000..1932709
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/CacheInitializer.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.initializer;
+
+import org.jboss.logging.Logger;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class CacheInitializer {
+
+ private static final Logger log = Logger.getLogger(CacheInitializer.class);
+
+ public void initCache() {
+ }
+
+ public void loadSessions() {
+ while (!isFinished()) {
+ if (!isCoordinator()) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException ie) {
+ log.error("Interrupted", ie);
+ }
+ } else {
+ startLoading();
+ }
+ }
+ }
+
+
+ protected abstract boolean isFinished();
+
+ protected abstract boolean isCoordinator();
+
+ /**
+ * Just coordinator will run this
+ */
+ protected abstract void startLoading();
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/DBLockBasedCacheInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/DBLockBasedCacheInitializer.java
new file mode 100644
index 0000000..ecc8c78
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/DBLockBasedCacheInitializer.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.initializer;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.dblock.DBLockManager;
+import org.keycloak.models.dblock.DBLockProvider;
+
+/**
+ * Encapsulates preloading of sessions within the DB Lock. This DB-aware lock ensures that "startLoading" is done on single DC and the other DCs need to wait.
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class DBLockBasedCacheInitializer extends CacheInitializer {
+
+ private static final Logger log = Logger.getLogger(DBLockBasedCacheInitializer.class);
+
+ private final KeycloakSession session;
+ private final CacheInitializer delegate;
+
+ public DBLockBasedCacheInitializer(KeycloakSession session, CacheInitializer delegate) {
+ this.session = session;
+ this.delegate = delegate;
+ }
+
+
+ @Override
+ public void initCache() {
+ delegate.initCache();
+ }
+
+
+ @Override
+ protected boolean isFinished() {
+ return delegate.isFinished();
+ }
+
+
+ @Override
+ protected boolean isCoordinator() {
+ return delegate.isCoordinator();
+ }
+
+
+ /**
+ * Just coordinator will run this. And there is DB-lock, so the delegate.startLoading() will be permitted just by the single DC
+ */
+ @Override
+ protected void startLoading() {
+ DBLockManager dbLockManager = new DBLockManager(session);
+ dbLockManager.checkForcedUnlock();
+ DBLockProvider dbLock = dbLockManager.getDBLock();
+ dbLock.waitForLock();
+ try {
+
+ if (isFinished()) {
+ log.infof("Task already finished when DBLock retrieved");
+ } else {
+ delegate.startLoading();
+ }
+ } finally {
+ dbLock.releaseLock();
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java
index 4b04d9b..ec647af 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java
@@ -31,7 +31,7 @@ import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
-public class SessionInitializerWorker implements DistributedCallable<String, Serializable, InfinispanUserSessionInitializer.WorkerResult>, Serializable {
+public class SessionInitializerWorker implements DistributedCallable<String, Serializable, InfinispanCacheInitializer.WorkerResult>, Serializable {
private static final Logger log = Logger.getLogger(SessionInitializerWorker.class);
@@ -53,7 +53,7 @@ public class SessionInitializerWorker implements DistributedCallable<String, Ser
}
@Override
- public InfinispanUserSessionInitializer.WorkerResult call() throws Exception {
+ public InfinispanCacheInitializer.WorkerResult call() throws Exception {
if (log.isTraceEnabled()) {
log.tracef("Running computation for segment: %d", segment);
}
@@ -61,7 +61,7 @@ public class SessionInitializerWorker implements DistributedCallable<String, Ser
KeycloakSessionFactory sessionFactory = workCache.getAdvancedCache().getComponentRegistry().getComponent(KeycloakSessionFactory.class);
if (sessionFactory == null) {
log.debugf("KeycloakSessionFactory not yet set in cache. Worker skipped");
- return InfinispanUserSessionInitializer.WorkerResult.create(segment, false);
+ return InfinispanCacheInitializer.WorkerResult.create(segment, false);
}
final int first = segment * sessionsPerSegment;
@@ -76,7 +76,7 @@ public class SessionInitializerWorker implements DistributedCallable<String, Ser
});
- return InfinispanUserSessionInitializer.WorkerResult.create(segment, true);
+ return InfinispanCacheInitializer.WorkerResult.create(segment, true);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java
index 55c6323..49f1b14 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java
@@ -24,11 +24,51 @@ import java.io.Serializable;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
-public interface SessionLoader extends Serializable {
+public interface SessionLoader {
+ /**
+ * Will be triggered just once on cluster coordinator node to perform some generic initialization tasks (Eg. update DB before starting load).
+ *
+ * NOTE: This shouldn't be used for the initialization of loader instance itself!
+ *
+ * @param session
+ */
void init(KeycloakSession session);
+
+ /**
+ * Will be triggered just once on cluster coordinator node to count the number of sessions
+ *
+ * @param session
+ * @return
+ */
int getSessionsCount(KeycloakSession session);
+
+ /**
+ * Will be called on all cluster nodes to load the specified page.
+ *
+ * @param session
+ * @param first
+ * @param max
+ * @return
+ */
boolean loadSessions(KeycloakSession session, int first, int max);
+
+
+ /**
+ * This will be called on nodes to check if loading is finished. It allows loader to notify that loading is finished for some reason.
+ *
+ * @param initializer
+ * @return
+ */
+ boolean isFinished(BaseCacheInitializer initializer);
+
+
+ /**
+ * Callback triggered on cluster coordinator once it recognize that all sessions were successfully loaded
+ *
+ * @param initializer
+ */
+ void afterAllSessionsLoaded(BaseCacheInitializer initializer);
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SingleWorkerCacheInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SingleWorkerCacheInitializer.java
new file mode 100644
index 0000000..a60b4b9
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SingleWorkerCacheInitializer.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.initializer;
+
+import java.io.Serializable;
+
+import org.infinispan.Cache;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * This impl is able to run the non-paginatable loader task and hence will be executed just on single node.
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class SingleWorkerCacheInitializer extends BaseCacheInitializer {
+
+ private final KeycloakSession session;
+
+ public SingleWorkerCacheInitializer(KeycloakSession session, Cache<String, Serializable> workCache, SessionLoader sessionLoader, String stateKeySuffix) {
+ super(session.getKeycloakSessionFactory(), workCache, sessionLoader, stateKeySuffix, Integer.MAX_VALUE);
+ this.session = session;
+ }
+
+ @Override
+ protected void startLoading() {
+ InitializerState state = getOrCreateInitializerState();
+ while (!state.isFinished()) {
+ sessionLoader.loadSessions(session, -1, -1);
+ state.markSegmentFinished(0);
+ saveStateToCache(state);
+ }
+
+ // Loader callback after the task is finished
+ this.sessionLoader.afterAllSessionsLoaded(this);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStore.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStore.java
new file mode 100644
index 0000000..ba413ae
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStore.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.remotestore;
+
+import java.util.concurrent.Executor;
+
+import org.infinispan.client.hotrod.Flag;
+import org.infinispan.commons.configuration.ConfiguredBy;
+import org.infinispan.filter.KeyFilter;
+import org.infinispan.marshall.core.MarshalledEntry;
+import org.infinispan.metadata.InternalMetadata;
+import org.infinispan.persistence.remote.RemoteStore;
+import org.infinispan.persistence.spi.PersistenceException;
+import org.jboss.logging.Logger;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@ConfiguredBy(KcRemoteStoreConfiguration.class)
+public class KcRemoteStore extends RemoteStore {
+
+ protected static final Logger logger = Logger.getLogger(KcRemoteStore.class);
+
+ private String cacheName;
+
+ @Override
+ public void start() throws PersistenceException {
+ super.start();
+ if (getRemoteCache() == null) {
+ String cacheName = getConfiguration().remoteCacheName();
+ throw new IllegalStateException("Remote cache '" + cacheName + "' is not available.");
+ }
+ this.cacheName = getRemoteCache().getName();
+ }
+
+ @Override
+ public MarshalledEntry load(Object key) throws PersistenceException {
+ logger.debugf("Calling load: '%s' for remote cache '%s'", key, cacheName);
+
+ MarshalledEntry entry = super.load(key);
+ if (entry == null) {
+ return null;
+ }
+
+ // wrap remote entity
+ SessionEntity entity = (SessionEntity) entry.getValue();
+ SessionEntityWrapper entityWrapper = new SessionEntityWrapper(entity);
+
+ MarshalledEntry wrappedEntry = marshalledEntry(entry.getKey(), entityWrapper, entry.getMetadata());
+
+ logger.debugf("Found entry in load: %s", wrappedEntry.toString());
+
+ return wrappedEntry;
+ }
+
+
+ // Don't do anything. Iterate over remoteCache.keySet() can have big performance impact. We handle bulk load by ourselves if needed.
+ @Override
+ public void process(KeyFilter filter, CacheLoaderTask task, Executor executor, boolean fetchValue, boolean fetchMetadata) {
+ logger.debugf("Skip calling process with filter '%s' on cache '%s'", filter, cacheName);
+ // super.process(filter, task, executor, fetchValue, fetchMetadata);
+ }
+
+
+ // Don't do anything. Writes handled by KC itself as we need more flexibility
+ @Override
+ public void write(MarshalledEntry entry) throws PersistenceException {
+ }
+
+
+ @Override
+ public boolean delete(Object key) throws PersistenceException {
+ logger.debugf("Calling delete for key '%s' on cache '%s'", key, cacheName);
+
+ // Optimization - we don't need to know the previous value.
+ // TODO: For some usecases (bulk removal of user sessions), it may be better for performance to call removeAsync and wait for all futures to be finished
+ getRemoteCache().remove(key);
+
+ return true;
+ }
+
+ protected MarshalledEntry marshalledEntry(Object key, Object value, InternalMetadata metadata) {
+ return ctx.getMarshalledEntryFactory().newMarshalledEntry(key, value, metadata);
+ }
+
+
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfiguration.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfiguration.java
new file mode 100644
index 0000000..d786872
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfiguration.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.remotestore;
+
+import org.infinispan.commons.configuration.BuiltBy;
+import org.infinispan.commons.configuration.ConfigurationFor;
+import org.infinispan.commons.configuration.attributes.AttributeSet;
+import org.infinispan.configuration.cache.AsyncStoreConfiguration;
+import org.infinispan.configuration.cache.SingletonStoreConfiguration;
+import org.infinispan.persistence.remote.configuration.ConnectionPoolConfiguration;
+import org.infinispan.persistence.remote.configuration.ExecutorFactoryConfiguration;
+import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@BuiltBy(KcRemoteStoreConfigurationBuilder.class)
+@ConfigurationFor(KcRemoteStore.class)
+public class KcRemoteStoreConfiguration extends RemoteStoreConfiguration {
+
+ public KcRemoteStoreConfiguration(AttributeSet attributes, AsyncStoreConfiguration async, SingletonStoreConfiguration singletonStore,
+ ExecutorFactoryConfiguration asyncExecutorFactory, ConnectionPoolConfiguration connectionPool) {
+ super(attributes, async, singletonStore, asyncExecutorFactory, connectionPool);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfigurationBuilder.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfigurationBuilder.java
new file mode 100644
index 0000000..9e99967
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfigurationBuilder.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.remotestore;
+
+import org.infinispan.configuration.cache.PersistenceConfigurationBuilder;
+import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration;
+import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class KcRemoteStoreConfigurationBuilder extends RemoteStoreConfigurationBuilder {
+
+ public KcRemoteStoreConfigurationBuilder(PersistenceConfigurationBuilder builder) {
+ super(builder);
+ }
+
+ @Override
+ public KcRemoteStoreConfiguration create() {
+ RemoteStoreConfiguration cfg = super.create();
+ KcRemoteStoreConfiguration cfg2 = new KcRemoteStoreConfiguration(cfg.attributes(), cfg.async(), cfg.singletonStore(), cfg.asyncExecutorFactory(), cfg.connectionPool());
+ return cfg2;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
new file mode 100644
index 0000000..89fd215
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.remotestore;
+
+import org.keycloak.common.util.Time;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.infinispan.client.hotrod.Flag;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.VersionedValue;
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RemoteCacheInvoker {
+
+ public static final Logger logger = Logger.getLogger(RemoteCacheInvoker.class);
+
+ private final Map<String, RemoteCacheContext> remoteCaches = new HashMap<>();
+
+
+ public void addRemoteCache(String cacheName, RemoteCache remoteCache, MaxIdleTimeLoader maxIdleLoader) {
+ RemoteCacheContext ctx = new RemoteCacheContext(remoteCache, maxIdleLoader);
+ remoteCaches.put(cacheName, ctx);
+ }
+
+ public Set<String> getRemoteCacheNames() {
+ return Collections.unmodifiableSet(remoteCaches.keySet());
+ }
+
+
+ public <S extends SessionEntity> void runTask(KeycloakSession kcSession, RealmModel realm, String cacheName, String key, SessionUpdateTask<S> task, SessionEntityWrapper<S> sessionWrapper) {
+ RemoteCacheContext context = remoteCaches.get(cacheName);
+ if (context == null) {
+ return;
+ }
+
+ S session = sessionWrapper.getEntity();
+
+ SessionUpdateTask.CacheOperation operation = task.getOperation(session);
+ SessionUpdateTask.CrossDCMessageStatus status = task.getCrossDCMessageStatus(sessionWrapper);
+
+ if (status == SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED) {
+ logger.debugf("Skip writing to remoteCache for entity '%s' of cache '%s' and operation '%s'", key, cacheName, operation);
+ return;
+ }
+
+ long maxIdleTimeMs = context.maxIdleTimeLoader.getMaxIdleTimeMs(realm);
+
+ // Double the timeout to ensure that entry won't expire on remoteCache in case that write of some entities to remoteCache is postponed (eg. userSession.lastSessionRefresh)
+ maxIdleTimeMs = maxIdleTimeMs * 2;
+
+ logger.debugf("Running task '%s' on remote cache '%s' . Key is '%s'", operation, cacheName, key);
+
+ runOnRemoteCache(context.remoteCache, maxIdleTimeMs, key, task, sessionWrapper);
+ }
+
+
+ private <S extends SessionEntity> void runOnRemoteCache(RemoteCache remoteCache, long maxIdleMs, String key, SessionUpdateTask<S> task, SessionEntityWrapper<S> sessionWrapper) {
+ S session = sessionWrapper.getEntity();
+ SessionUpdateTask.CacheOperation operation = task.getOperation(session);
+
+ switch (operation) {
+ case REMOVE:
+ // REMOVE already handled at remote cache store level
+ //remoteCache.remove(key);
+ break;
+ case ADD:
+ remoteCache.put(key, session, task.getLifespanMs(), TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
+ break;
+ case ADD_IF_ABSENT:
+ final int currentTime = Time.currentTime();
+ SessionEntity existing = (SessionEntity) remoteCache
+ .withFlags(Flag.FORCE_RETURN_VALUE)
+ .putIfAbsent(key, session, -1, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
+ if (existing != null) {
+ throw new IllegalStateException("There is already existing value in cache for key " + key);
+ }
+ sessionWrapper.putLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE, currentTime);
+ break;
+ case REPLACE:
+ replace(remoteCache, task.getLifespanMs(), maxIdleMs, key, task);
+ break;
+ default:
+ throw new IllegalStateException("Unsupported state " + operation);
+ }
+ }
+
+
+ private <S extends SessionEntity> void replace(RemoteCache remoteCache, long lifespanMs, long maxIdleMs, String key, SessionUpdateTask<S> task) {
+ boolean replaced = false;
+ while (!replaced) {
+ VersionedValue<S> versioned = remoteCache.getVersioned(key);
+ if (versioned == null) {
+ logger.warnf("Not found entity to replace for key '%s'", key);
+ return;
+ }
+
+ S session = versioned.getValue();
+
+ // Run task on the remote session
+ task.runUpdate(session);
+
+ logger.debugf("Before replaceWithVersion. Entity to write version %d: %s", versioned.getVersion(), session);
+
+ replaced = remoteCache.replaceWithVersion(key, session, versioned.getVersion(), lifespanMs, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
+
+ if (!replaced) {
+ logger.debugf("Failed to replace entity '%s' version %d. Will retry again", key, versioned.getVersion());
+ } else {
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Replaced entity version %d in remote cache: %s", versioned.getVersion(), session);
+ }
+ }
+ }
+ }
+
+
+ private class RemoteCacheContext {
+
+ private final RemoteCache remoteCache;
+ private final MaxIdleTimeLoader maxIdleTimeLoader;
+
+ public RemoteCacheContext(RemoteCache remoteCache, MaxIdleTimeLoader maxIdleLoader) {
+ this.remoteCache = remoteCache;
+ this.maxIdleTimeLoader = maxIdleLoader;
+ }
+
+ }
+
+
+ @FunctionalInterface
+ public interface MaxIdleTimeLoader {
+
+ long getMaxIdleTimeMs(RealmModel realm);
+
+ }
+
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java
new file mode 100644
index 0000000..d29e220
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.remotestore;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved;
+import org.infinispan.client.hotrod.annotation.ClientCacheFailover;
+import org.infinispan.client.hotrod.annotation.ClientListener;
+import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheFailoverEvent;
+import org.infinispan.client.hotrod.event.ClientEvent;
+import org.infinispan.context.Flag;
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+import java.util.Random;
+import java.util.logging.Level;
+import org.infinispan.client.hotrod.VersionedValue;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@ClientListener
+public class RemoteCacheSessionListener {
+
+ protected static final Logger logger = Logger.getLogger(RemoteCacheSessionListener.class);
+
+ private Cache<String, SessionEntityWrapper> cache;
+ private RemoteCache remoteCache;
+ private boolean distributed;
+ private String myAddress;
+
+
+ protected RemoteCacheSessionListener() {
+ }
+
+
+ protected void init(KeycloakSession session, Cache<String, SessionEntityWrapper> cache, RemoteCache remoteCache) {
+ this.cache = cache;
+ this.remoteCache = remoteCache;
+
+ this.distributed = InfinispanUtil.isDistributedCache(cache);
+ if (this.distributed) {
+ this.myAddress = InfinispanUtil.getMyAddress(session);
+ } else {
+ this.myAddress = null;
+ }
+ }
+
+
+ @ClientCacheEntryCreated
+ public void created(ClientCacheEntryCreatedEvent event) {
+ String key = (String) event.getKey();
+
+ if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
+ // Should load it from remoteStore
+ cache.get(key);
+ }
+ }
+
+
+ @ClientCacheEntryModified
+ public void updated(ClientCacheEntryModifiedEvent event) {
+ String key = (String) event.getKey();
+
+ if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
+
+ replaceRemoteEntityInCache(key, event.getVersion());
+ }
+ }
+
+ private static final int MAXIMUM_REPLACE_RETRIES = 10;
+
+ private void replaceRemoteEntityInCache(String key, long eventVersion) {
+ // TODO can be optimized and remoteSession sent in the event itself?
+ boolean replaced = false;
+ int replaceRetries = 0;
+ int sleepInterval = 25;
+ do {
+ replaceRetries++;
+
+ SessionEntityWrapper localEntityWrapper = cache.get(key);
+ VersionedValue remoteSessionVersioned = remoteCache.getVersioned(key);
+ if (remoteSessionVersioned == null || remoteSessionVersioned.getVersion() < eventVersion) {
+ try {
+ logger.debugf("Got replace remote entity event prematurely, will try again. Event version: %d, got: %d",
+ eventVersion, remoteSessionVersioned == null ? -1 : remoteSessionVersioned.getVersion());
+ Thread.sleep(new Random().nextInt(sleepInterval)); // using exponential backoff
+ continue;
+ } catch (InterruptedException ex) {
+ continue;
+ } finally {
+ sleepInterval = sleepInterval << 1;
+ }
+ }
+ SessionEntity remoteSession = (SessionEntity) remoteCache.get(key);
+
+ logger.debugf("Read session%s. Entity read from remote cache: %s", replaceRetries > 1 ? "" : " again", remoteSession);
+
+ SessionEntityWrapper sessionWrapper = remoteSession.mergeRemoteEntityWithLocalEntity(localEntityWrapper);
+
+ // We received event from remoteCache, so we won't update it back
+ replaced = cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
+ .replace(key, localEntityWrapper, sessionWrapper);
+
+ if (! replaced) {
+ logger.debugf("Did not succeed in merging sessions, will try again: %s", remoteSession);
+ }
+ } while (replaceRetries < MAXIMUM_REPLACE_RETRIES && ! replaced);
+ }
+
+
+ @ClientCacheEntryRemoved
+ public void removed(ClientCacheEntryRemovedEvent event) {
+ String key = (String) event.getKey();
+
+ if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
+ // We received event from remoteCache, so we won't update it back
+ cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
+ .remove(key);
+ }
+ }
+
+
+ @ClientCacheFailover
+ public void failover(ClientCacheFailoverEvent event) {
+ logger.infof("Received failover event: " + event.toString());
+ }
+
+
+ // For distributed caches, ensure that local modification is executed just on owner OR if event.isCommandRetried
+ protected boolean shouldUpdateLocalCache(ClientEvent.Type type, String key, boolean commandRetried) {
+ boolean result;
+
+ // Case when cache is stopping or stopped already
+ if (!cache.getStatus().allowInvocations()) {
+ return false;
+ }
+
+ if (!distributed || commandRetried) {
+ result = true;
+ } else {
+ String keyAddress = InfinispanUtil.getKeyPrimaryOwnerAddress(cache, key);
+ result = myAddress.equals(keyAddress);
+ }
+
+ logger.debugf("Received event from remote store. Event '%s', key '%s', skip '%b'", type.toString(), key, !result);
+
+ return result;
+ }
+
+
+
+ @ClientListener(includeCurrentState = true)
+ public static class FetchInitialStateCacheListener extends RemoteCacheSessionListener {
+ }
+
+
+ @ClientListener(includeCurrentState = false)
+ public static class DontFetchInitialStateCacheListener extends RemoteCacheSessionListener {
+ }
+
+
+ public static RemoteCacheSessionListener createListener(KeycloakSession session, Cache<String, SessionEntityWrapper> cache, RemoteCache remoteCache) {
+ /*boolean isCoordinator = InfinispanUtil.isCoordinator(cache);
+
+ // Just cluster coordinator will fetch userSessions from remote cache.
+ // In case that coordinator is failover during state fetch, there is slight risk that not all userSessions will be fetched to local cluster. Assume acceptable for now
+ RemoteCacheSessionListener listener;
+ if (isCoordinator) {
+ logger.infof("Will fetch initial state from remote cache for cache '%s'", cache.getName());
+ listener = new FetchInitialStateCacheListener();
+ } else {
+ logger.infof("Won't fetch initial state from remote cache for cache '%s'", cache.getName());
+ listener = new DontFetchInitialStateCacheListener();
+ }*/
+
+ RemoteCacheSessionListener listener = new RemoteCacheSessionListener();
+ listener.init(session, cache, remoteCache);
+
+ return listener;
+ }
+
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
new file mode 100644
index 0000000..65c31bc
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.remotestore;
+
+import java.io.Serializable;
+import java.util.Map;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.context.Flag;
+import org.jboss.logging.Logger;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.sessions.infinispan.initializer.BaseCacheInitializer;
+import org.keycloak.models.sessions.infinispan.initializer.OfflinePersistentUserSessionLoader;
+import org.keycloak.models.sessions.infinispan.initializer.SessionLoader;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RemoteCacheSessionsLoader implements SessionLoader {
+
+ private static final Logger log = Logger.getLogger(RemoteCacheSessionsLoader.class);
+
+ // Hardcoded limit for now. See if needs to be configurable (or if preloading can be enabled/disabled in configuration)
+ public static final int LIMIT = 100000;
+
+ private final String cacheName;
+
+ public RemoteCacheSessionsLoader(String cacheName) {
+ this.cacheName = cacheName;
+ }
+
+ @Override
+ public void init(KeycloakSession session) {
+
+ }
+
+ @Override
+ public int getSessionsCount(KeycloakSession session) {
+ RemoteCache remoteCache = InfinispanUtil.getRemoteCache(getCache(session));
+ return remoteCache.size();
+ }
+
+ @Override
+ public boolean loadSessions(KeycloakSession session, int first, int max) {
+ Cache cache = getCache(session);
+ Cache decoratedCache = cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE, Flag.IGNORE_RETURN_VALUES);
+
+ RemoteCache<?, ?> remoteCache = InfinispanUtil.getRemoteCache(cache);
+
+ int size = remoteCache.size();
+
+ if (size > LIMIT) {
+ log.infof("Skip bulk load of '%d' sessions from remote cache '%s'. Sessions will be retrieved lazily", size, cache.getName());
+ return true;
+ } else {
+ log.infof("Will do bulk load of '%d' sessions from remote cache '%s'", size, cache.getName());
+ }
+
+
+ for (Map.Entry<?, ?> entry : remoteCache.getBulk().entrySet()) {
+ SessionEntity entity = (SessionEntity) entry.getValue();
+ SessionEntityWrapper entityWrapper = new SessionEntityWrapper(entity);
+
+ decoratedCache.putAsync(entry.getKey(), entityWrapper);
+ }
+
+ return true;
+ }
+
+
+ private Cache getCache(KeycloakSession session) {
+ InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class);
+ return ispn.getCache(cacheName);
+ }
+
+
+ @Override
+ public boolean isFinished(BaseCacheInitializer initializer) {
+ Cache<String, Serializable> workCache = initializer.getWorkCache();
+
+ // Check if persistent sessions were already loaded in this DC. This is possible just for offline sessions ATM
+ Boolean sessionsLoaded = (Boolean) workCache
+ .getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE)
+ .get(OfflinePersistentUserSessionLoader.PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC);
+
+ if (cacheName.equals(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) && sessionsLoaded != null && sessionsLoaded) {
+ log.debugf("Sessions already loaded in current DC. Skip sessions loading from remote cache '%s'", cacheName);
+ return true;
+ } else {
+ log.debugf("Sessions maybe not yet loaded in current DC. Will load them from remote cache '%s'", cacheName);
+ return false;
+ }
+ }
+
+
+ @Override
+ public void afterAllSessionsLoaded(BaseCacheInitializer initializer) {
+
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
index dd2db68..f75391c 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
@@ -17,7 +17,7 @@
package org.keycloak.models.sessions.infinispan.stream;
-import org.keycloak.models.sessions.infinispan.UserSessionTimestamp;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
@@ -25,7 +25,6 @@ import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import java.io.Serializable;
import java.util.Map;
-import java.util.Optional;
import java.util.function.Function;
/**
@@ -33,19 +32,19 @@ import java.util.function.Function;
*/
public class Mappers {
- public static Function<Map.Entry<String, Optional<UserSessionTimestamp>>, UserSessionTimestamp> userSessionTimestamp() {
- return new UserSessionTimestampMapper();
+ public static Function<Map.Entry<String, SessionEntityWrapper>, Map.Entry<String, SessionEntity>> unwrap() {
+ return new SessionUnwrap();
}
- public static Function<Map.Entry<String, SessionEntity>, String> sessionId() {
+ public static Function<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>, String> sessionId() {
return new SessionIdMapper();
}
- public static Function<Map.Entry<String, SessionEntity>, SessionEntity> sessionEntity() {
+ public static Function<Map.Entry<String, SessionEntityWrapper>, SessionEntity> sessionEntity() {
return new SessionEntityMapper();
}
- public static Function<Map.Entry<String, SessionEntity>, UserSessionEntity> userSessionEntity() {
+ public static Function<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>, UserSessionEntity> userSessionEntity() {
return new UserSessionEntityMapper();
}
@@ -53,32 +52,55 @@ public class Mappers {
return new LoginFailureIdMapper();
}
- private static class UserSessionTimestampMapper implements Function<Map.Entry<String, Optional<org.keycloak.models.sessions.infinispan.UserSessionTimestamp>>, org.keycloak.models.sessions.infinispan.UserSessionTimestamp>, Serializable {
+
+ private static class SessionUnwrap implements Function<Map.Entry<String, SessionEntityWrapper>, Map.Entry<String, SessionEntity>>, Serializable {
+
@Override
- public org.keycloak.models.sessions.infinispan.UserSessionTimestamp apply(Map.Entry<String, Optional<org.keycloak.models.sessions.infinispan.UserSessionTimestamp>> e) {
- return e.getValue().get();
+ public Map.Entry<String, SessionEntity> apply(Map.Entry<String, SessionEntityWrapper> wrapperEntry) {
+ return new Map.Entry<String, SessionEntity>() {
+
+ @Override
+ public String getKey() {
+ return wrapperEntry.getKey();
+ }
+
+ @Override
+ public SessionEntity getValue() {
+ return wrapperEntry.getValue().getEntity();
+ }
+
+ @Override
+ public SessionEntity setValue(SessionEntity value) {
+ throw new IllegalStateException("Unsupported operation");
+ }
+
+ };
}
+
}
- private static class SessionIdMapper implements Function<Map.Entry<String, SessionEntity>, String>, Serializable {
+
+ private static class SessionIdMapper implements Function<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>, String>, Serializable {
@Override
- public String apply(Map.Entry<String, SessionEntity> entry) {
+ public String apply(Map.Entry<String, SessionEntityWrapper<UserSessionEntity>> entry) {
return entry.getKey();
}
}
- private static class SessionEntityMapper implements Function<Map.Entry<String, SessionEntity>, SessionEntity>, Serializable {
+ private static class SessionEntityMapper implements Function<Map.Entry<String, SessionEntityWrapper>, SessionEntity>, Serializable {
@Override
- public SessionEntity apply(Map.Entry<String, SessionEntity> entry) {
- return entry.getValue();
+ public SessionEntity apply(Map.Entry<String, SessionEntityWrapper> entry) {
+ return entry.getValue().getEntity();
}
}
- private static class UserSessionEntityMapper implements Function<Map.Entry<String, SessionEntity>, UserSessionEntity>, Serializable {
+ private static class UserSessionEntityMapper implements Function<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>, UserSessionEntity>, Serializable {
+
@Override
- public UserSessionEntity apply(Map.Entry<String, SessionEntity> entry) {
- return (UserSessionEntity) entry.getValue();
+ public UserSessionEntity apply(Map.Entry<String, SessionEntityWrapper<UserSessionEntity>> entry) {
+ return entry.getValue().getEntity();
}
+
}
private static class LoginFailureIdMapper implements Function<Map.Entry<LoginFailureKey, LoginFailureEntity>, LoginFailureKey>, Serializable {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/SessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/SessionPredicate.java
index c8160bb..f72b92d 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/SessionPredicate.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/SessionPredicate.java
@@ -17,6 +17,7 @@
package org.keycloak.models.sessions.infinispan.stream;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import java.io.Serializable;
@@ -26,7 +27,7 @@ import java.util.function.Predicate;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class SessionPredicate implements Predicate<Map.Entry<String, SessionEntity>>, Serializable {
+public class SessionPredicate<S extends SessionEntity> implements Predicate<Map.Entry<String, SessionEntityWrapper<S>>>, Serializable {
private String realm;
@@ -39,8 +40,8 @@ public class SessionPredicate implements Predicate<Map.Entry<String, SessionEnti
}
@Override
- public boolean test(Map.Entry<String, SessionEntity> entry) {
- return realm.equals(entry.getValue().getRealm());
+ public boolean test(Map.Entry<String, SessionEntityWrapper<S>> entry) {
+ return realm.equals(entry.getValue().getEntity().getRealm());
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java
index 0cc3fcc..06609f2 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java
@@ -17,6 +17,7 @@
package org.keycloak.models.sessions.infinispan.stream;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
@@ -27,7 +28,7 @@ import java.util.function.Predicate;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class UserSessionPredicate implements Predicate<Map.Entry<String, SessionEntity>>, Serializable {
+public class UserSessionPredicate implements Predicate<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>>, Serializable {
private String realm;
@@ -77,12 +78,8 @@ public class UserSessionPredicate implements Predicate<Map.Entry<String, Session
}
@Override
- public boolean test(Map.Entry<String, SessionEntity> entry) {
- SessionEntity e = entry.getValue();
-
- if (!(e instanceof UserSessionEntity)) {
- return false;
- }
+ public boolean test(Map.Entry<String, SessionEntityWrapper<UserSessionEntity>> entry) {
+ SessionEntity e = entry.getValue().getEntity();
UserSessionEntity entity = (UserSessionEntity) e;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
index f35dea9..3f09773 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
@@ -17,15 +17,17 @@
package org.keycloak.models.sessions.infinispan;
-import org.infinispan.Cache;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
+import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshChecker;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
-import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import java.util.Collections;
@@ -33,7 +35,6 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -44,7 +45,7 @@ public class UserSessionAdapter implements UserSessionModel {
private final InfinispanUserSessionProvider provider;
- private final Cache<String, SessionEntity> cache;
+ private final InfinispanChangelogBasedTransaction updateTx;
private final RealmModel realm;
@@ -52,11 +53,11 @@ public class UserSessionAdapter implements UserSessionModel {
private final boolean offline;
- public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache, RealmModel realm,
+ public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, InfinispanChangelogBasedTransaction updateTx, RealmModel realm,
UserSessionEntity entity, boolean offline) {
this.session = session;
this.provider = provider;
- this.cache = cache;
+ this.updateTx = updateTx;
this.realm = realm;
this.entity = entity;
this.offline = offline;
@@ -74,7 +75,7 @@ public class UserSessionAdapter implements UserSessionModel {
// Check if client still exists
ClientModel client = realm.getClientById(key);
if (client != null) {
- result.put(key, new AuthenticatedClientSessionAdapter(value, client, this, provider, cache));
+ result.put(key, new AuthenticatedClientSessionAdapter(value, client, this, provider, updateTx));
} else {
removedClientUUIDS.add(key);
}
@@ -83,10 +84,18 @@ public class UserSessionAdapter implements UserSessionModel {
// Update user session
if (!removedClientUUIDS.isEmpty()) {
- for (String clientUUID : removedClientUUIDS) {
- entity.getAuthenticatedClientSessions().remove(clientUUID);
- }
- update();
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity entity) {
+ for (String clientUUID : removedClientUUIDS) {
+ entity.getAuthenticatedClientSessions().remove(clientUUID);
+ }
+ }
+
+ };
+
+ update(task);
}
return Collections.unmodifiableMap(result);
@@ -115,12 +124,6 @@ public class UserSessionAdapter implements UserSessionModel {
}
@Override
- public void setUser(UserModel user) {
- entity.setUser(user.getId());
- update();
- }
-
- @Override
public String getLoginUsername() {
return entity.getLoginUsername();
}
@@ -148,8 +151,26 @@ public class UserSessionAdapter implements UserSessionModel {
}
public void setLastSessionRefresh(int lastSessionRefresh) {
- entity.setLastSessionRefresh(lastSessionRefresh);
- update();
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity entity) {
+ entity.setLastSessionRefresh(lastSessionRefresh);
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
+ return new LastSessionRefreshChecker(provider.getLastSessionRefreshStore(), provider.getOfflineLastSessionRefreshStore())
+ .getCrossDCMessageStatus(UserSessionAdapter.this.session, UserSessionAdapter.this.realm, sessionWrapper, offline, lastSessionRefresh);
+ }
+
+ @Override
+ public String toString() {
+ return "setLastSessionRefresh(" + lastSessionRefresh + ')';
+ }
+ };
+
+ update(task);
}
@Override
@@ -159,22 +180,36 @@ public class UserSessionAdapter implements UserSessionModel {
@Override
public void setNote(String name, String value) {
- if (value == null) {
- if (entity.getNotes().containsKey(name)) {
- removeNote(name);
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity entity) {
+ if (value == null) {
+ if (entity.getNotes().containsKey(name)) {
+ removeNote(name);
+ }
+ return;
+ }
+ entity.getNotes().put(name, value);
}
- return;
- }
- entity.getNotes().put(name, value);
- update();
+
+ };
+
+ update(task);
}
@Override
public void removeNote(String name) {
- if (entity.getNotes() != null) {
- entity.getNotes().remove(name);
- update();
- }
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity entity) {
+ entity.getNotes().remove(name);
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -189,19 +224,34 @@ public class UserSessionAdapter implements UserSessionModel {
@Override
public void setState(State state) {
- entity.setState(state);
- update();
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity entity) {
+ entity.setState(state);
+ }
+
+ };
+
+ update(task);
}
@Override
public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
- provider.updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
- entity.setState(null);
- entity.getNotes().clear();
- entity.getAuthenticatedClientSessions().clear();
+ @Override
+ public void runUpdate(UserSessionEntity entity) {
+ provider.updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
- update();
+ entity.setState(null);
+ entity.getNotes().clear();
+ entity.getAuthenticatedClientSessions().clear();
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -222,11 +272,8 @@ public class UserSessionAdapter implements UserSessionModel {
return entity;
}
- void update() {
- provider.getTx().replace(cache, entity.getId(), entity);
+ void update(UserSessionUpdateTask task) {
+ updateTx.addTask(getId(), task);
}
- Cache<String, SessionEntity> getCache() {
- return cache;
- }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/InfinispanUtil.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/InfinispanUtil.java
new file mode 100644
index 0000000..1bb2862
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/InfinispanUtil.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.util;
+
+import java.util.Set;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.configuration.cache.CacheMode;
+import org.infinispan.distribution.DistributionManager;
+import org.infinispan.persistence.manager.PersistenceManager;
+import org.infinispan.persistence.remote.RemoteStore;
+import org.infinispan.remoting.transport.Address;
+import org.infinispan.remoting.transport.Transport;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanUtil {
+
+ // See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario
+ public static Set<RemoteStore> getRemoteStores(Cache ispnCache) {
+ return ispnCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class);
+ }
+
+
+ public static RemoteCache getRemoteCache(Cache ispnCache) {
+ Set<RemoteStore> remoteStores = getRemoteStores(ispnCache);
+ if (remoteStores.isEmpty()) {
+ return null;
+ } else {
+ return remoteStores.iterator().next().getRemoteCache();
+ }
+ }
+
+
+ public static boolean isDistributedCache(Cache ispnCache) {
+ CacheMode cacheMode = ispnCache.getCacheConfiguration().clustering().cacheMode();
+ return cacheMode.isDistributed();
+ }
+
+
+ public static String getMyAddress(KeycloakSession session) {
+ return session.getProvider(InfinispanConnectionProvider.class).getNodeName();
+ }
+
+ public static String getMySite(KeycloakSession session) {
+ return session.getProvider(InfinispanConnectionProvider.class).getSiteName();
+ }
+
+
+ /**
+ *
+ * @param ispnCache
+ * @param key
+ * @return address of the node, who is owner of the specified key in current cluster
+ */
+ public static String getKeyPrimaryOwnerAddress(Cache ispnCache, Object key) {
+ DistributionManager distManager = ispnCache.getAdvancedCache().getDistributionManager();
+ if (distManager == null) {
+ throw new IllegalArgumentException("Cache '" + ispnCache.getName() + "' is not distributed cache");
+ }
+
+ return distManager.getPrimaryLocation(key).toString();
+ }
+
+
+ /**
+ *
+ * @param cache
+ * @return true if cluster coordinator OR if it's local cache
+ */
+ public static boolean isCoordinator(Cache cache) {
+ Transport transport = cache.getCacheManager().getTransport();
+ return transport == null || transport.isCoordinator();
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java
new file mode 100644
index 0000000..e732c11
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.util;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.infinispan.commons.marshall.Externalizer;
+import org.infinispan.commons.marshall.MarshallUtil;
+import org.jboss.logging.Logger;
+
+/**
+ *
+ * Helper to optimize marshalling/unmarhsalling of some types
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class KeycloakMarshallUtil {
+
+ private static final Logger log = Logger.getLogger(KeycloakMarshallUtil.class);
+
+ public static final StringExternalizer STRING_EXT = new StringExternalizer();
+
+ // MAP
+
+ public static <K, V> void writeMap(Map<K, V> map, Externalizer<K> keyExternalizer, Externalizer<V> valueExternalizer, ObjectOutput output) throws IOException {
+ if (map == null) {
+ output.writeByte(0);
+ } else {
+ output.writeByte(1);
+
+ // Copy the map as it can be updated concurrently
+ Map<K, V> copy = new HashMap<>(map);
+ //Map<K, V> copy = map;
+
+ output.writeInt(copy.size());
+
+ for (Map.Entry<K, V> entry : copy.entrySet()) {
+ keyExternalizer.writeObject(output, entry.getKey());
+ valueExternalizer.writeObject(output, entry.getValue());
+ }
+ }
+ }
+
+ public static <K, V, TYPED_MAP extends Map<K, V>> TYPED_MAP readMap(ObjectInput input,
+ Externalizer<K> keyExternalizer, Externalizer<V> valueExternalizer,
+ MarshallUtil.MapBuilder<K, V, TYPED_MAP> mapBuilder) throws IOException, ClassNotFoundException {
+ byte b = input.readByte();
+ if (b == 0) {
+ return null;
+ } else {
+
+ int size = input.readInt();
+
+ TYPED_MAP map = mapBuilder.build(size);
+
+ for (int i=0 ; i<size ; i++) {
+ K key = keyExternalizer.readObject(input);
+ V value = valueExternalizer.readObject(input);
+
+ map.put(key, value);
+ }
+
+ return map;
+ }
+ }
+
+ // COLLECTION
+
+ public static <E> void writeCollection(Collection<E> col, Externalizer<E> valueExternalizer, ObjectOutput output) throws IOException {
+ if (col == null) {
+ output.writeByte(0);
+ } else {
+ output.writeByte(1);
+
+ // Copy the collection as it can be updated concurrently
+ Collection<E> copy = new LinkedList<>(col);
+
+ output.writeInt(copy.size());
+
+ for (E entry : copy) {
+ valueExternalizer.writeObject(output, entry);
+ }
+ }
+ }
+
+ public static <E, T extends Collection<E>> T readCollection(ObjectInput input, Externalizer<E> valueExternalizer,
+ MarshallUtil.CollectionBuilder<E, T> colBuilder) throws ClassNotFoundException, IOException {
+ byte b = input.readByte();
+ if (b == 0) {
+ return null;
+ } else {
+
+ int size = input.readInt();
+
+ T col = colBuilder.build(size);
+
+ for (int i=0 ; i<size ; i++) {
+ E value = valueExternalizer.readObject(input);
+ col.add(value);
+ }
+
+ return col;
+ }
+ }
+
+
+
+
+ public static class ConcurrentHashMapBuilder<K, V> implements MarshallUtil.MapBuilder<K, V, ConcurrentHashMap<K, V>> {
+
+ @Override
+ public ConcurrentHashMap<K, V> build(int size) {
+ return new ConcurrentHashMap<>(size);
+ }
+
+ }
+
+ public static class HashSetBuilder<E> implements MarshallUtil.CollectionBuilder<E, HashSet<E>> {
+
+ @Override
+ public HashSet<E> build(int size) {
+ return new HashSet<>(size);
+ }
+ }
+
+
+ private static class StringExternalizer implements Externalizer<String> {
+
+ @Override
+ public void writeObject(ObjectOutput output, String str) throws IOException {
+ MarshallUtil.marshallString(str, output);
+ }
+
+ @Override
+ public String readObject(ObjectInput input) throws IOException, ClassNotFoundException {
+ return MarshallUtil.unmarshallString(input);
+ }
+
+ }
+
+}
diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheClientListenersTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheClientListenersTest.java
new file mode 100644
index 0000000..b524885
--- /dev/null
+++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheClientListenersTest.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.cluster.infinispan;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
+import org.infinispan.client.hotrod.annotation.ClientListener;
+import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.persistence.manager.PersistenceManager;
+import org.infinispan.persistence.remote.RemoteStore;
+import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
+import org.junit.Assert;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+
+/**
+ * Test that hotrod ClientListeners are correctly executed as expected
+ *
+ * STEPS TO REPRODUCE:
+ * - Unzip infinispan-server-8.2.6.Final to some locations ISPN1 and ISPN2
+ *
+ * - Edit both ISPN1/standalone/configuration/clustered.xml and ISPN2/standalone/configuration/clustered.xml . Configure cache in container "clustered"
+ *
+ * <replicated-cache-configuration name="sessions-cfg" mode="ASYNC" start="EAGER" batching="false">
+ <transaction mode="NON_XA" locking="PESSIMISTIC"/>
+ </replicated-cache-configuration>
+
+ <replicated-cache name="work" configuration="sessions-cfg" />
+
+ - Run server1
+ ./standalone.sh -c clustered.xml -Djava.net.preferIPv4Stack=true -Djboss.socket.binding.port-offset=1010 -Djboss.default.multicast.address=234.56.78.99 -Djboss.node.name=cache-server
+
+ - Run server2
+ ./standalone.sh -c clustered.xml -Djava.net.preferIPv4Stack=true -Djboss.socket.binding.port-offset=2010 -Djboss.default.multicast.address=234.56.78.99 -Djboss.node.name=cache-server-dc-2
+
+ - Run this test as main class from IDE
+ *
+ *
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ConcurrencyJDGRemoteCacheClientListenersTest {
+
+ // Helper map to track if listeners were executed
+ private static Map<String, EntryInfo> state = new HashMap<>();
+
+ private static AtomicInteger totalListenerCalls = new AtomicInteger(0);
+
+ private static AtomicInteger totalErrors = new AtomicInteger(0);
+
+
+ public static void main(String[] args) throws Exception {
+ // Init map somehow
+ for (int i=0 ; i<1000 ; i++) {
+ String key = "key-" + i;
+ EntryInfo entryInfo = new EntryInfo();
+ entryInfo.val.set(i);
+ state.put(key, entryInfo);
+ }
+
+ // Create caches, listeners and finally worker threads
+ Worker worker1 = createWorker(1);
+ Worker worker2 = createWorker(2);
+
+ // Note "run", so it's not executed asynchronously here!!!
+ worker1.run();
+
+//
+// // Start and join workers
+// worker1.start();
+// worker2.start();
+//
+// worker1.join();
+// worker2.join();
+
+ // Output
+ for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+ System.out.println(entry.getKey() + ":::" + entry.getValue());
+ }
+
+ System.out.println("totalListeners: " + totalListenerCalls.get() + ", totalErrors: " + totalErrors.get());
+
+
+ // Assert that ClientListener was able to read the value and save it into EntryInfo
+ try {
+ for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+ EntryInfo info = entry.getValue();
+ Assert.assertEquals(info.val.get(), info.dc1Created.get());
+ Assert.assertEquals(info.val.get(), info.dc2Created.get());
+ Assert.assertEquals(info.val.get() * 2, info.dc1Updated.get());
+ Assert.assertEquals(info.val.get() * 2, info.dc2Updated.get());
+ worker1.cache.remove(entry.getKey());
+ }
+ } finally {
+ // Finish JVM
+ worker1.cache.getCacheManager().stop();
+ worker2.cache.getCacheManager().stop();
+ }
+ }
+
+ private static Worker createWorker(int threadId) {
+ EmbeddedCacheManager manager = new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.WORK_CACHE_NAME, RemoteStoreConfigurationBuilder.class);
+ Cache<String, Integer> cache = manager.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+
+ System.out.println("Retrieved cache: " + threadId);
+
+ RemoteStore remoteStore = cache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class).iterator().next();
+ HotRodListener listener = new HotRodListener(cache, threadId);
+ remoteStore.getRemoteCache().addClientListener(listener);
+
+ return new Worker(cache, threadId);
+ }
+
+
+ @ClientListener
+ public static class HotRodListener {
+
+ private final RemoteCache<String, Integer> remoteCache;
+ private final int threadId;
+
+ public HotRodListener(Cache<String, Integer> cache, int threadId) {
+ this.remoteCache = InfinispanUtil.getRemoteCache(cache);
+ this.threadId = threadId;
+ }
+
+ //private AtomicInteger listenerCount = new AtomicInteger(0);
+
+ @ClientCacheEntryCreated
+ public void created(ClientCacheEntryCreatedEvent event) {
+ String cacheKey = (String) event.getKey();
+ event(cacheKey, true);
+
+ }
+
+
+ @ClientCacheEntryModified
+ public void updated(ClientCacheEntryModifiedEvent event) {
+ String cacheKey = (String) event.getKey();
+ event(cacheKey, false);
+ }
+
+
+ private void event(String cacheKey, boolean created) {
+ EntryInfo entryInfo = state.get(cacheKey);
+ entryInfo.successfulListenerWrites.incrementAndGet();
+
+ totalListenerCalls.incrementAndGet();
+
+ Integer val = remoteCache.get(cacheKey);
+ if (val != null) {
+ AtomicInteger dcVal;
+ if (created) {
+ dcVal = threadId == 1 ? entryInfo.dc1Created : entryInfo.dc2Created;
+ } else {
+ dcVal = threadId == 1 ? entryInfo.dc1Updated : entryInfo.dc2Updated;
+ }
+ dcVal.set(val);
+ } else {
+ System.err.println("NOT A VALUE FOR KEY: " + cacheKey);
+ totalErrors.incrementAndGet();
+ }
+ }
+
+ }
+
+
+ private static class Worker extends Thread {
+
+ private final Cache<String, Integer> cache;
+
+ private final int myThreadId;
+
+ private Worker(Cache<String, Integer> cache, int myThreadId) {
+ this.cache = cache;
+ this.myThreadId = myThreadId;
+ }
+
+ @Override
+ public void run() {
+ for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+ String cacheKey = entry.getKey();
+ Integer value = entry.getValue().val.get();
+
+ this.cache.put(cacheKey, value);
+ }
+
+ System.out.println("Worker creating finished: " + myThreadId);
+
+ for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+ String cacheKey = entry.getKey();
+ Integer value = entry.getValue().val.get() * 2;
+
+ this.cache.replace(cacheKey, value);
+ }
+
+ System.out.println("Worker updating finished: " + myThreadId);
+ }
+
+ }
+
+
+ public static class EntryInfo {
+ AtomicInteger val = new AtomicInteger();
+ AtomicInteger successfulListenerWrites = new AtomicInteger(0);
+ AtomicInteger dc1Created = new AtomicInteger();
+ AtomicInteger dc2Created = new AtomicInteger();
+ AtomicInteger dc1Updated = new AtomicInteger();
+ AtomicInteger dc2Updated = new AtomicInteger();
+
+ @Override
+ public String toString() {
+ return String.format("val: %d, successfulListenerWrites: %d, dc1Created: %d, dc2Created: %d, dc1Updated: %d, dc2Updated: %d", val.get(), successfulListenerWrites.get(),
+ dc1Created.get(), dc2Created.get(), dc1Updated.get(), dc2Updated.get());
+ }
+ }
+}
diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java
index e7c1337..9c23452 100644
--- a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java
+++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java
@@ -43,11 +43,12 @@ import org.junit.Ignore;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
/**
- * Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG
+ * Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG. Especially tests "putIfAbsent" contract.
+ *
+ * Steps: {@see ConcurrencyJDGRemoteCacheClientListenersTest}
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
-@Ignore
public class ConcurrencyJDGRemoteCacheTest {
private static Map<String, EntryInfo> state = new HashMap<>();
@@ -82,7 +83,7 @@ public class ConcurrencyJDGRemoteCacheTest {
}
private static Worker createWorker(int threadId) {
- EmbeddedCacheManager manager = createManager(threadId);
+ EmbeddedCacheManager manager = new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.WORK_CACHE_NAME, RemoteStoreConfigurationBuilder.class);
Cache<String, Integer> cache = manager.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
System.out.println("Retrieved cache: " + threadId);
@@ -94,56 +95,6 @@ public class ConcurrencyJDGRemoteCacheTest {
return new Worker(cache, threadId);
}
- private static EmbeddedCacheManager createManager(int threadId) {
- System.setProperty("java.net.preferIPv4Stack", "true");
- System.setProperty("jgroups.tcp.port", "53715");
- GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
-
- boolean clustered = false;
- boolean async = false;
- boolean allowDuplicateJMXDomains = true;
-
- if (clustered) {
- gcb = gcb.clusteredDefault();
- gcb.transport().clusterName("test-clustering");
- }
-
- gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
-
- EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build());
-
- Configuration invalidationCacheConfiguration = getCacheBackedByRemoteStore(threadId);
-
- cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, invalidationCacheConfiguration);
- return cacheManager;
-
- }
-
- private static Configuration getCacheBackedByRemoteStore(int threadId) {
- ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder();
-
- // int port = threadId==1 ? 11222 : 11322;
- int port = 11222;
-
- return cacheConfigBuilder.persistence().addStore(RemoteStoreConfigurationBuilder.class)
- .fetchPersistentState(false)
- .ignoreModifications(false)
- .purgeOnStartup(false)
- .preload(false)
- .shared(true)
- .remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME)
- .rawValues(true)
- .forceReturnValues(false)
- .addServer()
- .host("localhost")
- .port(port)
- .connectionPool()
- .maxActive(20)
- .exhaustedAction(ExhaustedAction.CREATE_NEW)
- .async()
- . enabled(false).build();
- }
-
@ClientListener
public static class HotRodListener {
@@ -214,7 +165,7 @@ public class ConcurrencyJDGRemoteCacheTest {
}
}
- private static class EntryInfo {
+ public static class EntryInfo {
AtomicInteger successfulInitializations = new AtomicInteger(0);
AtomicInteger successfulListenerWrites = new AtomicInteger(0);
AtomicInteger th1 = new AtomicInteger();
diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java
new file mode 100644
index 0000000..ff4c3ce
--- /dev/null
+++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.cluster.infinispan;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved;
+import org.infinispan.client.hotrod.annotation.ClientListener;
+import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent;
+import org.infinispan.context.Flag;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
+import org.jboss.logging.Logger;
+import org.junit.Assert;
+import org.keycloak.common.util.Time;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+
+/**
+ * Check that removing of session from remoteCache is session immediately removed on remoteCache in other DC. This is true.
+ *
+ * Also check that listeners are executed asynchronously with some delay.
+ *
+ * Steps: {@see ConcurrencyJDGRemoteCacheClientListenersTest}
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ConcurrencyJDGRemoveSessionTest {
+
+ protected static final Logger logger = Logger.getLogger(ConcurrencyJDGRemoveSessionTest.class);
+
+ private static final int ITERATIONS = 10000;
+
+ private static RemoteCache remoteCache1;
+ private static RemoteCache remoteCache2;
+
+ private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0);
+ private static final AtomicInteger failedReplaceCounter2 = new AtomicInteger(0);
+
+ private static final AtomicInteger successfulListenerWrites = new AtomicInteger(0);
+ private static final AtomicInteger successfulListenerWrites2 = new AtomicInteger(0);
+
+ //private static Map<String, EntryInfo> state = new HashMap<>();
+
+ public static void main(String[] args) throws Exception {
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> cache1 = createManager(1).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> cache2 = createManager(2).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+
+ // Create caches, listeners and finally worker threads
+ Thread worker1 = createWorker(cache1, 1);
+ Thread worker2 = createWorker(cache2, 2);
+
+ // Create 100 initial sessions
+ for (int i=0 ; i<ITERATIONS ; i++) {
+ String sessionId = String.valueOf(i);
+ SessionEntityWrapper<UserSessionEntity> wrappedSession = createSessionEntity(sessionId);
+ cache1.put(sessionId, wrappedSession);
+ }
+
+ logger.info("SESSIONS CREATED");
+
+ // Create 100 initial sessions
+ for (int i=0 ; i<ITERATIONS ; i++) {
+ String sessionId = String.valueOf(i);
+ SessionEntityWrapper loadedWrapper = cache2.get(sessionId);
+ Assert.assertNotNull("Loaded wrapper for key " + sessionId, loadedWrapper);
+ }
+
+ logger.info("SESSIONS AVAILABLE ON DC2");
+
+
+ long start = System.currentTimeMillis();
+
+ try {
+ // Just running in current thread
+ worker1.run();
+
+ logger.info("SESSIONS REMOVED");
+
+ //Thread.sleep(5000);
+
+ // Doing it in opposite direction to ensure that newer are checked first.
+ // This us currently FAILING (expected) as listeners are executed asynchronously.
+ for (int i=ITERATIONS-1 ; i>=0 ; i--) {
+ String sessionId = String.valueOf(i);
+
+ logger.infof("Before call cache2.get: %s", sessionId);
+
+ SessionEntityWrapper loadedWrapper = cache2.get(sessionId);
+ Assert.assertNull("Loaded wrapper not null for key " + sessionId, loadedWrapper);
+ }
+
+ logger.info("SESSIONS NOT AVAILABLE ON DC2");
+
+
+ // // Start and join workers
+// worker1.start();
+// worker2.start();
+//
+// worker1.join();
+// worker2.join();
+
+ } finally {
+ Thread.sleep(2000);
+
+ // Finish JVM
+ cache1.getCacheManager().stop();
+ cache2.getCacheManager().stop();
+ }
+
+ long took = System.currentTimeMillis() - start;
+
+// // Output
+// for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+// System.out.println(entry.getKey() + ":::" + entry.getValue());
+// worker1.cache.remove(entry.getKey());
+// }
+
+// System.out.println("Finished. Took: " + took + " ms. Notes: " + cache1.get("123").getEntity().getNotes().size() +
+// ", successfulListenerWrites: " + successfulListenerWrites.get() + ", successfulListenerWrites2: " + successfulListenerWrites2.get() +
+// ", failedReplaceCounter: " + failedReplaceCounter.get() + ", failedReplaceCounter2: " + failedReplaceCounter2.get() );
+//
+// System.out.println("Sleeping before other report");
+//
+// Thread.sleep(1000);
+//
+// System.out.println("Finished. Took: " + took + " ms. Notes: " + cache1.get("123").getEntity().getNotes().size() +
+// ", successfulListenerWrites: " + successfulListenerWrites.get() + ", successfulListenerWrites2: " + successfulListenerWrites2.get() +
+// ", failedReplaceCounter: " + failedReplaceCounter.get() + ", failedReplaceCounter2: " + failedReplaceCounter2.get());
+
+
+ }
+
+
+ private static SessionEntityWrapper<UserSessionEntity> createSessionEntity(String sessionId) {
+ // Create 100 initial sessions
+ UserSessionEntity session = new UserSessionEntity();
+ session.setId(sessionId);
+ session.setRealm("foo");
+ session.setBrokerSessionId("!23123123");
+ session.setBrokerUserId(null);
+ session.setUser("foo");
+ session.setLoginUsername("foo");
+ session.setIpAddress("123.44.143.178");
+ session.setStarted(Time.currentTime());
+ session.setLastSessionRefresh(Time.currentTime());
+
+ AuthenticatedClientSessionEntity clientSession = new AuthenticatedClientSessionEntity();
+ clientSession.setAuthMethod("saml");
+ clientSession.setAction("something");
+ clientSession.setTimestamp(1234);
+ clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2")));
+ clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2")));
+ session.getAuthenticatedClientSessions().put("client1", clientSession);
+
+ SessionEntityWrapper<UserSessionEntity> wrappedSession = new SessionEntityWrapper<>(session);
+ return wrappedSession;
+ }
+
+
+ private static Thread createWorker(Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, int threadId) {
+ System.out.println("Retrieved cache: " + threadId);
+
+ RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache);
+
+ if (threadId == 1) {
+ remoteCache1 = remoteCache;
+ } else {
+ remoteCache2 = remoteCache;
+ }
+
+ AtomicInteger counter = threadId ==1 ? successfulListenerWrites : successfulListenerWrites2;
+ HotRodListener listener = new HotRodListener(cache, remoteCache, counter);
+ remoteCache.addClientListener(listener);
+
+ return new RemoteCacheWorker(remoteCache, threadId);
+ //return new CacheWorker(cache, threadId);
+ }
+
+
+ private static EmbeddedCacheManager createManager(int threadId) {
+ return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class);
+ }
+
+
+ @ClientListener
+ public static class HotRodListener {
+
+ private Cache<String, SessionEntityWrapper<UserSessionEntity>> origCache;
+ private RemoteCache remoteCache;
+ private AtomicInteger listenerCount;
+
+ public HotRodListener(Cache<String, SessionEntityWrapper<UserSessionEntity>> origCache, RemoteCache remoteCache, AtomicInteger listenerCount) {
+ this.listenerCount = listenerCount;
+ this.remoteCache = remoteCache;
+ this.origCache = origCache;
+ }
+
+
+ @ClientCacheEntryCreated
+ public void created(ClientCacheEntryCreatedEvent event) {
+ String cacheKey = (String) event.getKey();
+
+ logger.infof("Listener executed for creating of session %s", cacheKey);
+ }
+
+
+ @ClientCacheEntryModified
+ public void modified(ClientCacheEntryModifiedEvent event) {
+ String cacheKey = (String) event.getKey();
+
+ logger.infof("Listener executed for modifying of session %s", cacheKey);
+ }
+
+
+ @ClientCacheEntryRemoved
+ public void removed(ClientCacheEntryRemovedEvent event) {
+ String cacheKey = (String) event.getKey();
+
+ logger.infof("Listener executed for removing of session %s", cacheKey);
+
+ // TODO: for distributed caches, ensure that it is executed just on owner OR if event.isCommandRetried
+ origCache
+ .getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE)
+ .remove(cacheKey);
+
+ }
+
+ }
+
+ private static class RemoteCacheWorker extends Thread {
+
+ private final RemoteCache<String, Object> remoteCache;
+
+ private final int myThreadId;
+
+ private RemoteCacheWorker(RemoteCache remoteCache, int myThreadId) {
+ this.remoteCache = remoteCache;
+ this.myThreadId = myThreadId;
+ }
+
+ @Override
+ public void run() {
+
+ for (int i=0 ; i<ITERATIONS ; i++) {
+ String sessionId = String.valueOf(i);
+ remoteCache.remove(sessionId);
+
+
+ logger.infof("Session %s removed on DC1", sessionId);
+
+ // Check if it's immediately seen that session is removed on 2nd DC
+ RemoteCache secondDCRemoteCache = myThreadId == 1 ? remoteCache2 : remoteCache1;
+ SessionEntityWrapper thatSession = (SessionEntityWrapper) secondDCRemoteCache.get(sessionId);
+ Assert.assertNull("Session with ID " + sessionId + " not removed on the other DC. ThreadID: " + myThreadId, thatSession);
+
+ // Also check that it's immediatelly removed on my DC
+ SessionEntityWrapper mySession = (SessionEntityWrapper) remoteCache.get(sessionId);
+ Assert.assertNull("Session with ID " + sessionId + " not removed on the other DC. ThreadID: " + myThreadId, mySession);
+ }
+
+ }
+
+ }
+
+}
diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java
new file mode 100644
index 0000000..dd34b19
--- /dev/null
+++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.cluster.infinispan;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.VersionedValue;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
+import org.infinispan.client.hotrod.annotation.ClientListener;
+import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.configuration.cache.ConfigurationBuilder;
+import org.infinispan.configuration.global.GlobalConfigurationBuilder;
+import org.infinispan.context.Flag;
+import org.infinispan.manager.DefaultCacheManager;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.persistence.manager.PersistenceManager;
+import org.infinispan.persistence.remote.RemoteStore;
+import org.infinispan.persistence.remote.configuration.ExhaustedAction;
+import org.jboss.logging.Logger;
+import org.junit.Assert;
+import org.keycloak.common.util.Time;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import org.keycloak.models.sessions.infinispan.remotestore.KcRemoteStore;
+import org.keycloak.models.sessions.infinispan.remotestore.KcRemoteStoreConfigurationBuilder;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+
+/**
+ * Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG. Especially tests "replaceWithVersion" contract.
+ *
+ * Steps: {@see ConcurrencyJDGRemoteCacheClientListenersTest}
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ConcurrencyJDGSessionsCacheTest {
+
+ protected static final Logger logger = Logger.getLogger(ConcurrencyJDGSessionsCacheTest.class);
+
+ private static final int ITERATION_PER_WORKER = 1000;
+
+ private static RemoteCache remoteCache1;
+ private static RemoteCache remoteCache2;
+
+ private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0);
+ private static final AtomicInteger failedReplaceCounter2 = new AtomicInteger(0);
+
+ private static final AtomicInteger successfulListenerWrites = new AtomicInteger(0);
+ private static final AtomicInteger successfulListenerWrites2 = new AtomicInteger(0);
+
+ //private static Map<String, EntryInfo> state = new HashMap<>();
+
+ public static void main(String[] args) throws Exception {
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> cache1 = createManager(1).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> cache2 = createManager(2).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+
+ // Create initial item
+ UserSessionEntity session = new UserSessionEntity();
+ session.setId("123");
+ session.setRealm("foo");
+ session.setBrokerSessionId("!23123123");
+ session.setBrokerUserId(null);
+ session.setUser("foo");
+ session.setLoginUsername("foo");
+ session.setIpAddress("123.44.143.178");
+ session.setStarted(Time.currentTime());
+ session.setLastSessionRefresh(Time.currentTime());
+
+ AuthenticatedClientSessionEntity clientSession = new AuthenticatedClientSessionEntity();
+ clientSession.setAuthMethod("saml");
+ clientSession.setAction("something");
+ clientSession.setTimestamp(1234);
+ clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2")));
+ clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2")));
+ session.getAuthenticatedClientSessions().put("client1", clientSession);
+
+ SessionEntityWrapper<UserSessionEntity> wrappedSession = new SessionEntityWrapper<>(session);
+
+ // Some dummy testing of remoteStore behaviour
+ logger.info("Before put");
+
+ cache1
+ .getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL) // will still invoke remoteStore . Just doesn't propagate to cluster
+ .put("123", wrappedSession);
+
+ logger.info("After put");
+
+ cache1.replace("123", wrappedSession);
+
+ logger.info("After replace");
+
+ cache1.get("123");
+
+ logger.info("After cache1.get");
+
+ cache2.get("123");
+
+ logger.info("After cache2.get");
+
+ cache1.get("123");
+
+ logger.info("After cache1.get - second call");
+
+ cache2.get("123");
+
+ logger.info("After cache2.get - second call");
+
+ cache2.replace("123", wrappedSession);
+
+ logger.info("After replace - second call");
+
+ cache1.get("123");
+
+ logger.info("After cache1.get - third call");
+
+ cache2.get("123");
+
+ logger.info("After cache2.get - third call");
+
+ cache1
+ .getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD)
+ .entrySet().stream().forEach(e -> {
+ });
+
+ logger.info("After cache1.stream");
+
+ // Explicitly call put on remoteCache (KcRemoteCache.write ignores remote writes)
+ InfinispanUtil.getRemoteCache(cache1).put("123", session);
+
+ // Create caches, listeners and finally worker threads
+ Thread worker1 = createWorker(cache1, 1);
+ Thread worker2 = createWorker(cache2, 2);
+
+ long start = System.currentTimeMillis();
+
+ // Start and join workers
+ worker1.start();
+ worker2.start();
+
+ worker1.join();
+ worker2.join();
+
+ long took = System.currentTimeMillis() - start;
+
+// // Output
+// for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+// System.out.println(entry.getKey() + ":::" + entry.getValue());
+// worker1.cache.remove(entry.getKey());
+// }
+
+ System.out.println("Finished. Took: " + took + " ms. Notes: " + cache1.get("123").getEntity().getNotes().size() +
+ ", successfulListenerWrites: " + successfulListenerWrites.get() + ", successfulListenerWrites2: " + successfulListenerWrites2.get() +
+ ", failedReplaceCounter: " + failedReplaceCounter.get() + ", failedReplaceCounter2: " + failedReplaceCounter2.get() );
+
+ System.out.println("Sleeping before other report");
+
+ Thread.sleep(1000);
+
+ System.out.println("Finished. Took: " + took + " ms. Notes: " + cache1.get("123").getEntity().getNotes().size() +
+ ", successfulListenerWrites: " + successfulListenerWrites.get() + ", successfulListenerWrites2: " + successfulListenerWrites2.get() +
+ ", failedReplaceCounter: " + failedReplaceCounter.get() + ", failedReplaceCounter2: " + failedReplaceCounter2.get());
+
+
+
+ // Finish JVM
+ cache1.getCacheManager().stop();
+ cache2.getCacheManager().stop();
+ }
+
+ private static Thread createWorker(Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, int threadId) {
+ System.out.println("Retrieved cache: " + threadId);
+
+ RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache);
+
+ if (threadId == 1) {
+ remoteCache1 = remoteCache;
+ } else {
+ remoteCache2 = remoteCache;
+ }
+
+ AtomicInteger counter = threadId ==1 ? successfulListenerWrites : successfulListenerWrites2;
+ HotRodListener listener = new HotRodListener(cache, remoteCache, counter);
+ remoteCache.addClientListener(listener);
+
+ return new RemoteCacheWorker(remoteCache, threadId);
+ //return new CacheWorker(cache, threadId);
+ }
+
+
+ private static EmbeddedCacheManager createManager(int threadId) {
+ return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.SESSION_CACHE_NAME, KcRemoteStoreConfigurationBuilder.class);
+ }
+
+
+ @ClientListener
+ public static class HotRodListener {
+
+ private Cache<String, SessionEntityWrapper<UserSessionEntity>> origCache;
+ private RemoteCache remoteCache;
+ private AtomicInteger listenerCount;
+
+ public HotRodListener(Cache<String, SessionEntityWrapper<UserSessionEntity>> origCache, RemoteCache remoteCache, AtomicInteger listenerCount) {
+ this.listenerCount = listenerCount;
+ this.remoteCache = remoteCache;
+ this.origCache = origCache;
+ }
+
+ @ClientCacheEntryCreated
+ public void created(ClientCacheEntryCreatedEvent event) {
+ String cacheKey = (String) event.getKey();
+ listenerCount.incrementAndGet();
+ }
+
+ @ClientCacheEntryModified
+ public void updated(ClientCacheEntryModifiedEvent event) {
+ String cacheKey = (String) event.getKey();
+ listenerCount.incrementAndGet();
+
+ // TODO: can be optimized
+ SessionEntity session = (SessionEntity) remoteCache.get(cacheKey);
+ SessionEntityWrapper sessionWrapper = new SessionEntityWrapper(session);
+
+ // TODO: for distributed caches, ensure that it is executed just on owner OR if event.isCommandRetried
+ origCache
+ .getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE)
+ .replace(cacheKey, sessionWrapper);
+ }
+
+
+
+
+ }
+
+ private static class RemoteCacheWorker extends Thread {
+
+ private final RemoteCache<String, UserSessionEntity> remoteCache;
+
+ private final int myThreadId;
+
+ private RemoteCacheWorker(RemoteCache remoteCache, int myThreadId) {
+ this.remoteCache = remoteCache;
+ this.myThreadId = myThreadId;
+ }
+
+ @Override
+ public void run() {
+
+ for (int i=0 ; i<ITERATION_PER_WORKER ; i++) {
+
+ String noteKey = "n-" + myThreadId + "-" + i;
+
+ boolean replaced = false;
+ while (!replaced) {
+ VersionedValue<UserSessionEntity> versioned = remoteCache.getVersioned("123");
+ UserSessionEntity oldSession = versioned.getValue();
+ //UserSessionEntity clone = DistributedCacheConcurrentWritesTest.cloneSession(oldSession);
+ UserSessionEntity clone = oldSession;
+
+ clone.getNotes().put(noteKey, "someVal");
+ //cache.replace("123", clone);
+ replaced = cacheReplace(versioned, clone);
+ }
+
+ // Try to see if remoteCache on 2nd DC is immediatelly seeing our change
+ RemoteCache secondDCRemoteCache = myThreadId == 1 ? remoteCache2 : remoteCache1;
+ UserSessionEntity thatSession = (UserSessionEntity) secondDCRemoteCache.get("123");
+
+ Assert.assertEquals("someVal", thatSession.getNotes().get(noteKey));
+ //System.out.println("Passed");
+ }
+
+ }
+
+ private boolean cacheReplace(VersionedValue<UserSessionEntity> oldSession, UserSessionEntity newSession) {
+ try {
+ boolean replaced = remoteCache.replaceWithVersion("123", newSession, oldSession.getVersion());
+ //cache.replace("123", newSession);
+ if (!replaced) {
+ failedReplaceCounter.incrementAndGet();
+ //return false;
+ //System.out.println("Replace failed!!!");
+ }
+ return replaced;
+ } catch (Exception re) {
+ failedReplaceCounter2.incrementAndGet();
+ return false;
+ }
+ //return replaced;
+ }
+
+ }
+/*
+ // Worker, which operates on "classic" cache and rely on operations delegated to the second cache
+ private static class CacheWorker extends Thread {
+
+ private final Cache<String, SessionEntityWrapper<UserSessionEntity>> cache;
+
+ private final int myThreadId;
+
+ private CacheWorker(Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, int myThreadId) {
+ this.cache = cache;
+ this.myThreadId = myThreadId;
+ }
+
+ @Override
+ public void run() {
+
+ for (int i=0 ; i<ITERATION_PER_WORKER ; i++) {
+
+ String noteKey = "n-" + myThreadId + "-" + i;
+
+ boolean replaced = false;
+ while (!replaced) {
+ VersionedValue<UserSessionEntity> versioned = cache.getVersioned("123");
+ UserSessionEntity oldSession = versioned.getValue();
+ //UserSessionEntity clone = DistributedCacheConcurrentWritesTest.cloneSession(oldSession);
+ UserSessionEntity clone = oldSession;
+
+ clone.getNotes().put(noteKey, "someVal");
+ //cache.replace("123", clone);
+ replaced = cacheReplace(versioned, clone);
+ }
+ }
+
+ }
+
+ }*/
+
+
+}
diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/TestCacheManagerFactory.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/TestCacheManagerFactory.java
new file mode 100644
index 0000000..06dd95f
--- /dev/null
+++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/TestCacheManagerFactory.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.cluster.infinispan;
+
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.configuration.cache.ConfigurationBuilder;
+import org.infinispan.configuration.global.GlobalConfigurationBuilder;
+import org.infinispan.manager.DefaultCacheManager;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.persistence.remote.configuration.ExhaustedAction;
+import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+class TestCacheManagerFactory {
+
+
+ <T extends RemoteStoreConfigurationBuilder> EmbeddedCacheManager createManager(int threadId, String cacheName, Class<T> builderClass) {
+ System.setProperty("java.net.preferIPv4Stack", "true");
+ System.setProperty("jgroups.tcp.port", "53715");
+ GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
+
+ boolean clustered = false;
+ boolean async = false;
+ boolean allowDuplicateJMXDomains = true;
+
+ if (clustered) {
+ gcb = gcb.clusteredDefault();
+ gcb.transport().clusterName("test-clustering");
+ }
+
+ gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
+
+ EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build());
+
+ Configuration invalidationCacheConfiguration = getCacheBackedByRemoteStore(threadId, cacheName, builderClass);
+
+ cacheManager.defineConfiguration(cacheName, invalidationCacheConfiguration);
+ return cacheManager;
+
+ }
+
+
+ private <T extends RemoteStoreConfigurationBuilder> Configuration getCacheBackedByRemoteStore(int threadId, String cacheName, Class<T> builderClass) {
+ ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder();
+
+ int port = threadId==1 ? 12232 : 13232;
+ //int port = 12232;
+
+ return cacheConfigBuilder.persistence().addStore(builderClass)
+ .fetchPersistentState(false)
+ .ignoreModifications(false)
+ .purgeOnStartup(false)
+ .preload(false)
+ .shared(true)
+ .remoteCacheName(cacheName)
+ .rawValues(true)
+ .forceReturnValues(false)
+ .marshaller(KeycloakHotRodMarshallerFactory.class.getName())
+ .addServer()
+ .host("localhost")
+ .port(port)
+ .connectionPool()
+ .maxActive(20)
+ .exhaustedAction(ExhaustedAction.CREATE_NEW)
+ .async()
+ . enabled(false).build();
+ }
+}
diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java
new file mode 100644
index 0000000..80bcd8b
--- /dev/null
+++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.initializer;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.infinispan.Cache;
+import org.infinispan.configuration.cache.CacheMode;
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.configuration.cache.ConfigurationBuilder;
+import org.infinispan.configuration.global.GlobalConfigurationBuilder;
+import org.infinispan.manager.DefaultCacheManager;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
+import org.jgroups.JChannel;
+import org.junit.Ignore;
+import org.keycloak.common.util.Time;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+
+/**
+ * Test concurrent writes to distributed cache with usage of atomic replace
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Ignore
+public class DistributedCacheConcurrentWritesTest {
+
+ private static final int ITERATION_PER_WORKER = 1000;
+
+ private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0);
+ private static final AtomicInteger failedReplaceCounter2 = new AtomicInteger(0);
+
+ public static void main(String[] args) throws Exception {
+ CacheWrapper<String, UserSessionEntity> cache1 = createCache("node1");
+ CacheWrapper<String, UserSessionEntity> cache2 = createCache("node2");
+
+ // Create initial item
+ UserSessionEntity session = new UserSessionEntity();
+ session.setId("123");
+ session.setRealm("foo");
+ session.setBrokerSessionId("!23123123");
+ session.setBrokerUserId(null);
+ session.setUser("foo");
+ session.setLoginUsername("foo");
+ session.setIpAddress("123.44.143.178");
+ session.setStarted(Time.currentTime());
+ session.setLastSessionRefresh(Time.currentTime());
+
+ AuthenticatedClientSessionEntity clientSession = new AuthenticatedClientSessionEntity();
+ clientSession.setAuthMethod("saml");
+ clientSession.setAction("something");
+ clientSession.setTimestamp(1234);
+ clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2")));
+ clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2")));
+ session.getAuthenticatedClientSessions().put("client1", clientSession);
+
+ cache1.put("123", session);
+
+ // Create 2 workers for concurrent write and start them
+ Worker worker1 = new Worker(1, cache1);
+ Worker worker2 = new Worker(2, cache2);
+
+ long start = System.currentTimeMillis();
+
+ System.out.println("Started clustering test");
+
+ worker1.start();
+ //worker1.join();
+ worker2.start();
+
+ worker1.join();
+ worker2.join();
+
+ long took = System.currentTimeMillis() - start;
+ session = cache1.get("123").getEntity();
+ System.out.println("Took: " + took + " ms. Notes count: " + session.getNotes().size() + ", failedReplaceCounter: " + failedReplaceCounter.get()
+ + ", failedReplaceCounter2: " + failedReplaceCounter2.get());
+
+ // JGroups statistics
+ JChannel channel = (JChannel)((JGroupsTransport)cache1.wrappedCache.getAdvancedCache().getRpcManager().getTransport()).getChannel();
+ System.out.println("Sent MB: " + channel.getSentBytes() / 1000000 + ", sent messages: " + channel.getSentMessages() + ", received MB: " + channel.getReceivedBytes() / 1000000 +
+ ", received messages: " + channel.getReceivedMessages());
+
+ // Kill JVM
+ cache1.getCache().stop();
+ cache2.getCache().stop();
+ cache1.getCache().getCacheManager().stop();
+ cache2.getCache().getCacheManager().stop();
+
+ System.out.println("Managers killed");
+ }
+
+
+ private static class Worker extends Thread {
+
+ private final CacheWrapper<String, UserSessionEntity> cache;
+ private final int threadId;
+
+ public Worker(int threadId, CacheWrapper<String, UserSessionEntity> cache) {
+ this.threadId = threadId;
+ this.cache = cache;
+ }
+
+ @Override
+ public void run() {
+
+ for (int i=0 ; i<ITERATION_PER_WORKER ; i++) {
+
+ String noteKey = "n-" + threadId + "-" + i;
+
+ boolean replaced = false;
+ while (!replaced) {
+ SessionEntityWrapper<UserSessionEntity> oldWrapped = cache.get("123");
+ UserSessionEntity oldSession = oldWrapped.getEntity();
+ //UserSessionEntity clone = DistributedCacheConcurrentWritesTest.cloneSession(oldSession);
+ UserSessionEntity clone = oldSession;
+
+ clone.getNotes().put(noteKey, "someVal");
+ //cache.replace("123", clone);
+ replaced = cacheReplace(oldWrapped, clone);
+ }
+ }
+
+ }
+
+ private boolean cacheReplace(SessionEntityWrapper<UserSessionEntity> oldSession, UserSessionEntity newSession) {
+ try {
+ boolean replaced = cache.replace("123", oldSession, newSession);
+ //cache.replace("123", newSession);
+ if (!replaced) {
+ failedReplaceCounter.incrementAndGet();
+ //return false;
+ //System.out.println("Replace failed!!!");
+ }
+ return replaced;
+ } catch (Exception re) {
+ failedReplaceCounter2.incrementAndGet();
+ return false;
+ }
+ //return replaced;
+ }
+
+ }
+
+ // Session clone
+
+ private static UserSessionEntity cloneSession(UserSessionEntity session) {
+ UserSessionEntity clone = new UserSessionEntity();
+ clone.setId(session.getId());
+ clone.setRealm(session.getRealm());
+ clone.setNotes(new ConcurrentHashMap<>(session.getNotes()));
+ return clone;
+ }
+
+
+ // Cache creation utils
+
+ public static class CacheWrapper<K, V extends SessionEntity> {
+
+ private final Cache<K, SessionEntityWrapper<V>> wrappedCache;
+
+ public CacheWrapper(Cache<K, SessionEntityWrapper<V>> wrappedCache) {
+ this.wrappedCache = wrappedCache;
+ }
+
+
+ public SessionEntityWrapper<V> get(K key) {
+ SessionEntityWrapper<V> val = wrappedCache.get(key);
+ return val;
+ }
+
+ public void put(K key, V newVal) {
+ SessionEntityWrapper<V> newWrapper = new SessionEntityWrapper<>(newVal);
+ wrappedCache.put(key, newWrapper);
+ }
+
+
+ public boolean replace(K key, SessionEntityWrapper<V> oldVal, V newVal) {
+ SessionEntityWrapper<V> newWrapper = new SessionEntityWrapper<>(newVal);
+ return wrappedCache.replace(key, oldVal, newWrapper);
+ }
+
+ private Cache<K, SessionEntityWrapper<V>> getCache() {
+ return wrappedCache;
+ }
+
+ }
+
+
+ public static CacheWrapper<String, UserSessionEntity> createCache(String nodeName) {
+ EmbeddedCacheManager mgr = createManager(nodeName);
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> wrapped = mgr.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+ return new CacheWrapper<>(wrapped);
+ }
+
+
+ public static EmbeddedCacheManager createManager(String nodeName) {
+ System.setProperty("java.net.preferIPv4Stack", "true");
+ System.setProperty("jgroups.tcp.port", "53715");
+ GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
+
+ boolean clustered = true;
+ boolean async = false;
+ boolean allowDuplicateJMXDomains = true;
+
+ if (clustered) {
+ gcb = gcb.clusteredDefault();
+ gcb.transport().clusterName("test-clustering");
+ gcb.transport().nodeName(nodeName);
+ }
+ gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
+
+ EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build());
+
+
+ ConfigurationBuilder distConfigBuilder = new ConfigurationBuilder();
+ if (clustered) {
+ distConfigBuilder.clustering().cacheMode(async ? CacheMode.DIST_ASYNC : CacheMode.DIST_SYNC);
+ distConfigBuilder.clustering().hash().numOwners(1);
+
+ // Disable L1 cache
+ distConfigBuilder.clustering().hash().l1().enabled(false);
+ }
+ Configuration distConfig = distConfigBuilder.build();
+
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, distConfig);
+ return cacheManager;
+
+ }
+}
diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java
new file mode 100644
index 0000000..89e7750
--- /dev/null
+++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.initializer;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.infinispan.Cache;
+import org.infinispan.configuration.cache.CacheMode;
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.configuration.cache.ConfigurationBuilder;
+import org.infinispan.configuration.cache.VersioningScheme;
+import org.infinispan.configuration.global.GlobalConfigurationBuilder;
+import org.infinispan.context.Flag;
+import org.infinispan.manager.DefaultCacheManager;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
+import org.infinispan.transaction.LockingMode;
+import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
+import org.infinispan.util.concurrent.IsolationLevel;
+import org.jgroups.JChannel;
+import org.junit.Ignore;
+import org.keycloak.common.util.Time;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+
+/**
+ * Test concurrent writes to distributed cache with usage of write skew
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Ignore
+public class DistributedCacheWriteSkewTest {
+
+ private static final int ITERATION_PER_WORKER = 1000;
+
+ private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0);
+
+ public static void main(String[] args) throws Exception {
+ Cache<String, UserSessionEntity> cache1 = createManager("node1").getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+ Cache<String, UserSessionEntity> cache2 = createManager("node2").getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+
+ // Create initial item
+ UserSessionEntity session = new UserSessionEntity();
+ session.setId("123");
+ session.setRealm("foo");
+ session.setBrokerSessionId("!23123123");
+ session.setBrokerUserId(null);
+ session.setUser("foo");
+ session.setLoginUsername("foo");
+ session.setIpAddress("123.44.143.178");
+ session.setStarted(Time.currentTime());
+ session.setLastSessionRefresh(Time.currentTime());
+
+ AuthenticatedClientSessionEntity clientSession = new AuthenticatedClientSessionEntity();
+ clientSession.setAuthMethod("saml");
+ clientSession.setAction("something");
+ clientSession.setTimestamp(1234);
+ clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2")));
+ clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2")));
+ session.getAuthenticatedClientSessions().put("client1", clientSession);
+
+ cache1.put("123", session);
+
+ //cache1.replace("123", session);
+
+ // Create 2 workers for concurrent write and start them
+ Worker worker1 = new Worker(1, cache1);
+ Worker worker2 = new Worker(2, cache2);
+
+ long start = System.currentTimeMillis();
+
+ System.out.println("Started clustering test");
+
+ worker1.start();
+ //worker1.join();
+ worker2.start();
+
+ worker1.join();
+ worker2.join();
+
+ long took = System.currentTimeMillis() - start;
+ session = cache1.get("123");
+ System.out.println("Took: " + took + " ms. Notes count: " + session.getNotes().size() + ", failedReplaceCounter: " + failedReplaceCounter.get());
+
+ // JGroups statistics
+ JChannel channel = (JChannel)((JGroupsTransport)cache1.getAdvancedCache().getRpcManager().getTransport()).getChannel();
+ System.out.println("Sent MB: " + channel.getSentBytes() / 1000000 + ", sent messages: " + channel.getSentMessages() + ", received MB: " + channel.getReceivedBytes() / 1000000 +
+ ", received messages: " + channel.getReceivedMessages());
+
+ // Kill JVM
+ cache1.stop();
+ cache2.stop();
+ cache1.getCacheManager().stop();
+ cache2.getCacheManager().stop();
+
+ System.out.println("Managers killed");
+ }
+
+
+ private static class Worker extends Thread {
+
+ private final Cache<String, UserSessionEntity> cache;
+ private final int threadId;
+
+ public Worker(int threadId, Cache<String, UserSessionEntity> cache) {
+ this.threadId = threadId;
+ this.cache = cache;
+ }
+
+ @Override
+ public void run() {
+
+ for (int i=0 ; i<ITERATION_PER_WORKER ; i++) {
+
+ String noteKey = "n-" + threadId + "-" + i;
+
+ boolean replaced = false;
+ while (!replaced) {
+ try {
+ //cache.startBatch();
+
+ UserSessionEntity oldSession = cache.get("123");
+
+ //UserSessionEntity clone = DistributedCacheConcurrentWritesTest.cloneSession(oldSession);
+ UserSessionEntity clone = oldSession;
+
+ clone.getNotes().put(noteKey, "someVal");
+
+ cache.replace("123", clone);
+ //cache.getAdvancedCache().withFlags(Flag.FAIL_SILENTLY).endBatch(true);
+ replaced = true;
+ } catch (Exception e) {
+ System.out.println(e);
+ failedReplaceCounter.incrementAndGet();
+ }
+
+ }
+ }
+
+ }
+
+ }
+
+
+ public static EmbeddedCacheManager createManager(String nodeName) {
+ System.setProperty("java.net.preferIPv4Stack", "true");
+ System.setProperty("jgroups.tcp.port", "53715");
+ GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
+
+ boolean clustered = true;
+ boolean async = false;
+ boolean allowDuplicateJMXDomains = true;
+
+ if (clustered) {
+ gcb = gcb.clusteredDefault();
+ gcb.transport().clusterName("test-clustering");
+ gcb.transport().nodeName(nodeName);
+ }
+ gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
+
+ EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build());
+
+
+ ConfigurationBuilder distConfigBuilder = new ConfigurationBuilder();
+ if (clustered) {
+ distConfigBuilder.clustering().cacheMode(async ? CacheMode.DIST_ASYNC : CacheMode.DIST_SYNC);
+ distConfigBuilder.clustering().hash().numOwners(1);
+
+ // Disable L1 cache
+ distConfigBuilder.clustering().hash().l1().enabled(false);
+
+ //distConfigBuilder.storeAsBinary().enable().storeKeysAsBinary(false).storeValuesAsBinary(true);
+
+ distConfigBuilder.versioning().enabled(true);
+ distConfigBuilder.versioning().scheme(VersioningScheme.SIMPLE);
+
+ distConfigBuilder.locking().writeSkewCheck(true);
+ distConfigBuilder.locking().isolationLevel(IsolationLevel.REPEATABLE_READ);
+ distConfigBuilder.locking().concurrencyLevel(32);
+ distConfigBuilder.locking().lockAcquisitionTimeout(1000, TimeUnit.SECONDS);
+
+ distConfigBuilder.versioning().enabled(true);
+ distConfigBuilder.versioning().scheme(VersioningScheme.SIMPLE);
+
+
+ // distConfigBuilder.invocationBatching().enable();
+ //distConfigBuilder.transaction().transactionMode(TransactionMode.TRANSACTIONAL);
+ distConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
+ distConfigBuilder.transaction().lockingMode(LockingMode.OPTIMISTIC);
+ }
+ Configuration distConfig = distConfigBuilder.build();
+
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, distConfig);
+ return cacheManager;
+
+ }
+}
model/jpa/pom.xml 2(+1 -1)
diff --git a/model/jpa/pom.xml b/model/jpa/pom.xml
index 051c9dd..54166f5 100755
--- a/model/jpa/pom.xml
+++ b/model/jpa/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java
index eb350be..1bd41e2 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java
@@ -24,6 +24,7 @@ import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
+import javax.persistence.FlushModeType;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
@@ -34,14 +35,10 @@ import javax.persistence.criteria.Root;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.jpa.entities.PolicyEntity;
-import org.keycloak.authorization.jpa.entities.ResourceServerEntity;
import org.keycloak.authorization.model.Policy;
-import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.store.PolicyStore;
-import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
/**
@@ -96,8 +93,10 @@ public class JPAPolicyStore implements PolicyStore {
public Policy findByName(String name, String resourceServerId) {
TypedQuery<String> query = entityManager.createNamedQuery("findPolicyIdByName", String.class);
+ query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("serverId", resourceServerId);
query.setParameter("name", name);
+
try {
String id = query.getSingleResult();
return provider.getStoreFactory().getPolicyStore().findById(id, resourceServerId);
@@ -167,6 +166,7 @@ public class JPAPolicyStore implements PolicyStore {
public List<Policy> findByResource(final String resourceId, String resourceServerId) {
TypedQuery<String> query = entityManager.createNamedQuery("findPolicyIdByResource", String.class);
+ query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("resourceId", resourceId);
query.setParameter("serverId", resourceServerId);
@@ -182,6 +182,7 @@ public class JPAPolicyStore implements PolicyStore {
public List<Policy> findByResourceType(final String resourceType, String resourceServerId) {
TypedQuery<String> query = entityManager.createNamedQuery("findPolicyIdByResourceType", String.class);
+ query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("type", resourceType);
query.setParameter("serverId", resourceServerId);
@@ -202,6 +203,7 @@ public class JPAPolicyStore implements PolicyStore {
// Use separate subquery to handle DB2 and MSSSQL
TypedQuery<String> query = entityManager.createNamedQuery("findPolicyIdByScope", String.class);
+ query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("scopeIds", scopeIds);
query.setParameter("serverId", resourceServerId);
@@ -217,6 +219,7 @@ public class JPAPolicyStore implements PolicyStore {
public List<Policy> findByType(String type, String resourceServerId) {
TypedQuery<String> query = entityManager.createNamedQuery("findPolicyIdByType", String.class);
+ query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("serverId", resourceServerId);
query.setParameter("type", type);
@@ -233,6 +236,7 @@ public class JPAPolicyStore implements PolicyStore {
TypedQuery<String> query = entityManager.createNamedQuery("findPolicyIdByDependentPolices", String.class);
+ query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("serverId", resourceServerId);
query.setParameter("policyId", policyId);
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
index 8a647d8..7a505ab 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
@@ -19,13 +19,13 @@ package org.keycloak.authorization.jpa.store;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.jpa.entities.ResourceEntity;
-import org.keycloak.authorization.jpa.entities.ResourceServerEntity;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager;
+import javax.persistence.FlushModeType;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
@@ -34,7 +34,6 @@ import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -62,13 +61,14 @@ public class JPAResourceStore implements ResourceStore {
entity.setOwner(owner);
this.entityManager.persist(entity);
+ this.entityManager.flush();
return new ResourceAdapter(entity, entityManager, provider.getStoreFactory());
}
@Override
public void delete(String id) {
- ResourceEntity resource = entityManager.find(ResourceEntity.class, id);
+ ResourceEntity resource = entityManager.getReference(ResourceEntity.class, id);
if (resource == null) return;
resource.getScopes().clear();
@@ -90,14 +90,18 @@ public class JPAResourceStore implements ResourceStore {
public List<Resource> findByOwner(String ownerId, String resourceServerId) {
TypedQuery<String> query = entityManager.createNamedQuery("findResourceIdByOwner", String.class);
+ query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("owner", ownerId);
query.setParameter("serverId", resourceServerId);
List<String> result = query.getResultList();
List<Resource> list = new LinkedList<>();
+ ResourceStore resourceStore = provider.getStoreFactory().getResourceStore();
+
for (String id : result) {
- list.add(provider.getStoreFactory().getResourceStore().findById(id, resourceServerId));
+ list.add(resourceStore.findById(id, resourceServerId));
}
+
return list;
}
@@ -105,14 +109,18 @@ public class JPAResourceStore implements ResourceStore {
public List<Resource> findByUri(String uri, String resourceServerId) {
TypedQuery<String> query = entityManager.createNamedQuery("findResourceIdByUri", String.class);
+ query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("uri", uri);
query.setParameter("serverId", resourceServerId);
List<String> result = query.getResultList();
List<Resource> list = new LinkedList<>();
+ ResourceStore resourceStore = provider.getStoreFactory().getResourceStore();
+
for (String id : result) {
- list.add(provider.getStoreFactory().getResourceStore().findById(id, resourceServerId));
+ list.add(resourceStore.findById(id, resourceServerId));
}
+
return list;
}
@@ -124,9 +132,12 @@ public class JPAResourceStore implements ResourceStore {
List<String> result = query.getResultList();
List<Resource> list = new LinkedList<>();
+ ResourceStore resourceStore = provider.getStoreFactory().getResourceStore();
+
for (String id : result) {
- list.add(provider.getStoreFactory().getResourceStore().findById(id, resourceServerId));
+ list.add(resourceStore.findById(id, resourceServerId));
}
+
return list;
}
@@ -163,9 +174,12 @@ public class JPAResourceStore implements ResourceStore {
List<String> result = query.getResultList();
List<Resource> list = new LinkedList<>();
+ ResourceStore resourceStore = provider.getStoreFactory().getResourceStore();
+
for (String id : result) {
- list.add(provider.getStoreFactory().getResourceStore().findById(id, resourceServerId));
+ list.add(resourceStore.findById(id, resourceServerId));
}
+
return list;
}
@@ -173,14 +187,18 @@ public class JPAResourceStore implements ResourceStore {
public List<Resource> findByScope(List<String> scopes, String resourceServerId) {
TypedQuery<String> query = entityManager.createNamedQuery("findResourceIdByScope", String.class);
+ query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("scopeIds", scopes);
query.setParameter("serverId", resourceServerId);
List<String> result = query.getResultList();
List<Resource> list = new LinkedList<>();
+ ResourceStore resourceStore = provider.getStoreFactory().getResourceStore();
+
for (String id : result) {
- list.add(provider.getStoreFactory().getResourceStore().findById(id, resourceServerId));
+ list.add(resourceStore.findById(id, resourceServerId));
}
+
return list;
}
@@ -188,8 +206,10 @@ public class JPAResourceStore implements ResourceStore {
public Resource findByName(String name, String resourceServerId) {
TypedQuery<String> query = entityManager.createNamedQuery("findResourceIdByName", String.class);
+ query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("serverId", resourceServerId);
query.setParameter("name", name);
+
try {
String id = query.getSingleResult();
return provider.getStoreFactory().getResourceStore().findById(id, resourceServerId);
@@ -202,14 +222,18 @@ public class JPAResourceStore implements ResourceStore {
public List<Resource> findByType(String type, String resourceServerId) {
TypedQuery<String> query = entityManager.createNamedQuery("findResourceIdByType", String.class);
+ query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("type", type);
query.setParameter("serverId", resourceServerId);
List<String> result = query.getResultList();
List<Resource> list = new LinkedList<>();
+ ResourceStore resourceStore = provider.getStoreFactory().getResourceStore();
+
for (String id : result) {
- list.add(provider.getStoreFactory().getResourceStore().findById(id, resourceServerId));
+ list.add(resourceStore.findById(id, resourceServerId));
}
+
return list;
}
}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java
index f8a9350..befde65 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java
@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
+import javax.persistence.FlushModeType;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
@@ -32,7 +33,6 @@ import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.keycloak.authorization.AuthorizationProvider;
-import org.keycloak.authorization.jpa.entities.ResourceServerEntity;
import org.keycloak.authorization.jpa.entities.ScopeEntity;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
@@ -61,6 +61,7 @@ public class JPAScopeStore implements ScopeStore {
entity.setResourceServer(ResourceServerAdapter.toEntity(entityManager, resourceServer));
this.entityManager.persist(entity);
+ this.entityManager.flush();
return new ScopeAdapter(entity, entityManager, provider.getStoreFactory());
}
@@ -91,8 +92,10 @@ public class JPAScopeStore implements ScopeStore {
try {
TypedQuery<String> query = entityManager.createNamedQuery("findScopeIdByName", String.class);
+ query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("serverId", resourceServerId);
query.setParameter("name", name);
+
String id = query.getSingleResult();
return provider.getStoreFactory().getScopeStore().findById(id, resourceServerId);
} catch (NoResultException nre) {
@@ -104,6 +107,7 @@ public class JPAScopeStore implements ScopeStore {
public List<Scope> findByResourceServer(final String serverId) {
TypedQuery<String> query = entityManager.createNamedQuery("findScopeIdByResourceServer", String.class);
+ query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("serverId", serverId);
List<String> result = query.getResultList();
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index 13988dc..33578e3 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -220,6 +220,8 @@ public class RealmEntity {
@Column(name="CLIENT_AUTH_FLOW")
protected String clientAuthenticationFlow;
+ @Column(name="DOCKER_AUTH_FLOW")
+ protected String dockerAuthenticationFlow;
@Column(name="INTERNATIONALIZATION_ENABLED")
@@ -733,6 +735,15 @@ public class RealmEntity {
this.clientAuthenticationFlow = clientAuthenticationFlow;
}
+ public String getDockerAuthenticationFlow() {
+ return dockerAuthenticationFlow;
+ }
+
+ public RealmEntity setDockerAuthenticationFlow(String dockerAuthenticationFlow) {
+ this.dockerAuthenticationFlow = dockerAuthenticationFlow;
+ return this;
+ }
+
public Collection<ClientTemplateEntity> getClientTemplates() {
return clientTemplates;
}
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 b71a493..05d7517 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
@@ -1336,6 +1336,18 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
}
@Override
+ public AuthenticationFlowModel getDockerAuthenticationFlow() {
+ String flowId = realm.getDockerAuthenticationFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setDockerAuthenticationFlow(AuthenticationFlowModel flow) {
+ realm.setDockerAuthenticationFlow(flow.getId());
+ }
+
+ @Override
public List<AuthenticationFlowModel> getAuthenticationFlows() {
return realm.getAuthenticationFlows().stream()
.map(this::entityToModel)
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml
index bd55645..daa1c50 100644
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml
@@ -15,10 +15,14 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
+ <changeSet author="keycloak" id="3.2.0">
+ <addColumn tableName="REALM">
+ <column name="DOCKER_AUTH_FLOW" type="VARCHAR(36)">
+ <constraints nullable="true"/>
+ </column>
+ </addColumn>
- <changeSet author="mposolda@redhat.com" id="3.2.0">
<dropPrimaryKey constraintName="CONSTRAINT_OFFL_CL_SES_PK2" tableName="OFFLINE_CLIENT_SESSION" />
<dropColumn tableName="OFFLINE_CLIENT_SESSION" columnName="CLIENT_SESSION_ID" />
<addPrimaryKey columnNames="USER_SESSION_ID,CLIENT_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_CL_SES_PK3" tableName="OFFLINE_CLIENT_SESSION"/>
@@ -38,9 +42,6 @@
<addPrimaryKey columnNames="ID" constraintName="CNSTR_CLIENT_INIT_ACC_PK" tableName="CLIENT_INITIAL_ACCESS"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="CLIENT_INITIAL_ACCESS" constraintName="FK_CLIENT_INIT_ACC_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
- </changeSet>
-
- <changeSet author="glavoie@gmail.com" id="3.2.0.idx">
<createIndex indexName="IDX_ASSOC_POL_ASSOC_POL_ID" tableName="ASSOCIATED_POLICY">
<column name="ASSOCIATED_POLICY_ID" type="VARCHAR(36)"/>
</createIndex>
model/pom.xml 2(+1 -1)
diff --git a/model/pom.xml b/model/pom.xml
index 944ec51..43f3834 100755
--- a/model/pom.xml
+++ b/model/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Keycloak Model Parent</name>
pom.xml 11(+9 -2)
diff --git a/pom.xml b/pom.xml
index d5b108d..8b216dc 100755
--- a/pom.xml
+++ b/pom.xml
@@ -33,7 +33,7 @@
</description>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
@@ -45,7 +45,7 @@
<jboss.as.version>7.2.0.Final</jboss.as.version>
<wildfly.version>11.0.0.Alpha1</wildfly.version>
<wildfly.build-tools.version>1.2.2.Final</wildfly.build-tools.version>
- <eap.version>7.1.0.Beta1-redhat-2</eap.version>
+ <eap.version>7.1.0.Beta1-redhat-5</eap.version>
<eap.build-tools.version>1.2.2.Final</eap.build-tools.version>
<wildfly.core.version>3.0.0.Beta11</wildfly.core.version>
@@ -80,6 +80,7 @@
<sun.jaxb.version>2.2.11</sun.jaxb.version>
<sun.xsom.version>20140925</sun.xsom.version>
<undertow.version>1.4.11.Final</undertow.version>
+ <woodstox.version>5.0.3</woodstox.version>
<xmlsec.version>2.0.5</xmlsec.version>
<!-- Authorization Drools Policy Provider -->
@@ -123,6 +124,7 @@
<osgi.bundle.plugin.version>2.3.7</osgi.bundle.plugin.version>
<wildfly.plugin.version>1.1.0.Final</wildfly.plugin.version>
<nexus.staging.plugin.version>1.6.5</nexus.staging.plugin.version>
+ <frontend.plugin.version>1.5</frontend.plugin.version>
<!-- Surefire Settings -->
<surefire.memory.settings>-Xms512m -Xmx2048m -XX:MetaspaceSize=96m -XX:MaxMetaspaceSize=256m</surefire.memory.settings>
@@ -1500,6 +1502,11 @@
<artifactId>maven-bundle-plugin</artifactId>
<version>${osgi.bundle.plugin.version}</version>
</plugin>
+ <plugin>
+ <groupId>com.github.eirslett</groupId>
+ <artifactId>frontend-maven-plugin</artifactId>
+ <version>${frontend.plugin.version}</version>
+ </plugin>
</plugins>
</pluginManagement>
</build>
proxy/launcher/pom.xml 2(+1 -1)
diff --git a/proxy/launcher/pom.xml b/proxy/launcher/pom.xml
index dbb2a6a..e3a71d1 100755
--- a/proxy/launcher/pom.xml
+++ b/proxy/launcher/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
proxy/pom.xml 2(+1 -1)
diff --git a/proxy/pom.xml b/proxy/pom.xml
index 081e320..8858109 100755
--- a/proxy/pom.xml
+++ b/proxy/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Keycloak Proxy</name>
proxy/proxy-server/pom.xml 2(+1 -1)
diff --git a/proxy/proxy-server/pom.xml b/proxy/proxy-server/pom.xml
index 450a408..be58cd6 100755
--- a/proxy/proxy-server/pom.xml
+++ b/proxy/proxy-server/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
saml-core/pom.xml 2(+1 -1)
diff --git a/saml-core/pom.xml b/saml-core/pom.xml
index 100b6ef..8ed8de1 100755
--- a/saml-core/pom.xml
+++ b/saml-core/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java b/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
index 86b3ecb..be74b74 100755
--- a/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
+++ b/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
@@ -345,7 +345,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
logger.debugv("saml document: {0}", documentAsString);
byte[] responseBytes = documentAsString.getBytes(GeneralConstants.SAML_CHARSET);
- return RedirectBindingUtil.deflateBase64URLEncode(responseBytes);
+ return RedirectBindingUtil.deflateBase64Encode(responseBytes);
}
@@ -370,7 +370,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
} catch (InvalidKeyException | SignatureException e) {
throw new ProcessingException(e);
}
- String encodedSig = RedirectBindingUtil.base64URLEncode(sig);
+ String encodedSig = RedirectBindingUtil.base64Encode(sig);
builder.queryParam(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY, encodedSig);
}
return builder.build();
diff --git a/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java b/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java
index f516124..7177322 100755
--- a/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java
@@ -511,7 +511,7 @@ public class DocumentUtil {
};
- private static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException {
+ public static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException {
DocumentBuilder res = XML_DOCUMENT_BUILDER.get();
res.reset();
return res;
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
index 2bfa41f..cb8a348 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
@@ -156,7 +156,7 @@ public class SAML2Request {
* @throws IOException
* @throws ParsingException
*/
- public SAML2Object getSAML2ObjectFromStream(InputStream is) throws ConfigurationException, ParsingException,
+ public static SAMLDocumentHolder getSAML2ObjectFromStream(InputStream is) throws ConfigurationException, ParsingException,
ProcessingException {
if (is == null)
throw logger.nullArgumentError("InputStream");
@@ -167,8 +167,7 @@ public class SAML2Request {
JAXPValidationUtil.checkSchemaValidation(samlDocument);
SAML2Object requestType = (SAML2Object) samlParser.parse(samlDocument);
- samlDocumentHolder = new SAMLDocumentHolder(requestType, samlDocument);
- return requestType;
+ return new SAMLDocumentHolder(requestType, samlDocument);
}
/**
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLAssertionWriter.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLAssertionWriter.java
index 867aceb..41461bf 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLAssertionWriter.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLAssertionWriter.java
@@ -47,6 +47,7 @@ import java.net.URI;
import java.util.List;
import java.util.Set;
+import javax.xml.crypto.dsig.XMLSignature;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI;
/**
@@ -69,8 +70,17 @@ public class SAMLAssertionWriter extends BaseWriter {
* @throws org.keycloak.saml.common.exceptions.ProcessingException
*/
public void write(AssertionType assertion) throws ProcessingException {
+ write(assertion, false);
+ }
+
+ public void write(AssertionType assertion, boolean forceWriteDsigNamespace) throws ProcessingException {
+ Element sig = assertion.getSignature();
+
StaxUtil.writeStartElement(writer, ASSERTION_PREFIX, JBossSAMLConstants.ASSERTION.get(), ASSERTION_NSURI.get());
StaxUtil.writeNameSpace(writer, ASSERTION_PREFIX, ASSERTION_NSURI.get());
+ if (forceWriteDsigNamespace && sig != null && sig.getPrefix() != null && ! sig.hasAttribute("xmlns:" + sig.getPrefix())) {
+ StaxUtil.writeNameSpace(writer, sig.getPrefix(), XMLSignature.XMLNS);
+ }
StaxUtil.writeDefaultNameSpace(writer, ASSERTION_NSURI.get());
// Attributes
@@ -82,7 +92,6 @@ public class SAMLAssertionWriter extends BaseWriter {
if (issuer != null)
write(issuer, new QName(ASSERTION_NSURI.get(), JBossSAMLConstants.ISSUER.get(), ASSERTION_PREFIX));
- Element sig = assertion.getSignature();
if (sig != null)
StaxUtil.writeDOMElement(writer, sig);
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java
index 9327a73..d2a59b9 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java
@@ -38,6 +38,7 @@ import javax.xml.stream.XMLStreamWriter;
import java.net.URI;
import java.util.List;
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
+import javax.xml.crypto.dsig.XMLSignature;
/**
* Write a SAML Response to stream
@@ -63,8 +64,17 @@ public class SAMLResponseWriter extends BaseWriter {
* @throws org.keycloak.saml.common.exceptions.ProcessingException
*/
public void write(ResponseType response) throws ProcessingException {
+ write(response, false);
+ }
+
+ public void write(ResponseType response, boolean forceWriteDsigNamespace) throws ProcessingException {
+ Element sig = response.getSignature();
+
StaxUtil.writeStartElement(writer, PROTOCOL_PREFIX, JBossSAMLConstants.RESPONSE.get(), JBossSAMLURIConstants.PROTOCOL_NSURI.get());
+ if (forceWriteDsigNamespace && sig != null && sig.getPrefix() != null && ! sig.hasAttribute("xmlns:" + sig.getPrefix())) {
+ StaxUtil.writeNameSpace(writer, sig.getPrefix(), XMLSignature.XMLNS);
+ }
StaxUtil.writeNameSpace(writer, PROTOCOL_PREFIX, JBossSAMLURIConstants.PROTOCOL_NSURI.get());
StaxUtil.writeNameSpace(writer, ASSERTION_PREFIX, JBossSAMLURIConstants.ASSERTION_NSURI.get());
@@ -75,7 +85,6 @@ public class SAMLResponseWriter extends BaseWriter {
write(issuer, new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ISSUER.get(), ASSERTION_PREFIX));
}
- Element sig = response.getSignature();
if (sig != null) {
StaxUtil.writeDOMElement(writer, sig);
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java
index 587113c..9c0938f 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java
@@ -61,6 +61,19 @@ public class RedirectBindingUtil {
}
/**
+ * On the byte array, apply base64 encoding
+ *
+ * @param stringToEncode
+ *
+ * @return
+ *
+ * @throws IOException
+ */
+ public static String base64Encode(byte[] stringToEncode) throws IOException {
+ return Base64.encodeBytes(stringToEncode, Base64.DONT_BREAK_LINES);
+ }
+
+ /**
* On the byte array, apply base64 encoding following by URL encoding
*
* @param stringToEncode
diff --git a/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java b/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java
index 00160e6..335a72d 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java
@@ -56,10 +56,8 @@ public class SAMLRequestParser {
is = new ByteArrayInputStream(message.getBytes(GeneralConstants.SAML_CHARSET));
}
- SAML2Request saml2Request = new SAML2Request();
try {
- saml2Request.getSAML2ObjectFromStream(is);
- return saml2Request.getSamlDocumentHolder();
+ return SAML2Request.getSAML2ObjectFromStream(is);
} catch (Exception e) {
logger.samlBase64DecodingError(e);
}
@@ -76,10 +74,8 @@ public class SAMLRequestParser {
log.debug(str);
}
is = new ByteArrayInputStream(samlBytes);
- SAML2Request saml2Request = new SAML2Request();
try {
- saml2Request.getSAML2ObjectFromStream(is);
- return saml2Request.getSamlDocumentHolder();
+ return SAML2Request.getSAML2ObjectFromStream(is);
} catch (Exception e) {
logger.samlBase64DecodingError(e);
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java b/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java
index 5feda2b..b020eb7 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java
@@ -23,14 +23,19 @@ package org.keycloak.saml;
*/
public class SPMetadataDescriptor {
- public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint, boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, String entityId, String nameIDPolicyFormat, String signingCerts) {
+ public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint,
+ boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, boolean wantAssertionsEncrypted,
+ String entityId, String nameIDPolicyFormat, String signingCerts, String encryptionCerts) {
String descriptor =
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"" + entityId + "\">\n" +
" <SPSSODescriptor AuthnRequestsSigned=\"" + wantAuthnRequestsSigned + "\" WantAssertionsSigned=\"" + wantAssertionsSigned + "\"\n" +
" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol http://schemas.xmlsoap.org/ws/2003/07/secext\">\n";
- if (wantAuthnRequestsSigned && signingCerts != null) {
+ if (wantAuthnRequestsSigned && signingCerts != null) {
descriptor += signingCerts;
}
+ if (wantAssertionsEncrypted && encryptionCerts != null) {
+ descriptor += encryptionCerts;
+ }
descriptor +=
" <SingleLogoutService Binding=\"" + binding + "\" Location=\"" + logoutEndpoint + "\"/>\n" +
" <NameIDFormat>" + nameIDPolicyFormat + "\n" +
saml-core-api/pom.xml 2(+1 -1)
diff --git a/saml-core-api/pom.xml b/saml-core-api/pom.xml
index d2c1f99..bd33054 100755
--- a/saml-core-api/pom.xml
+++ b/saml-core-api/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
server-spi/pom.xml 2(+1 -1)
diff --git a/server-spi/pom.xml b/server-spi/pom.xml
index 68672fb..e251128 100755
--- a/server-spi/pom.xml
+++ b/server-spi/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/server-spi/src/main/java/org/keycloak/models/AbstractKeycloakTransaction.java b/server-spi/src/main/java/org/keycloak/models/AbstractKeycloakTransaction.java
new file mode 100644
index 0000000..a3ea54b
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/AbstractKeycloakTransaction.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models;
+
+import org.jboss.logging.Logger;
+
+/**
+ * Handles some common transaction logic related to start, rollback-only etc.
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class AbstractKeycloakTransaction implements KeycloakTransaction {
+
+ public static final Logger logger = Logger.getLogger(AbstractKeycloakTransaction.class);
+
+ protected TransactionState state = TransactionState.NOT_STARTED;
+
+ @Override
+ public void begin() {
+ if (state != TransactionState.NOT_STARTED) {
+ throw new IllegalStateException("Transaction already started");
+ }
+
+ state = TransactionState.STARTED;
+ }
+
+ @Override
+ public void commit() {
+ if (state != TransactionState.STARTED) {
+ throw new IllegalStateException("Transaction in illegal state for commit: " + state);
+ }
+
+ commitImpl();
+
+ state = TransactionState.FINISHED;
+ }
+
+ @Override
+ public void rollback() {
+ if (state != TransactionState.STARTED && state != TransactionState.ROLLBACK_ONLY) {
+ throw new IllegalStateException("Transaction in illegal state for rollback: " + state);
+ }
+
+ rollbackImpl();
+
+ state = TransactionState.FINISHED;
+ }
+
+ @Override
+ public void setRollbackOnly() {
+ state = TransactionState.ROLLBACK_ONLY;
+ }
+
+ @Override
+ public boolean getRollbackOnly() {
+ return state == TransactionState.ROLLBACK_ONLY;
+ }
+
+ @Override
+ public boolean isActive() {
+ return state == TransactionState.STARTED || state == TransactionState.ROLLBACK_ONLY;
+ }
+
+ public TransactionState getState() {
+ return state;
+ }
+
+ public enum TransactionState {
+ NOT_STARTED, STARTED, ROLLBACK_ONLY, FINISHED
+ }
+
+
+ protected abstract void commitImpl();
+
+ protected abstract void rollbackImpl();
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java b/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java
index cf9d7d0..2725945 100644
--- a/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java
@@ -43,4 +43,8 @@ public interface ActionTokenKeyModel {
* @return Single-use random value used for verification whether the relevant action is allowed.
*/
UUID getActionVerificationNonce();
+
+ default String serializeKey() {
+ return String.format("%s.%d.%s.%s", getUserId(), getExpiration(), getActionVerificationNonce(), getActionId());
+ }
}
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index 133c247..f8d32e1 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -247,6 +247,9 @@ public interface RealmModel extends RoleContainerModel {
AuthenticationFlowModel getClientAuthenticationFlow();
void setClientAuthenticationFlow(AuthenticationFlowModel flow);
+ AuthenticationFlowModel getDockerAuthenticationFlow();
+ void setDockerAuthenticationFlow(AuthenticationFlowModel flow);
+
List<AuthenticationFlowModel> getAuthenticationFlows();
AuthenticationFlowModel getFlowByAlias(String alias);
AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model);
diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
index 28a3145..a6f1c35 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
@@ -62,8 +62,6 @@ public interface UserSessionModel {
State getState();
void setState(State state);
- void setUser(UserModel user);
-
// Will completely restart whole state of user session. It will just keep same ID.
void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId);
diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
index 848c098..8334838 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
@@ -20,6 +20,7 @@ package org.keycloak.models;
import org.keycloak.provider.Provider;
import java.util.List;
+import java.util.function.Predicate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -37,6 +38,12 @@ public interface UserSessionProvider extends Provider {
List<UserSessionModel> getUserSessionByBrokerUserId(RealmModel realm, String brokerUserId);
UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId);
+ /**
+ * Return userSession of specified ID as long as the predicate passes. Otherwise returs null.
+ * If predicate doesn't pass, implementation can do some best-effort actions to try have predicate passing (eg. download userSession from other DC)
+ */
+ UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline, Predicate<UserSessionModel> predicate);
+
long getActiveUserSessions(RealmModel realm, ClientModel client);
/** This will remove attached ClientLoginSessionModels too **/
diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java b/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java
index 3cf8d2c..5c83253 100755
--- a/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java
+++ b/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java
@@ -53,4 +53,8 @@ public interface ProviderFactory<T extends Provider> {
public String getId();
+ default int order() {
+ return 0;
+ }
+
}
diff --git a/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java
index c47a6a5..1598714 100644
--- a/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java
@@ -53,7 +53,7 @@ public interface CommonClientSessionModel {
// TODO: Not needed here...?
public Set<String> getProtocolMappers();
public void setProtocolMappers(Set<String> protocolMappers);
-
+
public static enum Action {
OAUTH_GRANT,
CODE_TO_TOKEN,
server-spi-private/pom.xml 2(+1 -1)
diff --git a/server-spi-private/pom.xml b/server-spi-private/pom.xml
index 0e9a511..3d3f430 100755
--- a/server-spi-private/pom.xml
+++ b/server-spi-private/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java
index abed174..c1d59c5 100644
--- a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java
@@ -21,6 +21,7 @@ package org.keycloak.cluster;
import org.keycloak.provider.Provider;
import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
/**
* Various utils related to clustering and concurrent tasks on cluster nodes
@@ -48,8 +49,20 @@ public interface ClusterProvider extends Provider {
/**
+ * Execute given task just if it's not already in progress (either on this or any other cluster node). It will return corresponding future to every caller and this future is fulfilled if:
+ * - The task is successfully finished. In that case Future will be true
+ * - The task wasn't successfully finished. For example because cluster node failover. In that case Future will be false
+ *
+ * @param taskKey
+ * @param taskTimeoutInSeconds timeout for given task. If there is existing task in progress for longer time, it's considered outdated so we will start our task.
+ * @param task
+ * @return Future, which will be completed once the running task is finished. Returns true if task was successfully finished. Otherwise (for example if cluster node when task was running leaved cluster) returns false
+ */
+ Future<Boolean> executeIfNotExecutedAsync(String taskKey, int taskTimeoutInSeconds, Callable task);
+
+
+ /**
* Register task (listener) under given key. When this key will be put to the cache on any cluster node, the task will be executed.
- * When using {@link #ALL} as the taskKey, then listener will be always triggered for any value put into the cache.
*
* @param taskKey
* @param task
@@ -58,18 +71,24 @@ public interface ClusterProvider extends Provider {
/**
- * Notify registered listeners on all cluster nodes. It will notify listeners registered under given taskKey AND also listeners registered with {@link #ALL} key (those are always executed)
+ * Notify registered listeners on all cluster nodes in all datacenters. It will notify listeners registered under given taskKey
*
* @param taskKey
* @param event
* @param ignoreSender if true, then sender node itself won't receive the notification
+ * @param dcNotify Specify which DCs to notify. See {@link DCNotify} enum values for more info
*/
- void notify(String taskKey, ClusterEvent event, boolean ignoreSender);
+ void notify(String taskKey, ClusterEvent event, boolean ignoreSender, DCNotify dcNotify);
+ enum DCNotify {
+ /** Send message to all cluster nodes in all DCs **/
+ ALL_DCS,
+
+ /** Send message to all cluster nodes on THIS datacenter only **/
+ LOCAL_DC_ONLY,
+
+ /** Send message to all cluster nodes in all datacenters, but NOT to this datacenter. Option "ignoreSender" of method {@link #notify} will be ignored as sender is ignored anyway due it is in this datacenter **/
+ ALL_BUT_LOCAL_DC
+ }
- /**
- * Special value to be used with {@link #registerListener} to specify that particular listener will be always triggered for all notifications
- * with any key.
- */
- String ALL = "ALL";
}
diff --git a/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java b/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java
index 7ea2b49..a12b028 100755
--- a/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java
@@ -17,15 +17,15 @@
package org.keycloak.email;
-import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
+import java.util.Map;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface EmailSenderProvider extends Provider {
- void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException;
-
+ void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException;
}
diff --git a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
index 1cc6151..da245fc 100755
--- a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
@@ -22,6 +22,8 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
+import java.util.Map;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -47,6 +49,15 @@ public interface EmailTemplateProvider extends Provider {
public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException;
/**
+ * Test SMTP connection with current logged in user
+ *
+ * @param config SMTP server configuration
+ * @param user SMTP recipient
+ * @throws EmailException
+ */
+ public void sendSmtpTestEmail(Map<String, String> config, UserModel user) throws EmailException;
+
+ /**
* Send to confirm that user wants to link his account with identity broker link
*/
void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException;
diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventType.java b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
index 920646f..b48e243 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/EventType.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
@@ -123,7 +123,9 @@ public enum EventType {
CLIENT_DELETE_ERROR(true),
CLIENT_INITIATED_ACCOUNT_LINKING(true),
- CLIENT_INITIATED_ACCOUNT_LINKING_ERROR(true);
+ CLIENT_INITIATED_ACCOUNT_LINKING_ERROR(true),
+ TOKEN_EXCHANGE(true),
+ TOKEN_EXCHANGE_ERROR(true);
private boolean saveByDefault;
diff --git a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_2_0.java b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_2_0.java
index 17cd0ac..98686af 100644
--- a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_2_0.java
+++ b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_2_0.java
@@ -27,11 +27,8 @@ import org.keycloak.migration.ModelVersion;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.DefaultAuthenticationFlows;
-/**
- * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
- * @version $Revision: 1 $
- */
public class MigrateTo3_2_0 implements Migration {
public static final ModelVersion VERSION = new ModelVersion("3.2.0");
@@ -44,6 +41,10 @@ public class MigrateTo3_2_0 implements Migration {
realm.setPasswordPolicy(builder.remove(PasswordPolicy.HASH_ITERATIONS_ID).build(session));
}
+ if (realm.getDockerAuthenticationFlow() == null) {
+ DefaultAuthenticationFlows.dockerAuthenticationFlow(realm);
+ }
+
ClientModel realmAccess = realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID);
if (realmAccess != null) {
addRoles(realmAccess);
diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
index 40f9081..8ff0966 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
@@ -57,6 +57,8 @@ public interface Constants {
String KEY = "key";
String SKIP_LINK = "skipLink";
+ String TEMPLATE_ATTR_ACTION_URI = "actionUri";
+ String TEMPLATE_ATTR_REQUIRED_ACTIONS = "requiredActions";
// Prefix for user attributes used in various "context"data maps
String USER_ATTRIBUTES_PREFIX = "user.attributes.";
diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
index 23436e0..6bea75f 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
@@ -116,11 +116,6 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
}
@Override
- public void setUser(UserModel user) {
- throw new IllegalStateException("Not supported");
- }
-
- @Override
public RealmModel getRealm() {
return realm;
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
index b028814..8030da6 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
@@ -42,6 +42,7 @@ public class DefaultAuthenticationFlows {
public static final String RESET_CREDENTIALS_FLOW = "reset credentials";
public static final String LOGIN_FORMS_FLOW = "forms";
public static final String SAML_ECP_FLOW = "saml ecp";
+ public static final String DOCKER_AUTH = "docker auth";
public static final String CLIENT_AUTHENTICATION_FLOW = "clients";
public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login";
@@ -58,6 +59,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, false);
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
+ if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
}
public static void migrateFlows(RealmModel realm) {
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
@@ -67,6 +69,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, true);
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
+ if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
}
public static void registrationFlow(RealmModel realm) {
@@ -528,4 +531,26 @@ public class DefaultAuthenticationFlows {
realm.addAuthenticatorExecution(execution);
}
+
+ public static void dockerAuthenticationFlow(final RealmModel realm) {
+ AuthenticationFlowModel dockerAuthFlow = new AuthenticationFlowModel();
+
+ dockerAuthFlow.setAlias(DOCKER_AUTH);
+ dockerAuthFlow.setDescription("Used by Docker clients to authenticate against the IDP");
+ dockerAuthFlow.setProviderId("basic-flow");
+ dockerAuthFlow.setTopLevel(true);
+ dockerAuthFlow.setBuiltIn(true);
+ dockerAuthFlow = realm.addAuthenticationFlow(dockerAuthFlow);
+ realm.setDockerAuthenticationFlow(dockerAuthFlow);
+
+ AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
+
+ execution.setParentFlow(dockerAuthFlow.getId());
+ execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+ execution.setAuthenticator("docker-http-basic-authenticator");
+ execution.setPriority(10);
+ execution.setAuthenticatorFlow(false);
+
+ realm.addAuthenticatorExecution(execution);
+ }
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index b454460..dc69fc8 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -489,6 +489,7 @@ public final class KeycloakModelUtils {
if ((realmFlow = realm.getClientAuthenticationFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
if ((realmFlow = realm.getDirectGrantFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
if ((realmFlow = realm.getResetCredentialsFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
+ if ((realmFlow = realm.getDockerAuthenticationFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
for (IdentityProviderModel idp : realm.getIdentityProviders()) {
if (model.getId().equals(idp.getFirstBrokerLoginFlowId())) return true;
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index a2a3e09..d11c272 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -24,6 +24,7 @@ import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.common.Profile;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
@@ -291,6 +292,7 @@ public class ModelToRepresentation {
if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());
if (realm.getResetCredentialsFlow() != null) rep.setResetCredentialsFlow(realm.getResetCredentialsFlow().getAlias());
if (realm.getClientAuthenticationFlow() != null) rep.setClientAuthenticationFlow(realm.getClientAuthenticationFlow().getAlias());
+ if (realm.getDockerAuthenticationFlow() != null) rep.setDockerAuthenticationFlow(realm.getDockerAuthenticationFlow().getAlias());
List<String> defaultRoles = realm.getDefaultRoles();
if (!defaultRoles.isEmpty()) {
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java b/server-spi-private/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java
index 73889e0..430d895 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java
@@ -17,6 +17,7 @@
package org.keycloak.models.utils;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderEvent;
/**
@@ -25,4 +26,14 @@ import org.keycloak.provider.ProviderEvent;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class PostMigrationEvent implements ProviderEvent {
+
+ private final KeycloakSession session;
+
+ public PostMigrationEvent(KeycloakSession session) {
+ this.session = session;
+ }
+
+ public KeycloakSession getSession() {
+ return session;
+ }
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 4a4b4fb..fe27fae 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -76,6 +76,7 @@ import org.keycloak.models.ScopeContainerModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.UserProvider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.ApplicationRepresentation;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
@@ -614,6 +615,18 @@ public class RepresentationToModel {
}
}
+ // Added in 3.2
+ if (rep.getDockerAuthenticationFlow() == null) {
+ AuthenticationFlowModel dockerAuthenticationFlow = newRealm.getFlowByAlias(DefaultAuthenticationFlows.DOCKER_AUTH);
+ if (dockerAuthenticationFlow == null) {
+ DefaultAuthenticationFlows.dockerAuthenticationFlow(newRealm);
+ } else {
+ newRealm.setDockerAuthenticationFlow(dockerAuthenticationFlow);
+ }
+ } else {
+ newRealm.setDockerAuthenticationFlow(newRealm.getFlowByAlias(rep.getDockerAuthenticationFlow()));
+ }
+
DefaultAuthenticationFlows.addIdentityProviderAuthenticator(newRealm, defaultProvider);
}
@@ -898,6 +911,9 @@ public class RepresentationToModel {
if (rep.getClientAuthenticationFlow() != null) {
realm.setClientAuthenticationFlow(realm.getFlowByAlias(rep.getClientAuthenticationFlow()));
}
+ if (rep.getDockerAuthenticationFlow() != null) {
+ realm.setDockerAuthenticationFlow(realm.getFlowByAlias(rep.getDockerAuthenticationFlow()));
+ }
}
// Basic realm stuff
@@ -1201,6 +1217,7 @@ public class RepresentationToModel {
if (rep.isUseTemplateScope() != null) resource.setUseTemplateScope(rep.isUseTemplateScope());
if (rep.isUseTemplateMappers() != null) resource.setUseTemplateMappers(rep.isUseTemplateMappers());
+ if (rep.getSecret() != null) resource.setSecret(rep.getSecret());
if (rep.getClientTemplate() != null) {
if (rep.getClientTemplate().equals(ClientTemplateRepresentation.NONE)) {
@@ -1913,24 +1930,21 @@ public class RepresentationToModel {
resourceServer.setPolicyEnforcementMode(rep.getPolicyEnforcementMode());
resourceServer.setAllowRemoteResourceManagement(rep.isAllowRemoteResourceManagement());
- rep.getScopes().forEach(scope -> {
+ for (ScopeRepresentation scope : rep.getScopes()) {
toModel(scope, resourceServer, authorization);
- });
+ }
KeycloakSession session = authorization.getKeycloakSession();
RealmModel realm = authorization.getRealm();
- rep.getResources().forEach(resourceRepresentation -> {
- ResourceOwnerRepresentation owner = resourceRepresentation.getOwner();
+ for (ResourceRepresentation resource : rep.getResources()) {
+ ResourceOwnerRepresentation owner = resource.getOwner();
if (owner == null) {
owner = new ResourceOwnerRepresentation();
- resourceRepresentation.setOwner(owner);
- }
-
- owner.setId(resourceServer.getClientId());
-
- if (owner.getName() != null) {
+ owner.setId(resourceServer.getClientId());
+ resource.setOwner(owner);
+ } else if (owner.getName() != null) {
UserModel user = session.users().getUserByUsername(owner.getName(), realm);
if (user != null) {
@@ -1938,8 +1952,8 @@ public class RepresentationToModel {
}
}
- toModel(resourceRepresentation, resourceServer, authorization);
- });
+ toModel(resource, resourceServer, authorization);
+ }
importPolicies(authorization, resourceServer, rep.getPolicies(), null);
}
@@ -1958,7 +1972,9 @@ public class RepresentationToModel {
PolicyStore policyStore = storeFactory.getPolicyStore();
try {
List<String> policies = (List<String>) JsonSerialization.readValue(applyPolicies, List.class);
- config.put("applyPolicies", JsonSerialization.writeValueAsString(policies.stream().map(policyName -> {
+ Set<String> policyIds = new HashSet<>();
+
+ for (String policyName : policies) {
Policy policy = policyStore.findByName(policyName, resourceServer.getId());
if (policy == null) {
@@ -1972,8 +1988,10 @@ public class RepresentationToModel {
}
}
- return policy.getId();
- }).collect(Collectors.toList())));
+ policyIds.add(policy.getId());
+ }
+
+ config.put("applyPolicies", JsonSerialization.writeValueAsString(policyIds));
} catch (Exception e) {
throw new RuntimeException("Error while importing policy [" + policyRepresentation.getName() + "].", e);
}
@@ -2012,33 +2030,40 @@ public class RepresentationToModel {
if (representation instanceof PolicyRepresentation) {
PolicyRepresentation policy = PolicyRepresentation.class.cast(representation);
- String resourcesConfig = policy.getConfig().get("resources");
- if (resourcesConfig != null) {
- try {
- resources = JsonSerialization.readValue(resourcesConfig, Set.class);
- } catch (IOException e) {
- throw new RuntimeException(e);
+ if (resources == null) {
+ String resourcesConfig = policy.getConfig().get("resources");
+
+ if (resourcesConfig != null) {
+ try {
+ resources = JsonSerialization.readValue(resourcesConfig, Set.class);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
}
- String scopesConfig = policy.getConfig().get("scopes");
+ if (scopes == null) {
+ String scopesConfig = policy.getConfig().get("scopes");
- if (scopesConfig != null) {
- try {
- scopes = JsonSerialization.readValue(scopesConfig, Set.class);
- } catch (IOException e) {
- throw new RuntimeException(e);
+ if (scopesConfig != null) {
+ try {
+ scopes = JsonSerialization.readValue(scopesConfig, Set.class);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
}
- String policiesConfig = policy.getConfig().get("applyPolicies");
+ if (policies == null) {
+ String policiesConfig = policy.getConfig().get("applyPolicies");
- if (policiesConfig != null) {
- try {
- policies = JsonSerialization.readValue(policiesConfig, Set.class);
- } catch (IOException e) {
- throw new RuntimeException(e);
+ if (policiesConfig != null) {
+ try {
+ policies = JsonSerialization.readValue(policiesConfig, Set.class);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
}
@@ -2229,10 +2254,10 @@ public class RepresentationToModel {
existing.setType(resource.getType());
existing.setUri(resource.getUri());
existing.setIconUri(resource.getIconUri());
-
existing.updateScopes(resource.getScopes().stream()
.map((ScopeRepresentation scope) -> toModel(scope, resourceServer, authorization))
.collect(Collectors.toSet()));
+
return existing;
}
@@ -2243,11 +2268,30 @@ public class RepresentationToModel {
owner.setId(resourceServer.getClientId());
}
- if (owner.getId() == null) {
+ String ownerId = owner.getId();
+
+ if (ownerId == null) {
throw new RuntimeException("No owner specified for resource [" + resource.getName() + "].");
}
- Resource model = resourceStore.create(resource.getName(), resourceServer, owner.getId());
+ if (!resourceServer.getClientId().equals(ownerId)) {
+ RealmModel realm = authorization.getRealm();
+ KeycloakSession keycloakSession = authorization.getKeycloakSession();
+ UserProvider users = keycloakSession.users();
+ UserModel ownerModel = users.getUserById(ownerId, realm);
+
+ if (ownerModel == null) {
+ ownerModel = users.getUserByUsername(ownerId, realm);
+ }
+
+ if (ownerModel == null) {
+ throw new RuntimeException("Owner must be a valid username or user identifier. If the resource server, the client id or null.");
+ }
+
+ owner.setId(ownerModel.getId());
+ }
+
+ Resource model = resourceStore.create(resource.getName(), resourceServer, ownerId);
model.setType(resource.getType());
model.setUri(resource.getUri());
services/pom.xml 8(+7 -1)
diff --git a/services/pom.xml b/services/pom.xml
index c1295db..733b812 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -145,6 +145,12 @@
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
+ <groupId>com.fasterxml.woodstox</groupId>
+ <artifactId>woodstox-core</artifactId>
+ <version>${woodstox.version}</version> <!-- this version has to match that of used in Wildfly -->
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
</dependency>
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java b/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java
index 52d94d9..ccdc2f8 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java
@@ -20,6 +20,7 @@ import org.keycloak.Config.Scope;
import org.keycloak.events.EventType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.sessions.AuthenticationSessionModel;
@@ -27,7 +28,7 @@ import org.keycloak.sessions.AuthenticationSessionModel;
*
* @author hmlnarik
*/
-public abstract class AbstractActionTokenHander<T extends DefaultActionToken> implements ActionTokenHandler<T>, ActionTokenHandlerFactory<T> {
+public abstract class AbstractActionTokenHander<T extends JsonWebToken> implements ActionTokenHandler<T>, ActionTokenHandlerFactory<T> {
private final String id;
private final Class<T> tokenClass;
@@ -86,8 +87,8 @@ public abstract class AbstractActionTokenHander<T extends DefaultActionToken> im
}
@Override
- public String getAuthenticationSessionIdFromToken(T token) {
- return token == null ? null : token.getAuthenticationSessionId();
+ public String getAuthenticationSessionIdFromToken(T token, ActionTokenContext<T> tokenContext) {
+ return token instanceof DefaultActionToken ? ((DefaultActionToken) token).getAuthenticationSessionId() : null;
}
@Override
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
index f8d02d3..4f98070 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
@@ -17,6 +17,7 @@
package org.keycloak.authentication.actiontoken;
import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.common.VerificationException;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.provider.Provider;
@@ -64,7 +65,7 @@ public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
* @param token Token. Can be {@code null}
* @return authentication session ID
*/
- String getAuthenticationSessionIdFromToken(T token);
+ String getAuthenticationSessionIdFromToken(T token, ActionTokenContext<T> tokenContext);
/**
* Returns a event type logged with {@link EventBuilder} class.
@@ -93,7 +94,7 @@ public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
* @param tokenContext
* @return
*/
- AuthenticationSessionModel startFreshAuthenticationSession(T token, ActionTokenContext<T> tokenContext);
+ AuthenticationSessionModel startFreshAuthenticationSession(T token, ActionTokenContext<T> tokenContext) throws VerificationException;
/**
* Returns {@code true} when the token can be used repeatedly to invoke the action, {@code false} when the token
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java
index ba44880..0f514d0 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java
@@ -39,7 +39,7 @@ public class DefaultActionToken extends DefaultActionTokenKey implements ActionT
public static final String JSON_FIELD_AUTHENTICATION_SESSION_ID = "asid";
- public static final Predicate<DefaultActionToken> ACTION_TOKEN_BASIC_CHECKS = t -> {
+ public static final Predicate<DefaultActionTokenKey> ACTION_TOKEN_BASIC_CHECKS = t -> {
if (t.getActionVerificationNonce() == null) {
throw new VerificationException("Nonce not present.");
}
@@ -131,7 +131,7 @@ public class DefaultActionToken extends DefaultActionTokenKey implements ActionT
* <li>{@code issuer}: URI of the given realm</li>
* <li>{@code audience}: URI of the given realm (same as issuer)</li>
* </ul>
- *
+ *
* @param session
* @param realm
* @param uri
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
index b41681f..9723005 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
@@ -36,6 +36,9 @@ public class DefaultActionTokenKey extends JsonWebToken implements ActionTokenKe
@JsonProperty(value = JSON_FIELD_ACTION_VERIFICATION_NONCE, required = true)
private UUID actionVerificationNonce;
+ public DefaultActionTokenKey() {
+ }
+
public DefaultActionTokenKey(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce) {
this.subject = userId;
this.type = actionId;
@@ -60,10 +63,6 @@ public class DefaultActionTokenKey extends JsonWebToken implements ActionTokenKe
return actionVerificationNonce;
}
- public String serializeKey() {
- return String.format("%s.%d.%s.%s", getUserId(), getExpiration(), getActionVerificationNonce(), getActionId());
- }
-
public static DefaultActionTokenKey from(String serializedKey) {
if (serializedKey == null) {
return null;
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
index 9993ab7..a1c857f 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
@@ -22,14 +22,20 @@ import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.actiontoken.*;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
+import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.*;
+import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import java.util.Objects;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
/**
*
@@ -64,6 +70,21 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander<
@Override
public Response handleToken(ExecuteActionsActionToken token, ActionTokenContext<ExecuteActionsActionToken> tokenContext) {
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
+ final UriInfo uriInfo = tokenContext.getUriInfo();
+ final RealmModel realm = tokenContext.getRealm();
+ final KeycloakSession session = tokenContext.getSession();
+ if (tokenContext.isAuthenticationSessionFresh()) {
+ // Update the authentication session in the token
+ token.setAuthenticationSessionId(authSession.getId());
+ UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+ String confirmUri = builder.build(realm.getName()).toString();
+
+ return session.getProvider(LoginFormsProvider.class)
+ .setSuccess(Messages.CONFIRM_EXECUTION_OF_ACTIONS)
+ .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
+ .setAttribute(Constants.TEMPLATE_ATTR_REQUIRED_ACTIONS, token.getRequiredActions())
+ .createInfoPage();
+ }
String redirectUri = RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), token.getRedirectUri(),
tokenContext.getRealm(), authSession.getClient());
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
index 7776634..39c6f9a 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
@@ -30,6 +30,7 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
private static final String JSON_FIELD_IDENTITY_PROVIDER_USERNAME = "idpu";
private static final String JSON_FIELD_IDENTITY_PROVIDER_ALIAS = "idpa";
+ private static final String JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID = "oasid";
@JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME)
private String identityProviderUsername;
@@ -37,6 +38,9 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
@JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_ALIAS)
private String identityProviderAlias;
+ @JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
+ private String originalAuthenticationSessionId;
+
public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId,
String identityProviderUsername, String identityProviderAlias) {
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
@@ -62,4 +66,12 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
public void setIdentityProviderAlias(String identityProviderAlias) {
this.identityProviderAlias = identityProviderAlias;
}
+
+ public String getOriginalAuthenticationSessionId() {
+ return originalAuthenticationSessionId;
+ }
+
+ public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) {
+ this.originalAuthenticationSessionId = originalAuthenticationSessionId;
+ }
}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
index bd56eea..c5dc897 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
@@ -24,13 +24,18 @@ import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAut
import org.keycloak.events.*;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.AuthenticationSessionProvider;
import java.util.Collections;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
/**
* Action token handler for verification of e-mail address.
@@ -58,6 +63,9 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
public Response handleToken(IdpVerifyAccountLinkActionToken token, ActionTokenContext<IdpVerifyAccountLinkActionToken> tokenContext) {
UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
EventBuilder event = tokenContext.getEvent();
+ final UriInfo uriInfo = tokenContext.getUriInfo();
+ final RealmModel realm = tokenContext.getRealm();
+ final KeycloakSession session = tokenContext.getSession();
event.event(EventType.IDENTITY_PROVIDER_LINK_ACCOUNT)
.detail(Details.EMAIL, user.getEmail())
@@ -65,16 +73,28 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
.detail(Details.IDENTITY_PROVIDER_USERNAME, token.getIdentityProviderUsername())
.success();
+ AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
+ if (tokenContext.isAuthenticationSessionFresh()) {
+ token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
+ token.setAuthenticationSessionId(authSession.getId());
+ UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+ String confirmUri = builder.build(realm.getName()).toString();
+
+ return session.getProvider(LoginFormsProvider.class)
+ .setSuccess(Messages.CONFIRM_ACCOUNT_LINKING, token.getIdentityProviderUsername(), token.getIdentityProviderAlias())
+ .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
+ .createInfoPage();
+ }
+
// verify user email as we know it is valid as this entry point would never have gotten here.
user.setEmailVerified(true);
- AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
- if (tokenContext.isAuthenticationSessionFresh()) {
- AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession());
- asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true);
+ if (token.getOriginalAuthenticationSessionId() != null) {
+ AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
+ asm.removeAuthenticationSession(realm, authSession, true);
- AuthenticationSessionProvider authSessProvider = tokenContext.getSession().authenticationSessions();
- authSession = authSessProvider.getAuthenticationSession(tokenContext.getRealm(), token.getAuthenticationSessionId());
+ AuthenticationSessionProvider authSessProvider = session.authenticationSessions();
+ authSession = authSessProvider.getAuthenticationSession(realm, token.getOriginalAuthenticationSessionId());
if (authSession != null) {
authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
@@ -85,7 +105,7 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
);
}
- return tokenContext.getSession().getProvider(LoginFormsProvider.class)
+ return session.getProvider(LoginFormsProvider.class)
.setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, token.getIdentityProviderAlias(), token.getIdentityProviderUsername())
.setAttribute(Constants.SKIP_LINK, true)
.createInfoPage();
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java
index 656c518..f9ebc6d 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java
@@ -29,10 +29,14 @@ public class VerifyEmailActionToken extends DefaultActionToken {
public static final String TOKEN_TYPE = "verify-email";
private static final String JSON_FIELD_EMAIL = "eml";
+ private static final String JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID = "oasid";
@JsonProperty(value = JSON_FIELD_EMAIL)
private String email;
+ @JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
+ private String originalAuthenticationSessionId;
+
public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String email) {
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
this.email = email;
@@ -48,4 +52,12 @@ public class VerifyEmailActionToken extends DefaultActionToken {
public void setEmail(String email) {
this.email = email;
}
+
+ public String getOriginalAuthenticationSessionId() {
+ return originalAuthenticationSessionId;
+ }
+
+ public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) {
+ this.originalAuthenticationSessionId = originalAuthenticationSessionId;
+ }
}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
index abe2127..b5d046e 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
@@ -21,14 +21,20 @@ import org.keycloak.TokenVerifier.Predicate;
import org.keycloak.authentication.actiontoken.*;
import org.keycloak.events.*;
import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
+import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import java.util.Objects;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
/**
* Action token handler for verification of e-mail address.
@@ -57,13 +63,29 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
}
@Override
- public Response handleToken(VerifyEmailActionToken token, ActionTokenContext<VerifyEmailActionToken> tokenContext) {
+ public Response handleToken(VerifyEmailActionToken token, ActionTokenContext<VerifyEmailActionToken> tokenContext) {
UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
EventBuilder event = tokenContext.getEvent();
event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail());
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
+ final UriInfo uriInfo = tokenContext.getUriInfo();
+ final RealmModel realm = tokenContext.getRealm();
+ final KeycloakSession session = tokenContext.getSession();
+
+ if (tokenContext.isAuthenticationSessionFresh()) {
+ // Update the authentication session in the token
+ token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
+ token.setAuthenticationSessionId(authSession.getId());
+ UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+ String confirmUri = builder.build(realm.getName()).toString();
+
+ return session.getProvider(LoginFormsProvider.class)
+ .setSuccess(Messages.CONFIRM_EMAIL_ADDRESS_VERIFICATION, user.getEmail())
+ .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
+ .createInfoPage();
+ }
// verify user email as we know it is valid as this entry point would never have gotten here.
user.setEmailVerified(true);
@@ -72,9 +94,10 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
event.success();
- if (tokenContext.isAuthenticationSessionFresh()) {
+ if (token.getOriginalAuthenticationSessionId() != null) {
AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession());
asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true);
+
return tokenContext.getSession().getProvider(LoginFormsProvider.class)
.setSuccess(Messages.EMAIL_VERIFIED)
.createInfoPage();
@@ -82,8 +105,8 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
tokenContext.setEvent(event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN));
- String nextAction = AuthenticationManager.nextRequiredAction(tokenContext.getSession(), authSession, tokenContext.getClientConnection(), tokenContext.getRequest(), tokenContext.getUriInfo(), event);
- return AuthenticationManager.redirectToRequiredActions(tokenContext.getSession(), tokenContext.getRealm(), authSession, tokenContext.getUriInfo(), nextAction);
+ String nextAction = AuthenticationManager.nextRequiredAction(session, authSession, tokenContext.getClientConnection(), tokenContext.getRequest(), uriInfo, event);
+ return AuthenticationManager.redirectToRequiredActions(session, realm, authSession, uriInfo, nextAction);
}
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
index ca841d0..d118634 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
@@ -116,7 +116,7 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
UriInfo uriInfo = session.getContext().getUri();
AuthenticationSessionModel authSession = context.getAuthenticationSession();
- int validityInSecs = realm.getActionTokenGeneratedByAdminLifespan();
+ int validityInSecs = realm.getActionTokenGeneratedByUserLifespan();
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
EventBuilder event = context.getEvent().clone().event(EventType.SEND_IDENTITY_PROVIDER_LINK)
diff --git a/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java b/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java
index 535634b..3d4f163 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java
@@ -35,13 +35,11 @@ public class AuthorizationService {
private final AdminPermissionEvaluator auth;
private final ClientModel client;
- private final KeycloakSession session;
- private final ResourceServer resourceServer;
+ private ResourceServer resourceServer;
private final AuthorizationProvider authorization;
private final AdminEventBuilder adminEvent;
public AuthorizationService(KeycloakSession session, ClientModel client, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
- this.session = session;
this.client = client;
this.authorization = session.getProvider(AuthorizationProvider.class);
this.adminEvent = adminEvent;
@@ -60,7 +58,7 @@ public class AuthorizationService {
public void enable(boolean newClient) {
if (!isEnabled()) {
- resourceServer().create(newClient);
+ this.resourceServer = resourceServer().create(newClient);
}
}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
index e52da9a..9c7a291 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
@@ -19,7 +19,6 @@ package org.keycloak.authorization.admin;
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
-import java.io.IOException;
import java.util.HashMap;
import javax.ws.rs.Consumes;
@@ -36,10 +35,6 @@ import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.ResourceServer;
-import org.keycloak.authorization.store.PolicyStore;
-import org.keycloak.authorization.store.ResourceStore;
-import org.keycloak.authorization.store.ScopeStore;
-import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.exportimport.util.ExportUtils;
@@ -56,8 +51,8 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
-import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.resources.admin.AdminEventBuilder;
+import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -83,7 +78,11 @@ public class ResourceServerService {
this.adminEvent = adminEvent;
}
- public void create(boolean newClient) {
+ public ResourceServer create(boolean newClient) {
+ if (resourceServer != null) {
+ throw new IllegalStateException("Resource server already created");
+ }
+
this.auth.realm().requireManageAuthorization();
UserModel serviceAccount = this.session.users().getServiceAccount(client);
@@ -96,6 +95,8 @@ public class ResourceServerService {
createDefaultRoles(serviceAccount);
createDefaultPermission(createDefaultResource(), createDefaultPolicy());
audit(OperationType.CREATE, uriInfo, newClient);
+
+ return resourceServer;
}
@PUT
@@ -111,22 +112,7 @@ public class ResourceServerService {
public void delete() {
this.auth.realm().requireManageAuthorization();
- StoreFactory storeFactory = authorization.getStoreFactory();
- ResourceStore resourceStore = storeFactory.getResourceStore();
- String id = resourceServer.getId();
-
- PolicyStore policyStore = storeFactory.getPolicyStore();
-
- policyStore.findByResourceServer(id).forEach(scope -> policyStore.delete(scope.getId()));
-
- resourceStore.findByResourceServer(id).forEach(resource -> resourceStore.delete(resource.getId()));
-
- ScopeStore scopeStore = storeFactory.getScopeStore();
-
- scopeStore.findByResourceServer(id).forEach(scope -> scopeStore.delete(scope.getId()));
-
- storeFactory.getResourceServerStore().delete(id);
-
+ authorization.getStoreFactory().getResourceServerStore().delete(resourceServer.getId());
audit(OperationType.DELETE, uriInfo, false);
}
@@ -148,7 +134,7 @@ public class ResourceServerService {
@Path("/import")
@POST
@Consumes(MediaType.APPLICATION_JSON)
- public Response importSettings(@Context final UriInfo uriInfo, ResourceServerRepresentation rep) throws IOException {
+ public Response importSettings(@Context final UriInfo uriInfo, ResourceServerRepresentation rep) {
this.auth.realm().requireManageAuthorization();
rep.setClientId(client.getId());
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
index 7c95281..3f8b737 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
@@ -101,39 +101,24 @@ public class ResourceSetService {
Resource existingResource = storeFactory.getResourceStore().findByName(resource.getName(), this.resourceServer.getId());
ResourceOwnerRepresentation owner = resource.getOwner();
- if (existingResource != null && existingResource.getResourceServer().getId().equals(this.resourceServer.getId())
- && existingResource.getOwner().equals(owner)) {
- return ErrorResponse.exists("Resource with name [" + resource.getName() + "] already exists.");
+ if (owner == null) {
+ owner = new ResourceOwnerRepresentation();
+ owner.setId(resourceServer.getClientId());
}
- if (owner != null) {
- String ownerId = owner.getId();
-
- if (ownerId != null) {
- if (!resourceServer.getClientId().equals(ownerId)) {
- RealmModel realm = authorization.getRealm();
- KeycloakSession keycloakSession = authorization.getKeycloakSession();
- UserProvider users = keycloakSession.users();
- UserModel ownerModel = users.getUserById(ownerId, realm);
-
- if (ownerModel == null) {
- ownerModel = users.getUserByUsername(ownerId, realm);
- }
-
- if (ownerModel == null) {
- return ErrorResponse.error("Owner must be a valid username or user identifier. If the resource server, the client id or null.", Status.BAD_REQUEST);
- }
+ String ownerId = owner.getId();
- owner.setId(ownerModel.getId());
- }
- }
+ if (ownerId == null) {
+ return ErrorResponse.error("You must specify the resource owner.", Status.BAD_REQUEST);
}
- Resource model = toModel(resource, this.resourceServer, authorization);
+ if (existingResource != null && existingResource.getOwner().equals(ownerId)) {
+ return ErrorResponse.exists("Resource with name [" + resource.getName() + "] already exists.");
+ }
ResourceRepresentation representation = new ResourceRepresentation();
- representation.setId(model.getId());
+ representation.setId(toModel(resource, this.resourceServer, authorization).getId());
return Response.status(Status.CREATED).entity(representation).build();
}
diff --git a/services/src/main/java/org/keycloak/authorization/common/ClientModelIdentity.java b/services/src/main/java/org/keycloak/authorization/common/ClientModelIdentity.java
new file mode 100644
index 0000000..d2c6b67
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/common/ClientModelIdentity.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.common;
+
+import org.keycloak.authorization.attribute.Attributes;
+import org.keycloak.authorization.identity.Identity;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ClientModelIdentity implements Identity {
+ protected RealmModel realm;
+ protected ClientModel client;
+ protected UserModel serviceAccount;
+
+ public ClientModelIdentity(KeycloakSession session, ClientModel client) {
+ this.realm = client.getRealm();
+ this.client = client;
+ this.serviceAccount = session.users().getServiceAccount(client);
+ }
+
+ @Override
+ public String getId() {
+ return client.getId();
+ }
+
+ @Override
+ public Attributes getAttributes() {
+ MultivaluedHashMap map = new MultivaluedHashMap<String, String>();
+ if (serviceAccount != null) map.addAll(serviceAccount.getAttributes());
+ return Attributes.from(map);
+ }
+
+ @Override
+ public boolean hasRealmRole(String roleName) {
+ if (serviceAccount == null) return false;
+ RoleModel role = realm.getRole(roleName);
+ if (role == null) return false;
+ return serviceAccount.hasRole(role);
+ }
+
+ @Override
+ public boolean hasClientRole(String clientId, String roleName) {
+ if (serviceAccount == null) return false;
+ ClientModel client = realm.getClientByClientId(clientId);
+ RoleModel role = client.getRole(roleName);
+ if (role == null) return false;
+ return serviceAccount.hasRole(role);
+ }
+
+ @Override
+ public boolean hasRole(String roleName) {
+ throw new RuntimeException("Should not execute");
+ }
+
+ @Override
+ public boolean hasClientRole(String roleName) {
+ throw new RuntimeException("Should not execute");
+ }
+}
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
index 45183c3..a5304ba 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -75,7 +75,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
String defaultScope = config.getDefaultScope();
if (!defaultScope.contains(SCOPE_OPENID)) {
- config.setDefaultScope(SCOPE_OPENID + " " + defaultScope);
+ config.setDefaultScope((SCOPE_OPENID + " " + defaultScope).trim());
}
}
@@ -232,48 +232,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
JsonWebToken idToken = validateToken(encodedIdToken);
try {
- String id = idToken.getSubject();
- BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
- String name = (String)idToken.getOtherClaims().get(IDToken.NAME);
- String preferredUsername = (String)idToken.getOtherClaims().get(IDToken.PREFERRED_USERNAME);
- String email = (String)idToken.getOtherClaims().get(IDToken.EMAIL);
-
- if (!getConfig().isDisableUserInfoService()) {
- String userInfoUrl = getUserInfoUrl();
- if (userInfoUrl != null && !userInfoUrl.isEmpty() && (id == null || name == null || preferredUsername == null || email == null)) {
- SimpleHttp request = JsonSimpleHttp.doGet(userInfoUrl, session)
- .header("Authorization", "Bearer " + accessToken);
- JsonNode userInfo = JsonSimpleHttp.asJson(request);
-
- id = getJsonProperty(userInfo, "sub");
- name = getJsonProperty(userInfo, "name");
- preferredUsername = getJsonProperty(userInfo, "preferred_username");
- email = getJsonProperty(userInfo, "email");
- AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
- }
- }
- identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
- identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
- processAccessTokenResponse(identity, tokenResponse);
-
- identity.setId(id);
- identity.setName(name);
- identity.setEmail(email);
-
- identity.setBrokerUserId(getConfig().getAlias() + "." + id);
- if (tokenResponse.getSessionState() != null) {
- identity.setBrokerSessionId(getConfig().getAlias() + "." + tokenResponse.getSessionState());
- }
-
- if (preferredUsername == null) {
- preferredUsername = email;
- }
-
- if (preferredUsername == null) {
- preferredUsername = id;
- }
-
- identity.setUsername(preferredUsername);
+ BrokeredIdentityContext identity = extractIdentity(tokenResponse, accessToken, idToken);
if (getConfig().isStoreToken()) {
identity.setToken(response);
@@ -285,6 +244,56 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
}
+ protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
+ String id = idToken.getSubject();
+ BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
+ String name = (String)idToken.getOtherClaims().get(IDToken.NAME);
+ String preferredUsername = (String)idToken.getOtherClaims().get(getUsernameClaimName());
+ String email = (String)idToken.getOtherClaims().get(IDToken.EMAIL);
+
+ if (!getConfig().isDisableUserInfoService()) {
+ String userInfoUrl = getUserInfoUrl();
+ if (userInfoUrl != null && !userInfoUrl.isEmpty() && (id == null || name == null || preferredUsername == null || email == null)) {
+ SimpleHttp request = JsonSimpleHttp.doGet(userInfoUrl, session)
+ .header("Authorization", "Bearer " + accessToken);
+ JsonNode userInfo = JsonSimpleHttp.asJson(request);
+
+ id = getJsonProperty(userInfo, "sub");
+ name = getJsonProperty(userInfo, "name");
+ preferredUsername = getJsonProperty(userInfo, "preferred_username");
+ email = getJsonProperty(userInfo, "email");
+ AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
+ }
+ }
+ identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
+ identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
+ processAccessTokenResponse(identity, tokenResponse);
+
+ identity.setId(id);
+ identity.setName(name);
+ identity.setEmail(email);
+
+ identity.setBrokerUserId(getConfig().getAlias() + "." + id);
+ if (tokenResponse.getSessionState() != null) {
+ identity.setBrokerSessionId(getConfig().getAlias() + "." + tokenResponse.getSessionState());
+ }
+
+ if (preferredUsername == null) {
+ preferredUsername = email;
+ }
+
+ if (preferredUsername == null) {
+ preferredUsername = id;
+ }
+
+ identity.setUsername(preferredUsername);
+ return identity;
+ }
+
+ protected String getUsernameClaimName() {
+ return IDToken.PREFERRED_USERNAME;
+ }
+
protected String getUserInfoUrl() {
return getConfig().getUserInfoUrl();
}
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLDataMarshaller.java b/services/src/main/java/org/keycloak/broker/saml/SAMLDataMarshaller.java
index 508fcbd..dc32463 100644
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLDataMarshaller.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLDataMarshaller.java
@@ -51,11 +51,11 @@ public class SAMLDataMarshaller extends DefaultDataMarshaller {
if (obj instanceof ResponseType) {
ResponseType responseType = (ResponseType) obj;
SAMLResponseWriter samlWriter = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(bos));
- samlWriter.write(responseType);
+ samlWriter.write(responseType, true);
} else if (obj instanceof AssertionType) {
AssertionType assertion = (AssertionType) obj;
SAMLAssertionWriter samlWriter = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(bos));
- samlWriter.write(assertion);
+ samlWriter.write(assertion, true);
} else if (obj instanceof AuthnStatementType) {
AuthnStatementType authnStatement = (AuthnStatementType) obj;
SAMLAssertionWriter samlWriter = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(bos));
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
index 4451b8c..ce5b77f 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
@@ -54,7 +54,6 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil;
-import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
@@ -88,10 +87,13 @@ import java.util.List;
import org.keycloak.rotation.HardcodedKeyLocator;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
-import org.w3c.dom.Document;
+import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
import org.w3c.dom.Element;
import java.util.*;
+import javax.xml.crypto.dsig.XMLSignature;
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -517,6 +519,17 @@ public class SAMLEndpoint {
protected class PostBinding extends Binding {
@Override
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
+ NodeList nl = documentHolder.getSamlDocument().getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
+ boolean anyElementSigned = (nl != null && nl.getLength() > 0);
+ if ((! anyElementSigned) && (documentHolder.getSamlObject() instanceof ResponseType)) {
+ ResponseType responseType = (ResponseType) documentHolder.getSamlObject();
+ List<ResponseType.RTChoiceType> assertions = responseType.getAssertions();
+ if (! assertions.isEmpty() ) {
+ // Only relax verification if the response is an authnresponse and contains (encrypted/plaintext) assertion.
+ // In that case, signature is validated on assertion element
+ return;
+ }
+ }
SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKeyLocator());
}
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
index 886ee4d..6a4470b 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
@@ -54,6 +54,7 @@ import java.util.Set;
import java.util.TreeSet;
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
import org.keycloak.keys.KeyMetadata;
+import org.keycloak.keys.KeyMetadata.Status;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.sessions.AuthenticationSessionModel;
@@ -237,18 +238,27 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
boolean wantAuthnRequestsSigned = getConfig().isWantAuthnRequestsSigned();
boolean wantAssertionsSigned = getConfig().isWantAssertionsSigned();
+ boolean wantAssertionsEncrypted = getConfig().isWantAssertionsEncrypted();
String entityId = getEntityId(uriInfo, realm);
String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat();
- StringBuilder keysString = new StringBuilder();
+ StringBuilder signingKeysString = new StringBuilder();
+ StringBuilder encryptionKeysString = new StringBuilder();
Set<RsaKeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
? (int) (o2.getProviderPriority() - o1.getProviderPriority())
: (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
keys.addAll(session.keys().getRsaKeys(realm, false));
for (RsaKeyMetadata key : keys) {
- addKeyInfo(keysString, key, KeyTypes.SIGNING.value());
+ addKeyInfo(signingKeysString, key, KeyTypes.SIGNING.value());
+
+ if (key.getStatus() == Status.ACTIVE) {
+ addKeyInfo(encryptionKeysString, key, KeyTypes.ENCRYPTION.value());
+ }
}
- String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint, wantAuthnRequestsSigned, wantAssertionsSigned, entityId, nameIDPolicyFormat, keysString.toString());
+ String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint,
+ wantAuthnRequestsSigned, wantAssertionsSigned, wantAssertionsEncrypted,
+ entityId, nameIDPolicyFormat, signingKeysString.toString(), encryptionKeysString.toString());
+
return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build();
}
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java
index 4d200a0..c6d999c 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java
@@ -27,6 +27,24 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
public static final XmlKeyInfoKeyNameTransformer DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER = XmlKeyInfoKeyNameTransformer.NONE;
+ public static final String ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO = "addExtensionsElementWithKeyInfo";
+ public static final String BACKCHANNEL_SUPPORTED = "backchannelSupported";
+ public static final String ENCRYPTION_PUBLIC_KEY = "encryptionPublicKey";
+ public static final String FORCE_AUTHN = "forceAuthn";
+ public static final String NAME_ID_POLICY_FORMAT = "nameIDPolicyFormat";
+ public static final String POST_BINDING_AUTHN_REQUEST = "postBindingAuthnRequest";
+ public static final String POST_BINDING_LOGOUT = "postBindingLogout";
+ public static final String POST_BINDING_RESPONSE = "postBindingResponse";
+ public static final String SIGNATURE_ALGORITHM = "signatureAlgorithm";
+ public static final String SIGNING_CERTIFICATE_KEY = "signingCertificate";
+ public static final String SINGLE_LOGOUT_SERVICE_URL = "singleLogoutServiceUrl";
+ public static final String SINGLE_SIGN_ON_SERVICE_URL = "singleSignOnServiceUrl";
+ public static final String VALIDATE_SIGNATURE = "validateSignature";
+ public static final String WANT_ASSERTIONS_ENCRYPTED = "wantAssertionsEncrypted";
+ public static final String WANT_ASSERTIONS_SIGNED = "wantAssertionsSigned";
+ public static final String WANT_AUTHN_REQUESTS_SIGNED = "wantAuthnRequestsSigned";
+ public static final String XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER = "xmlSigKeyInfoKeyNameTransformer";
+
public SAMLIdentityProviderConfig() {
}
@@ -35,35 +53,35 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
}
public String getSingleSignOnServiceUrl() {
- return getConfig().get("singleSignOnServiceUrl");
+ return getConfig().get(SINGLE_SIGN_ON_SERVICE_URL);
}
public void setSingleSignOnServiceUrl(String singleSignOnServiceUrl) {
- getConfig().put("singleSignOnServiceUrl", singleSignOnServiceUrl);
+ getConfig().put(SINGLE_SIGN_ON_SERVICE_URL, singleSignOnServiceUrl);
}
public String getSingleLogoutServiceUrl() {
- return getConfig().get("singleLogoutServiceUrl");
+ return getConfig().get(SINGLE_LOGOUT_SERVICE_URL);
}
public void setSingleLogoutServiceUrl(String singleLogoutServiceUrl) {
- getConfig().put("singleLogoutServiceUrl", singleLogoutServiceUrl);
+ getConfig().put(SINGLE_LOGOUT_SERVICE_URL, singleLogoutServiceUrl);
}
public boolean isValidateSignature() {
- return Boolean.valueOf(getConfig().get("validateSignature"));
+ return Boolean.valueOf(getConfig().get(VALIDATE_SIGNATURE));
}
public void setValidateSignature(boolean validateSignature) {
- getConfig().put("validateSignature", String.valueOf(validateSignature));
+ getConfig().put(VALIDATE_SIGNATURE, String.valueOf(validateSignature));
}
public boolean isForceAuthn() {
- return Boolean.valueOf(getConfig().get("forceAuthn"));
+ return Boolean.valueOf(getConfig().get(FORCE_AUTHN));
}
public void setForceAuthn(boolean forceAuthn) {
- getConfig().put("forceAuthn", String.valueOf(forceAuthn));
+ getConfig().put(FORCE_AUTHN, String.valueOf(forceAuthn));
}
/**
@@ -103,82 +121,80 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
return crt.split(",");
}
- public static final String SIGNING_CERTIFICATE_KEY = "signingCertificate";
-
public String getNameIDPolicyFormat() {
- return getConfig().get("nameIDPolicyFormat");
+ return getConfig().get(NAME_ID_POLICY_FORMAT);
}
public void setNameIDPolicyFormat(String nameIDPolicyFormat) {
- getConfig().put("nameIDPolicyFormat", nameIDPolicyFormat);
+ getConfig().put(NAME_ID_POLICY_FORMAT, nameIDPolicyFormat);
}
public boolean isWantAuthnRequestsSigned() {
- return Boolean.valueOf(getConfig().get("wantAuthnRequestsSigned"));
+ return Boolean.valueOf(getConfig().get(WANT_AUTHN_REQUESTS_SIGNED));
}
public void setWantAuthnRequestsSigned(boolean wantAuthnRequestsSigned) {
- getConfig().put("wantAuthnRequestsSigned", String.valueOf(wantAuthnRequestsSigned));
+ getConfig().put(WANT_AUTHN_REQUESTS_SIGNED, String.valueOf(wantAuthnRequestsSigned));
}
public boolean isWantAssertionsSigned() {
- return Boolean.valueOf(getConfig().get("wantAssertionsSigned"));
+ return Boolean.valueOf(getConfig().get(WANT_ASSERTIONS_SIGNED));
}
public void setWantAssertionsSigned(boolean wantAssertionsSigned) {
- getConfig().put("wantAssertionsSigned", String.valueOf(wantAssertionsSigned));
+ getConfig().put(WANT_ASSERTIONS_SIGNED, String.valueOf(wantAssertionsSigned));
}
public boolean isWantAssertionsEncrypted() {
- return Boolean.valueOf(getConfig().get("wantAssertionsEncrypted"));
+ return Boolean.valueOf(getConfig().get(WANT_ASSERTIONS_ENCRYPTED));
}
public void setWantAssertionsEncrypted(boolean wantAssertionsEncrypted) {
- getConfig().put("wantAssertionsEncrypted", String.valueOf(wantAssertionsEncrypted));
+ getConfig().put(WANT_ASSERTIONS_ENCRYPTED, String.valueOf(wantAssertionsEncrypted));
}
public boolean isAddExtensionsElementWithKeyInfo() {
- return Boolean.valueOf(getConfig().get("addExtensionsElementWithKeyInfo"));
+ return Boolean.valueOf(getConfig().get(ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO));
}
public void setAddExtensionsElementWithKeyInfo(boolean addExtensionsElementWithKeyInfo) {
- getConfig().put("addExtensionsElementWithKeyInfo", String.valueOf(addExtensionsElementWithKeyInfo));
+ getConfig().put(ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO, String.valueOf(addExtensionsElementWithKeyInfo));
}
public String getSignatureAlgorithm() {
- return getConfig().get("signatureAlgorithm");
+ return getConfig().get(SIGNATURE_ALGORITHM);
}
public void setSignatureAlgorithm(String signatureAlgorithm) {
- getConfig().put("signatureAlgorithm", signatureAlgorithm);
+ getConfig().put(SIGNATURE_ALGORITHM, signatureAlgorithm);
}
public String getEncryptionPublicKey() {
- return getConfig().get("encryptionPublicKey");
+ return getConfig().get(ENCRYPTION_PUBLIC_KEY);
}
public void setEncryptionPublicKey(String encryptionPublicKey) {
- getConfig().put("encryptionPublicKey", encryptionPublicKey);
+ getConfig().put(ENCRYPTION_PUBLIC_KEY, encryptionPublicKey);
}
public boolean isPostBindingAuthnRequest() {
- return Boolean.valueOf(getConfig().get("postBindingAuthnRequest"));
+ return Boolean.valueOf(getConfig().get(POST_BINDING_AUTHN_REQUEST));
}
public void setPostBindingAuthnRequest(boolean postBindingAuthnRequest) {
- getConfig().put("postBindingAuthnRequest", String.valueOf(postBindingAuthnRequest));
+ getConfig().put(POST_BINDING_AUTHN_REQUEST, String.valueOf(postBindingAuthnRequest));
}
public boolean isPostBindingResponse() {
- return Boolean.valueOf(getConfig().get("postBindingResponse"));
+ return Boolean.valueOf(getConfig().get(POST_BINDING_RESPONSE));
}
public void setPostBindingResponse(boolean postBindingResponse) {
- getConfig().put("postBindingResponse", String.valueOf(postBindingResponse));
+ getConfig().put(POST_BINDING_RESPONSE, String.valueOf(postBindingResponse));
}
public boolean isPostBindingLogout() {
- String postBindingLogout = getConfig().get("postBindingLogout");
+ String postBindingLogout = getConfig().get(POST_BINDING_LOGOUT);
if (postBindingLogout == null) {
// To maintain unchanged behavior when adding this field, we set the inital value to equal that
// of the binding for the response:
@@ -188,15 +204,15 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
}
public void setPostBindingLogout(boolean postBindingLogout) {
- getConfig().put("postBindingLogout", String.valueOf(postBindingLogout));
+ getConfig().put(POST_BINDING_LOGOUT, String.valueOf(postBindingLogout));
}
public boolean isBackchannelSupported() {
- return Boolean.valueOf(getConfig().get("backchannelSupported"));
+ return Boolean.valueOf(getConfig().get(BACKCHANNEL_SUPPORTED));
}
public void setBackchannelSupported(boolean backchannel) {
- getConfig().put("backchannelSupported", String.valueOf(backchannel));
+ getConfig().put(BACKCHANNEL_SUPPORTED, String.valueOf(backchannel));
}
/**
@@ -204,11 +220,11 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
* @return Configured ransformer of {@link #DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER} if not set.
*/
public XmlKeyInfoKeyNameTransformer getXmlSigKeyInfoKeyNameTransformer() {
- return XmlKeyInfoKeyNameTransformer.from(getConfig().get("xmlSigKeyInfoKeyNameTransformer"), DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
+ return XmlKeyInfoKeyNameTransformer.from(getConfig().get(XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER), DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
}
public void setXmlSigKeyInfoKeyNameTransformer(XmlKeyInfoKeyNameTransformer xmlSigKeyInfoKeyNameTransformer) {
- getConfig().put("xmlSigKeyInfoKeyNameTransformer",
+ getConfig().put(XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER,
xmlSigKeyInfoKeyNameTransformer == null
? null
: xmlSigKeyInfoKeyNameTransformer.name());
diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
index 7477d84..ca3575c 100644
--- a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
+++ b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
@@ -20,7 +20,6 @@ package org.keycloak.email;
import com.sun.mail.smtp.SMTPMessage;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
import org.keycloak.truststore.HostnameVerificationPolicy;
@@ -57,20 +56,22 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
}
@Override
- public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
+ public void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
Transport transport = null;
try {
String address = retrieveEmailAddress(user);
- Map<String, String> config = realm.getSmtpConfig();
Properties props = new Properties();
- props.setProperty("mail.smtp.host", config.get("host"));
+
+ if (config.containsKey("host")) {
+ props.setProperty("mail.smtp.host", config.get("host"));
+ }
boolean auth = "true".equals(config.get("auth"));
boolean ssl = "true".equals(config.get("ssl"));
boolean starttls = "true".equals(config.get("starttls"));
- if (config.containsKey("port")) {
+ if (config.containsKey("port") && config.get("port") != null) {
props.setProperty("mail.smtp.port", config.get("port"));
}
@@ -103,13 +104,13 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
Multipart multipart = new MimeMultipart("alternative");
- if(textBody != null) {
+ if (textBody != null) {
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText(textBody, "UTF-8");
multipart.addBodyPart(textPart);
}
- if(htmlBody != null) {
+ if (htmlBody != null) {
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(htmlBody, "text/html; charset=UTF-8");
multipart.addBodyPart(htmlPart);
@@ -153,13 +154,16 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
}
}
- protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException {
+ protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException, EmailException {
+ if (email == null || "".equals(email.trim())) {
+ throw new EmailException("Please provide a valid address", null);
+ }
if (displayName == null || "".equals(displayName.trim())) {
return new InternetAddress(email);
}
return new InternetAddress(email, displayName, "utf-8");
}
-
+
protected String retrieveEmailAddress(UserModel user) {
return user.getEmail();
}
diff --git a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
index 5105eae..ddf29a1 100755
--- a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
+++ b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
@@ -97,7 +97,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
@Override
public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException {
- Map<String, Object> attributes = new HashMap<String, Object>();
+ Map<String, Object> attributes = new HashMap<String, Object>(this.attributes);
attributes.put("user", new ProfileBean(user));
attributes.put("link", link);
attributes.put("linkExpiration", expirationInMinutes);
@@ -108,8 +108,21 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
}
@Override
+ public void sendSmtpTestEmail(Map<String, String> config, UserModel user) throws EmailException {
+ setRealm(session.getContext().getRealm());
+ setUser(user);
+
+ Map<String, Object> attributes = new HashMap<String, Object>(this.attributes);
+ attributes.put("user", new ProfileBean(user));
+ attributes.put("realmName", realm.getName());
+
+ EmailTemplate email = processTemplate("emailTestSubject", Collections.emptyList(), "email-test.ftl", attributes);
+ send(config, email.getSubject(), email.getTextBody(), email.getHtmlBody());
+ }
+
+ @Override
public void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException {
- Map<String, Object> attributes = new HashMap<String, Object>();
+ Map<String, Object> attributes = new HashMap<String, Object>(this.attributes);
attributes.put("user", new ProfileBean(user));
attributes.put("link", link);
attributes.put("linkExpiration", expirationInMinutes);
@@ -129,7 +142,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
@Override
public void sendExecuteActions(String link, long expirationInMinutes) throws EmailException {
- Map<String, Object> attributes = new HashMap<String, Object>();
+ Map<String, Object> attributes = new HashMap<String, Object>(this.attributes);
attributes.put("user", new ProfileBean(user));
attributes.put("link", link);
attributes.put("linkExpiration", expirationInMinutes);
@@ -142,7 +155,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
@Override
public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException {
- Map<String, Object> attributes = new HashMap<String, Object>();
+ Map<String, Object> attributes = new HashMap<String, Object>(this.attributes);
attributes.put("user", new ProfileBean(user));
attributes.put("link", link);
attributes.put("linkExpiration", expirationInMinutes);
@@ -156,7 +169,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
send(subjectKey, Collections.emptyList(), template, attributes);
}
- private void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
+ private EmailTemplate processTemplate(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
try {
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL);
@@ -168,27 +181,39 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
String textTemplate = String.format("text/%s", template);
String textBody;
try {
- textBody = freeMarker.processTemplate(attributes, textTemplate, theme);
+ textBody = freeMarker.processTemplate(attributes, textTemplate, theme);
} catch (final FreeMarkerException e ) {
- textBody = null;
+ textBody = null;
}
String htmlTemplate = String.format("html/%s", template);
String htmlBody;
try {
- htmlBody = freeMarker.processTemplate(attributes, htmlTemplate, theme);
+ htmlBody = freeMarker.processTemplate(attributes, htmlTemplate, theme);
} catch (final FreeMarkerException e ) {
- htmlBody = null;
+ htmlBody = null;
}
- send(subject, textBody, htmlBody);
+ return new EmailTemplate(subject, textBody, htmlBody);
+ } catch (Exception e) {
+ throw new EmailException("Failed to template email", e);
+ }
+ }
+ private void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
+ try {
+ EmailTemplate email = processTemplate(subjectKey, subjectAttributes, template, attributes);
+ send(email.getSubject(), email.getTextBody(), email.getHtmlBody());
} catch (Exception e) {
throw new EmailException("Failed to template email", e);
}
}
private void send(String subject, String textBody, String htmlBody) throws EmailException {
+ send(realm.getSmtpConfig(), subject, textBody, htmlBody);
+ }
+
+ private void send(Map<String, String> config, String subject, String textBody, String htmlBody) throws EmailException {
EmailSenderProvider emailSender = session.getProvider(EmailSenderProvider.class);
- emailSender.send(realm, user, subject, textBody, htmlBody);
+ emailSender.send(config, user, subject, textBody, htmlBody);
}
@Override
@@ -203,4 +228,29 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
return sb.toString();
}
+ private class EmailTemplate {
+
+ private String subject;
+ private String textBody;
+ private String htmlBody;
+
+ public EmailTemplate(String subject, String textBody, String htmlBody) {
+ this.subject = subject;
+ this.textBody = textBody;
+ this.htmlBody = htmlBody;
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public String getTextBody() {
+ return textBody;
+ }
+
+ public String getHtmlBody() {
+ return htmlBody;
+ }
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java
index de5fd93..2669c29 100755
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java
@@ -19,6 +19,7 @@ package org.keycloak.forms.account.freemarker.model;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
@@ -27,8 +28,10 @@ import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.managers.UserSessionManager;
+import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -51,10 +54,17 @@ public class ApplicationsBean {
continue;
}
- Set<RoleModel> availableRoles = TokenManager.getAccess(null, false, client, user);
- // Don't show applications, which user doesn't have access into (any available roles)
- if (availableRoles.isEmpty()) {
- continue;
+ Set<RoleModel> availableRoles = new HashSet<>();
+ if (client.getClientId().equals(Constants.ADMIN_CLI_CLIENT_ID)
+ || client.getClientId().equals(Constants.ADMIN_CONSOLE_CLIENT_ID)) {
+ if (!AdminPermissions.realms(session, realm, user).isAdmin()) continue;
+
+ } else {
+ availableRoles = TokenManager.getAccess(null, false, client, user);
+ // Don't show applications, which user doesn't have access into (any available roles)
+ if (availableRoles.isEmpty()) {
+ continue;
+ }
}
List<RoleModel> realmRolesAvailable = new LinkedList<RoleModel>();
MultivaluedHashMap<String, ClientRoleEntry> resourceRolesAvailable = new MultivaluedHashMap<String, ClientRoleEntry>();
@@ -140,7 +150,48 @@ public class ApplicationsBean {
public MultivaluedHashMap<String, ClientRoleEntry> getResourceRolesGranted() {
return resourceRolesGranted;
}
-
+
+ public String getEffectiveUrl() {
+ String rootUrl = getClient().getRootUrl();
+ String baseUrl = getClient().getBaseUrl();
+
+ if (rootUrl == null) rootUrl = "";
+ if (baseUrl == null) baseUrl = "";
+
+ if (rootUrl.equals("") && baseUrl.equals("")) {
+ return "";
+ }
+
+ if (rootUrl.equals("") && !baseUrl.equals("")) {
+ return baseUrl;
+ }
+
+ if (!rootUrl.equals("") && baseUrl.equals("")) {
+ return rootUrl;
+ }
+
+ if (isBaseUrlRelative() && !rootUrl.equals("")) {
+ return concatUrls(rootUrl, baseUrl);
+ }
+
+ return baseUrl;
+ }
+
+ private String concatUrls(String u1, String u2) {
+ if (u1.endsWith("/")) u1 = u1.substring(0, u1.length() - 1);
+ if (u2.startsWith("/")) u2 = u2.substring(1);
+ return u1 + "/" + u2;
+ }
+
+ private boolean isBaseUrlRelative() {
+ String baseUrl = getClient().getBaseUrl();
+ if (baseUrl.equals("")) return false;
+ if (baseUrl.startsWith("/")) return true;
+ if (baseUrl.startsWith("./")) return true;
+ if (baseUrl.startsWith("../")) return true;
+ return false;
+ }
+
public ClientModel getClient() {
return client;
}
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index d7eb01c..8ec6a5b 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -449,7 +449,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
public Response createIdpLinkEmailPage() {
BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT);
String idpAlias = brokerContext.getIdpConfig().getAlias();
- idpAlias = ObjectUtil.capitalize(idpAlias);;
+ idpAlias = ObjectUtil.capitalize(idpAlias);
setMessage(MessageType.WARNING, Messages.LINK_IDP, idpAlias);
return createResponse(LoginFormsPages.LOGIN_IDP_LINK_EMAIL);
diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
index 9c1e5a5..c06caa0 100755
--- a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
+++ b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
@@ -22,6 +22,7 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
@@ -29,9 +30,12 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol.Error;
+import org.keycloak.services.ErrorPageException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.managers.UserSessionCrossDCManager;
+import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.AuthenticationFlowURLHelper;
@@ -62,7 +66,7 @@ public abstract class AuthorizationEndpointBase {
@Context
protected HttpHeaders headers;
@Context
- protected HttpRequest request;
+ protected HttpRequest httpRequest;
@Context
protected KeycloakSession session;
@Context
@@ -84,7 +88,7 @@ public abstract class AuthorizationEndpointBase {
.setRealm(realm)
.setSession(session)
.setUriInfo(uriInfo)
- .setRequest(request);
+ .setRequest(httpRequest);
authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, flowPath);
@@ -147,6 +151,19 @@ public abstract class AuthorizationEndpointBase {
return realm.getBrowserFlow();
}
+ protected void checkSsl() {
+ if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
+ event.error(Errors.SSL_REQUIRED);
+ throw new ErrorPageException(session, Messages.HTTPS_REQUIRED);
+ }
+ }
+
+ protected void checkRealm() {
+ if (!realm.isEnabled()) {
+ event.error(Errors.REALM_DISABLED);
+ throw new ErrorPageException(session, Messages.REALM_NOT_ENABLED);
+ }
+ }
protected AuthorizationEndpointChecks getOrCreateAuthenticationSession(ClientModel client, String requestState) {
AuthenticationSessionManager manager = new AuthenticationSessionManager(session);
@@ -192,7 +209,7 @@ public abstract class AuthorizationEndpointBase {
}
}
- UserSessionModel userSession = authSessionId==null ? null : session.sessions().getUserSession(realm, authSessionId);
+ UserSessionModel userSession = authSessionId==null ? null : new UserSessionCrossDCManager(session).getUserSessionIfExistsRemotely(realm, authSessionId);
if (userSession != null) {
logger.debugf("Sent request to authz endpoint. We don't have authentication session with ID '%s' but we have userSession. Will re-create authentication session with same ID", authSessionId);
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticator.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticator.java
new file mode 100644
index 0000000..b2c2b37
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticator.java
@@ -0,0 +1,76 @@
+package org.keycloak.protocol.docker;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.specimpl.ResponseBuilderImpl;
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.events.Errors;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator;
+import org.keycloak.representations.docker.DockerAccess;
+import org.keycloak.representations.docker.DockerError;
+import org.keycloak.representations.docker.DockerErrorResponseToken;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Optional;
+
+public class DockerAuthenticator extends HttpBasicAuthenticator {
+ private static final Logger logger = Logger.getLogger(DockerAuthenticator.class);
+
+ public static final String ID = "docker-http-basic-authenticator";
+
+ @Override
+ protected void notValidCredentialsAction(final AuthenticationFlowContext context, final RealmModel realm, final UserModel user) {
+ invalidUserAction(context, realm, user.getUsername(), context.getSession().getContext().resolveLocale(user));
+ }
+
+ @Override
+ protected void nullUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String userId) {
+ final String localeString = Optional.ofNullable(realm.getDefaultLocale()).orElse(Locale.ENGLISH.toString());
+ invalidUserAction(context, realm, userId, new Locale(localeString));
+ }
+
+ @Override
+ protected void userDisabledAction(AuthenticationFlowContext context, RealmModel realm, UserModel user) {
+ context.getEvent().user(user);
+ context.getEvent().error(Errors.USER_DISABLED);
+
+ final DockerError error = new DockerError("UNAUTHORIZED","Invalid username or password.",
+ Collections.singletonList(new DockerAccess(context.getAuthenticationSession().getClientNote(DockerAuthV2Protocol.SCOPE_PARAM))));
+
+ context.failure(AuthenticationFlowError.USER_DISABLED, new ResponseBuilderImpl()
+ .status(Response.Status.UNAUTHORIZED)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+ .entity(new DockerErrorResponseToken(Collections.singletonList(error)))
+ .build());
+ }
+
+ /**
+ * For Docker protocol the same error message will be returned for invalid credentials and incorrect user name. For SAML
+ * ECP, there is a different behavior for each.
+ */
+ private void invalidUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String userId, final Locale locale) {
+ context.getEvent().user(userId);
+ context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
+
+ final DockerError error = new DockerError("UNAUTHORIZED","Invalid username or password.",
+ Collections.singletonList(new DockerAccess(context.getAuthenticationSession().getClientNote(DockerAuthV2Protocol.SCOPE_PARAM))));
+
+ context.failure(AuthenticationFlowError.INVALID_USER, new ResponseBuilderImpl()
+ .status(Response.Status.UNAUTHORIZED)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+ .entity(new DockerErrorResponseToken(Collections.singletonList(error)))
+ .build());
+ }
+
+ @Override
+ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+ return true;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticatorFactory.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticatorFactory.java
new file mode 100644
index 0000000..9bba9c4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticatorFactory.java
@@ -0,0 +1,84 @@
+package org.keycloak.protocol.docker;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.common.Profile;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.provider.EnvironmentDependentProviderFactory;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.keycloak.models.AuthenticationExecutionModel.Requirement;
+
+public class DockerAuthenticatorFactory implements AuthenticatorFactory {
+
+ @Override
+ public String getHelpText() {
+ return "Uses HTTP Basic authentication to validate docker users, returning a docker error token on auth failure";
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Docker Authenticator";
+ }
+
+ @Override
+ public String getReferenceCategory() {
+ return "docker";
+ }
+
+ @Override
+ public boolean isConfigurable() {
+ return false;
+ }
+
+ private static final Requirement[] REQUIREMENT_CHOICES = {
+ Requirement.REQUIRED,
+ };
+
+ @Override
+ public Requirement[] getRequirementChoices() {
+ return REQUIREMENT_CHOICES;
+ }
+
+
+ @Override
+ public boolean isUserSetupAllowed() {
+ return false;
+ }
+
+ @Override
+ public Authenticator create(KeycloakSession session) {
+ return new DockerAuthenticator();
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ // no-op
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ // no-op
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public String getId() {
+ return DockerAuthenticator.ID;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java
new file mode 100644
index 0000000..3a7a324
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java
@@ -0,0 +1,184 @@
+package org.keycloak.protocol.docker;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.specimpl.ResponseBuilderImpl;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.models.AuthenticatedClientSessionModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeyManager;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.LoginProtocol;
+import org.keycloak.protocol.ProtocolMapper;
+import org.keycloak.protocol.docker.mapper.DockerAuthV2AttributeMapper;
+import org.keycloak.representations.docker.DockerResponse;
+import org.keycloak.representations.docker.DockerResponseToken;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.util.TokenUtil;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Set;
+
+public class DockerAuthV2Protocol implements LoginProtocol {
+ protected static final Logger logger = Logger.getLogger(DockerEndpoint.class);
+
+ public static final String LOGIN_PROTOCOL = "docker-v2";
+ public static final String ACCOUNT_PARAM = "account";
+ public static final String SERVICE_PARAM = "service";
+ public static final String SCOPE_PARAM = "scope";
+ public static final String ISSUER = "docker.iss"; // don't want to overlap with OIDC notes
+ public static final String ISO_8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
+
+ private KeycloakSession session;
+ private RealmModel realm;
+ private UriInfo uriInfo;
+ private HttpHeaders headers;
+ private EventBuilder event;
+
+ public DockerAuthV2Protocol() {
+ }
+
+ public DockerAuthV2Protocol(final KeycloakSession session, final RealmModel realm, final UriInfo uriInfo, final HttpHeaders headers, final EventBuilder event) {
+ this.session = session;
+ this.realm = realm;
+ this.uriInfo = uriInfo;
+ this.headers = headers;
+ this.event = event;
+ }
+
+ @Override
+ public LoginProtocol setSession(final KeycloakSession session) {
+ this.session = session;
+ return this;
+ }
+
+ @Override
+ public LoginProtocol setRealm(final RealmModel realm) {
+ this.realm = realm;
+ return this;
+ }
+
+ @Override
+ public LoginProtocol setUriInfo(final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ return this;
+ }
+
+ @Override
+ public LoginProtocol setHttpHeaders(final HttpHeaders headers) {
+ this.headers = headers;
+ return this;
+ }
+
+ @Override
+ public LoginProtocol setEventBuilder(final EventBuilder event) {
+ this.event = event;
+ return this;
+ }
+
+ @Override
+ public Response authenticated(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
+ // First, create a base response token with realm + user values populated
+ final ClientModel client = clientSession.getClient();
+ DockerResponseToken responseToken = new DockerResponseToken()
+ .id(KeycloakModelUtils.generateId())
+ .type(TokenUtil.TOKEN_TYPE_BEARER)
+ .issuer(clientSession.getNote(DockerAuthV2Protocol.ISSUER))
+ .subject(userSession.getUser().getUsername())
+ .issuedNow()
+ .audience(client.getClientId())
+ .issuedFor(client.getClientId());
+
+ // since realm access token is given in seconds
+ final int accessTokenLifespan = realm.getAccessTokenLifespan();
+ responseToken.notBefore(responseToken.getIssuedAt())
+ .expiration(responseToken.getIssuedAt() + accessTokenLifespan);
+
+ // Next, allow mappers to decorate the token to add/remove scopes as appropriate
+ final ClientSessionCode<AuthenticatedClientSessionModel> accessCode = new ClientSessionCode<>(session, realm, clientSession);
+ final Set<ProtocolMapperModel> mappings = accessCode.getRequestedProtocolMappers();
+ for (final ProtocolMapperModel mapping : mappings) {
+ final ProtocolMapper mapper = (ProtocolMapper) session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
+ if (mapper instanceof DockerAuthV2AttributeMapper) {
+ final DockerAuthV2AttributeMapper dockerAttributeMapper = (DockerAuthV2AttributeMapper) mapper;
+ if (dockerAttributeMapper.appliesTo(responseToken)) {
+ responseToken = dockerAttributeMapper.transformDockerResponseToken(responseToken, mapping, session, userSession, clientSession);
+ }
+ }
+ }
+
+ try {
+ // Finally, construct the response to the docker client with the token + metadata
+ if (event.getEvent() != null && EventType.LOGIN.equals(event.getEvent().getType())) {
+ final KeyManager.ActiveRsaKey activeKey = session.keys().getActiveRsaKey(realm);
+ final String encodedToken = new JWSBuilder()
+ .kid(new DockerKeyIdentifier(activeKey.getPublicKey()).toString())
+ .type("JWT")
+ .jsonContent(responseToken)
+ .rsa256(activeKey.getPrivateKey());
+ final String expiresInIso8601String = new SimpleDateFormat(ISO_8601_DATE_FORMAT).format(new Date(responseToken.getIssuedAt() * 1000L));
+
+ final DockerResponse responseEntity = new DockerResponse()
+ .setToken(encodedToken)
+ .setExpires_in(accessTokenLifespan)
+ .setIssued_at(expiresInIso8601String);
+ return new ResponseBuilderImpl().status(Response.Status.OK).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON).entity(responseEntity).build();
+ } else {
+ logger.errorv("Unable to handle request for event type {0}. Currently only LOGIN event types are supported by docker protocol.", event.getEvent() == null ? "null" : event.getEvent().getType());
+ throw new ErrorResponseException("invalid_request", "Event type not supported", Response.Status.BAD_REQUEST);
+ }
+ } catch (final InstantiationException e) {
+ logger.errorv("Error attempting to create Key ID for Docker JOSE header: ", e.getMessage());
+ throw new ErrorResponseException("token_error", "Unable to construct JOSE header for JWT", Response.Status.INTERNAL_SERVER_ERROR);
+ }
+
+ }
+
+ @Override
+ public Response sendError(final AuthenticationSessionModel clientSession, final LoginProtocol.Error error) {
+ return new ResponseBuilderImpl().status(Response.Status.INTERNAL_SERVER_ERROR).build();
+ }
+
+ @Override
+ public void backchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
+ errorResponse(userSession, "backchannelLogout");
+
+ }
+
+ @Override
+ public Response frontchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
+ return errorResponse(userSession, "frontchannelLogout");
+ }
+
+ @Override
+ public Response finishLogout(final UserSessionModel userSession) {
+ return errorResponse(userSession, "finishLogout");
+ }
+
+ @Override
+ public boolean requireReauthentication(final UserSessionModel userSession, final AuthenticationSessionModel clientSession) {
+ return true;
+ }
+
+ private Response errorResponse(final UserSessionModel userSession, final String methodName) {
+ logger.errorv("User {0} attempted to invoke unsupported method {1} on docker protocol.", userSession.getUser().getUsername(), methodName);
+ throw new ErrorResponseException("invalid_request", String.format("Attempted to invoke unsupported docker method %s", methodName), Response.Status.BAD_REQUEST);
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2ProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2ProtocolFactory.java
new file mode 100644
index 0000000..be4c6c0
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2ProtocolFactory.java
@@ -0,0 +1,86 @@
+package org.keycloak.protocol.docker;
+
+import org.keycloak.common.Profile;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.AbstractLoginProtocolFactory;
+import org.keycloak.protocol.LoginProtocol;
+import org.keycloak.protocol.docker.mapper.AllowAllDockerProtocolMapper;
+import org.keycloak.provider.EnvironmentDependentProviderFactory;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientTemplateRepresentation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class DockerAuthV2ProtocolFactory extends AbstractLoginProtocolFactory implements EnvironmentDependentProviderFactory {
+
+ static List<ProtocolMapperModel> builtins = new ArrayList<>();
+ static List<ProtocolMapperModel> defaultBuiltins = new ArrayList<>();
+
+ static {
+ final ProtocolMapperModel addAllRequestedScopeMapper = new ProtocolMapperModel();
+ addAllRequestedScopeMapper.setName(AllowAllDockerProtocolMapper.PROVIDER_ID);
+ addAllRequestedScopeMapper.setProtocolMapper(AllowAllDockerProtocolMapper.PROVIDER_ID);
+ addAllRequestedScopeMapper.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL);
+ addAllRequestedScopeMapper.setConsentRequired(false);
+ addAllRequestedScopeMapper.setConfig(Collections.EMPTY_MAP);
+ builtins.add(addAllRequestedScopeMapper);
+ defaultBuiltins.add(addAllRequestedScopeMapper);
+ }
+
+ @Override
+ protected void addDefaults(final ClientModel client) {
+ defaultBuiltins.forEach(builtinMapper -> client.addProtocolMapper(builtinMapper));
+ }
+
+ @Override
+ public List<ProtocolMapperModel> getBuiltinMappers() {
+ return builtins;
+ }
+
+ @Override
+ public List<ProtocolMapperModel> getDefaultBuiltinMappers() {
+ return defaultBuiltins;
+ }
+
+ @Override
+ public Object createProtocolEndpoint(final RealmModel realm, final EventBuilder event) {
+ return new DockerV2LoginProtocolService(realm, event);
+ }
+
+ @Override
+ public void setupClientDefaults(final ClientRepresentation rep, final ClientModel newClient) {
+ // no-op
+ }
+
+ @Override
+ public void setupTemplateDefaults(final ClientTemplateRepresentation clientRep, final ClientTemplateModel newClient) {
+ // no-op
+ }
+
+ @Override
+ public LoginProtocol create(final KeycloakSession session) {
+ return new DockerAuthV2Protocol().setSession(session);
+ }
+
+ @Override
+ public String getId() {
+ return DockerAuthV2Protocol.LOGIN_PROTOCOL;
+ }
+
+ @Override
+ public boolean isSupported() {
+ return Profile.isFeatureEnabled(Profile.Feature.DOCKER);
+ }
+
+ @Override
+ public int order() {
+ return -100;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java b/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java
new file mode 100644
index 0000000..8cf50e8
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java
@@ -0,0 +1,103 @@
+package org.keycloak.protocol.docker;
+
+import org.jboss.logging.Logger;
+import org.keycloak.common.Profile;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.AuthenticationFlowModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.AuthorizationEndpointBase;
+import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
+import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.Urls;
+import org.keycloak.services.util.CacheControlUtil;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.CommonClientSessionModel;
+import org.keycloak.utils.ProfileHelper;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+/**
+ * Implements a docker-client understandable format.
+ */
+public class DockerEndpoint extends AuthorizationEndpointBase {
+ protected static final Logger logger = Logger.getLogger(DockerEndpoint.class);
+
+ private final EventType login;
+ private String account;
+ private String service;
+ private String scope;
+ private ClientModel client;
+ private AuthenticationSessionModel authenticationSession;
+
+ public DockerEndpoint(final RealmModel realm, final EventBuilder event, final EventType login) {
+ super(realm, event);
+ this.login = login;
+ }
+
+ @GET
+ public Response build() {
+ ProfileHelper.requireFeature(Profile.Feature.DOCKER);
+
+ final MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
+
+ account = params.getFirst(DockerAuthV2Protocol.ACCOUNT_PARAM);
+ if (account == null) {
+ logger.debug("Account parameter not provided by docker auth. This is techincally required, but not actually used since " +
+ "username is provided by Basic auth header.");
+ }
+ service = params.getFirst(DockerAuthV2Protocol.SERVICE_PARAM);
+ if (service == null) {
+ throw new ErrorResponseException("invalid_request", "service parameter must be provided", Response.Status.BAD_REQUEST);
+ }
+ client = realm.getClientByClientId(service);
+ if (client == null) {
+ logger.errorv("Failed to lookup client given by service={0} parameter for realm: {1}.", service, realm.getName());
+ throw new ErrorResponseException("invalid_client", "Client specified by 'service' parameter does not exist", Response.Status.BAD_REQUEST);
+ }
+ scope = params.getFirst(DockerAuthV2Protocol.SCOPE_PARAM);
+
+ checkSsl();
+ checkRealm();
+
+ final AuthorizationEndpointRequest authRequest = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params);
+ AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, authRequest.getState());
+ if (checks.response != null) {
+ return checks.response;
+ }
+
+ authenticationSession = checks.authSession;
+ updateAuthenticationSession();
+
+ // So back button doesn't work
+ CacheControlUtil.noBackButtonCacheControlHeader();
+
+ return handleBrowserAuthenticationRequest(authenticationSession, new DockerAuthV2Protocol(session, realm, uriInfo, headers, event.event(login)), false, false);
+ }
+
+ private void updateAuthenticationSession() {
+ authenticationSession.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL);
+ authenticationSession.setAction(CommonClientSessionModel.Action.AUTHENTICATE.name());
+
+ // Docker specific stuff
+ authenticationSession.setClientNote(DockerAuthV2Protocol.ACCOUNT_PARAM, account);
+ authenticationSession.setClientNote(DockerAuthV2Protocol.SERVICE_PARAM, service);
+ authenticationSession.setClientNote(DockerAuthV2Protocol.SCOPE_PARAM, scope);
+ authenticationSession.setClientNote(DockerAuthV2Protocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+
+ }
+
+ @Override
+ protected AuthenticationFlowModel getAuthenticationFlow() {
+ return realm.getDockerAuthenticationFlow();
+ }
+
+ @Override
+ protected boolean isNewRequest(final AuthenticationSessionModel authSession, final ClientModel clientFromRequest, final String requestState) {
+ return true;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerKeyIdentifier.java b/services/src/main/java/org/keycloak/protocol/docker/DockerKeyIdentifier.java
new file mode 100644
index 0000000..384f218
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerKeyIdentifier.java
@@ -0,0 +1,127 @@
+package org.keycloak.protocol.docker;
+
+import org.keycloak.models.utils.Base32;
+
+import java.security.Key;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collector;
+import java.util.stream.Stream;
+
+/**
+ * The “kid” field has to be in a libtrust fingerprint compatible format. Such a format can be generated by following steps:
+ * 1) Take the DER encoded public key which the JWT token was signed against.
+ * 2) Create a SHA256 hash out of it and truncate to 240bits.
+ * 3) Split the result into 12 base32 encoded groups with : as delimiter.
+ *
+ * Ex: "kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6"
+ *
+ * @see https://docs.docker.com/registry/spec/auth/jwt/
+ * @see https://github.com/docker/libtrust/blob/master/key.go#L24
+ */
+public class DockerKeyIdentifier {
+
+ private final String identifier;
+
+ public DockerKeyIdentifier(final Key key) throws InstantiationException {
+ try {
+ final MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
+ final byte[] hashed = sha256.digest(key.getEncoded());
+ final byte[] hashedTruncated = truncateToBitLength(240, hashed);
+ final String base32Id = Base32.encode(hashedTruncated);
+ identifier = byteStream(base32Id.getBytes()).collect(new DelimitingCollector());
+ } catch (final NoSuchAlgorithmException e) {
+ throw new InstantiationException("Could not instantiate docker key identifier, no SHA-256 algorithm available.");
+ }
+ }
+
+ // ugh.
+ private Stream<Byte> byteStream(final byte[] bytes) {
+ final Collection<Byte> colectionedBytes = new ArrayList<>();
+ for (final byte aByte : bytes) {
+ colectionedBytes.add(aByte);
+ }
+
+ return colectionedBytes.stream();
+ }
+
+ private byte[] truncateToBitLength(final int bitLength, final byte[] arrayToTruncate) {
+ if (bitLength % 8 != 0) {
+ throw new IllegalArgumentException("Bit length for truncation of byte array given as a number not divisible by 8");
+ }
+
+ final int numberOfBytes = bitLength / 8;
+ return Arrays.copyOfRange(arrayToTruncate, 0, numberOfBytes);
+ }
+
+ @Override
+ public String toString() {
+ return identifier;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DockerKeyIdentifier)) return false;
+
+ final DockerKeyIdentifier that = (DockerKeyIdentifier) o;
+
+ return identifier != null ? identifier.equals(that.identifier) : that.identifier == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ return identifier != null ? identifier.hashCode() : 0;
+ }
+
+ // Could probably be generalized with size and delimiter arguments, but leaving it here for now until someone else needs it.
+ public static class DelimitingCollector implements Collector<Byte, StringBuilder, String> {
+
+ @Override
+ public Supplier<StringBuilder> supplier() {
+ return () -> new StringBuilder();
+ }
+
+ @Override
+ public BiConsumer<StringBuilder, Byte> accumulator() {
+ return ((stringBuilder, aByte) -> {
+ if (needsDelimiter(4, ":", stringBuilder)) {
+ stringBuilder.append(":");
+ }
+
+ stringBuilder.append(new String(new byte[]{aByte}));
+ });
+ }
+
+ private static boolean needsDelimiter(final int maxLength, final String delimiter, final StringBuilder builder) {
+ final int lastDelimiter = builder.lastIndexOf(delimiter);
+ final int charsSinceLastDelimiter = builder.length() - lastDelimiter;
+ return charsSinceLastDelimiter > maxLength;
+ }
+
+ @Override
+ public BinaryOperator<StringBuilder> combiner() {
+ return ((left, right) -> new StringBuilder(left.toString()).append(right.toString()));
+ }
+
+ @Override
+ public Function<StringBuilder, String> finisher() {
+ return StringBuilder::toString;
+ }
+
+ @Override
+ public Set<Characteristics> characteristics() {
+ return Collections.emptySet();
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerV2LoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/docker/DockerV2LoginProtocolService.java
new file mode 100644
index 0000000..a0dad58
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerV2LoginProtocolService.java
@@ -0,0 +1,70 @@
+package org.keycloak.protocol.docker;
+
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.common.Profile;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.utils.ProfileHelper;
+
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+public class DockerV2LoginProtocolService {
+
+ private final RealmModel realm;
+ private final TokenManager tokenManager;
+ private final EventBuilder event;
+
+ @Context
+ private UriInfo uriInfo;
+
+ @Context
+ private KeycloakSession session;
+
+ @Context
+ private HttpHeaders headers;
+
+ public DockerV2LoginProtocolService(final RealmModel realm, final EventBuilder event) {
+ this.realm = realm;
+ this.tokenManager = new TokenManager();
+ this.event = event;
+ }
+
+ public static UriBuilder authProtocolBaseUrl(final UriInfo uriInfo) {
+ final UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
+ return authProtocolBaseUrl(baseUriBuilder);
+ }
+
+ public static UriBuilder authProtocolBaseUrl(final UriBuilder baseUriBuilder) {
+ return baseUriBuilder.path(RealmsResource.class).path("{realm}/protocol/" + DockerAuthV2Protocol.LOGIN_PROTOCOL);
+ }
+
+ public static UriBuilder authUrl(final UriInfo uriInfo) {
+ final UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
+ return authUrl(baseUriBuilder);
+ }
+
+ public static UriBuilder authUrl(final UriBuilder baseUriBuilder) {
+ final UriBuilder uriBuilder = authProtocolBaseUrl(baseUriBuilder);
+ return uriBuilder.path(DockerV2LoginProtocolService.class, "auth");
+ }
+
+ /**
+ * Authorization endpoint
+ */
+ @Path("auth")
+ public Object auth() {
+ ProfileHelper.requireFeature(Profile.Feature.DOCKER);
+
+ final DockerEndpoint endpoint = new DockerEndpoint(realm, event, EventType.LOGIN);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerCertFileUtils.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerCertFileUtils.java
new file mode 100644
index 0000000..6687089
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerCertFileUtils.java
@@ -0,0 +1,37 @@
+package org.keycloak.protocol.docker.installation.compose;
+
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.util.Base64;
+
+public final class DockerCertFileUtils {
+ public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
+ public static final String END_CERT = "-----END CERTIFICATE-----";
+ public static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----";
+ public static final String END_PRIVATE_KEY = "-----END PRIVATE KEY-----";
+ public final static String LINE_SEPERATOR = System.getProperty("line.separator");
+
+ private DockerCertFileUtils() {
+ }
+
+ public static String formatCrtFileContents(final Certificate certificate) throws CertificateEncodingException {
+ return encodeAndPrettify(BEGIN_CERT, certificate.getEncoded(), END_CERT);
+ }
+
+ public static String formatPrivateKeyContents(final PrivateKey privateKey) {
+ return encodeAndPrettify(BEGIN_PRIVATE_KEY, privateKey.getEncoded(), END_PRIVATE_KEY);
+ }
+
+ public static String formatPublicKeyContents(final PublicKey publicKey) {
+ return encodeAndPrettify(BEGIN_CERT, publicKey.getEncoded(), END_CERT);
+ }
+
+ private static String encodeAndPrettify(final String header, final byte[] rawCrtText, final String footer) {
+ final Base64.Encoder encoder = Base64.getMimeEncoder(64, LINE_SEPERATOR.getBytes());
+ final String encodedCertText = new String(encoder.encode(rawCrtText));
+ final String prettified_cert = header + LINE_SEPERATOR + encodedCertText + LINE_SEPERATOR + footer;
+ return prettified_cert;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java
new file mode 100644
index 0000000..9d607f4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java
@@ -0,0 +1,62 @@
+package org.keycloak.protocol.docker.installation.compose;
+
+import org.keycloak.common.util.CertificateUtils;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.util.AbstractMap;
+import java.util.Map;
+
+public class DockerComposeCertsDirectory {
+
+ private final String directoryName;
+ private final Map.Entry<String, byte[]> localhostCertFile;
+ private final Map.Entry<String, byte[]> localhostKeyFile;
+ private final Map.Entry<String, byte[]> idpTrustChainFile;
+
+ public DockerComposeCertsDirectory(final String directoryName, final Certificate realmCert, final String registryCertFilename, final String registryKeyFilename, final String idpCertTrustChainFilename, final String realmName) {
+ this.directoryName = directoryName;
+
+ final KeyPairGenerator keyGen;
+ try {
+ keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048, new SecureRandom());
+
+ final KeyPair keypair = keyGen.generateKeyPair();
+ final PrivateKey privateKey = keypair.getPrivate();
+ final Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keypair, realmName);
+
+ localhostCertFile = new AbstractMap.SimpleImmutableEntry<>(registryCertFilename, DockerCertFileUtils.formatCrtFileContents(certificate).getBytes());
+ localhostKeyFile = new AbstractMap.SimpleImmutableEntry<>(registryKeyFilename, DockerCertFileUtils.formatPrivateKeyContents(privateKey).getBytes());
+ idpTrustChainFile = new AbstractMap.SimpleEntry<>(idpCertTrustChainFilename, DockerCertFileUtils.formatCrtFileContents(realmCert).getBytes());
+
+ } catch (final NoSuchAlgorithmException e) {
+ // TODO throw error here descritively
+ throw new RuntimeException(e);
+ } catch (final CertificateEncodingException e) {
+ // TODO throw error here descritively
+ throw new RuntimeException(e);
+ }
+ }
+
+ public String getDirectoryName() {
+ return directoryName;
+ }
+
+ public Map.Entry<String, byte[]> getLocalhostCertFile() {
+ return localhostCertFile;
+ }
+
+ public Map.Entry<String, byte[]> getLocalhostKeyFile() {
+ return localhostKeyFile;
+ }
+
+ public Map.Entry<String, byte[]> getIdpTrustChainFile() {
+ return idpTrustChainFile;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeYamlFile.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeYamlFile.java
new file mode 100644
index 0000000..1630ffa
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeYamlFile.java
@@ -0,0 +1,70 @@
+package org.keycloak.protocol.docker.installation.compose;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.net.URL;
+
+/**
+ * Representation of the docker-compose.yaml file
+ */
+public class DockerComposeYamlFile {
+
+ private final String registryDataDirName;
+ private final String localCertDirName;
+ private final String containerCertPath;
+ private final String localhostCrtFileName;
+ private final String localhostKeyFileName;
+ private final String authServerTrustChainFileName;
+ private final URL authServerUrl;
+ private final String realmName;
+ private final String serviceId;
+
+ /**
+ * @param registryDataDirName Directory name to be used for both the container's storage directory, as well as the local data directory name
+ * @param localCertDirName Name of the (relative) local directory that holds the certs
+ * @param containerCertPath Path at which the local certs directory should be mounted on the container
+ * @param localhostCrtFileName SSL Cert file name for the registry
+ * @param localhostKeyFileName SSL Key file name for the registry
+ * @param authServerTrustChainFileName IDP trust chain, used for auth token validation
+ * @param authServerUrl Root URL for Keycloak, commonly something like http://localhost:8080/auth for dev environments
+ * @param realmName Name of the realm for which the docker client is configured
+ * @param serviceId Docker's Service ID, corresponds to Keycloak's client ID
+ */
+ public DockerComposeYamlFile(final String registryDataDirName, final String localCertDirName, final String containerCertPath, final String localhostCrtFileName, final String localhostKeyFileName, final String authServerTrustChainFileName, final URL authServerUrl, final String realmName, final String serviceId) {
+ this.registryDataDirName = registryDataDirName;
+ this.localCertDirName = localCertDirName;
+ this.containerCertPath = containerCertPath;
+ this.localhostCrtFileName = localhostCrtFileName;
+ this.localhostKeyFileName = localhostKeyFileName;
+ this.authServerTrustChainFileName = authServerTrustChainFileName;
+ this.authServerUrl = authServerUrl;
+ this.realmName = realmName;
+ this.serviceId = serviceId;
+ }
+
+ public byte[] generateDockerComposeFileBytes() {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ final PrintWriter writer = new PrintWriter(output);
+
+ writer.print("registry:\n");
+ writer.print(" image: registry:2\n");
+ writer.print(" ports:\n");
+ writer.print(" - 127.0.0.1:5000:5000\n");
+ writer.print(" environment:\n");
+ writer.print(" REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /" + registryDataDirName + "\n");
+ writer.print(" REGISTRY_HTTP_TLS_CERTIFICATE: " + containerCertPath + "/" + localhostCrtFileName + "\n");
+ writer.print(" REGISTRY_HTTP_TLS_KEY: " + containerCertPath + "/" + localhostKeyFileName + "\n");
+ writer.print(" REGISTRY_AUTH_TOKEN_REALM: " + authServerUrl + "/realms/" + realmName + "/protocol/docker-v2/auth\n");
+ writer.print(" REGISTRY_AUTH_TOKEN_SERVICE: " + serviceId + "\n");
+ writer.print(" REGISTRY_AUTH_TOKEN_ISSUER: " + authServerUrl + "/realms/" + realmName + "\n");
+ writer.print(" REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: " + containerCertPath + "/" + authServerTrustChainFileName + "\n");
+ writer.print(" volumes:\n");
+ writer.print(" - ./" + registryDataDirName + ":/" + registryDataDirName + ":z\n");
+ writer.print(" - ./" + localCertDirName + ":" + containerCertPath + ":z");
+
+ writer.flush();
+ writer.close();
+
+ return output.toByteArray();
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeZipContent.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeZipContent.java
new file mode 100644
index 0000000..a4d0ee2
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeZipContent.java
@@ -0,0 +1,35 @@
+package org.keycloak.protocol.docker.installation.compose;
+
+import java.net.URL;
+import java.security.cert.Certificate;
+
+public class DockerComposeZipContent {
+
+ private final DockerComposeYamlFile yamlFile;
+ private final String dataDirectoryName;
+ private final DockerComposeCertsDirectory certsDirectory;
+
+ public DockerComposeZipContent(final Certificate realmCert, final URL realmBaseUrl, final String realmName, final String clientId) {
+ final String dataDirectoryName = "data";
+ final String certsDirectoryName = "certs";
+ final String registryCertFilename = "localhost.crt";
+ final String registryKeyFilename = "localhost.key";
+ final String idpCertTrustChainFilename = "localhost_trust_chain.pem";
+
+ this.yamlFile = new DockerComposeYamlFile(dataDirectoryName, certsDirectoryName, "/opt/" + certsDirectoryName, registryCertFilename, registryKeyFilename, idpCertTrustChainFilename, realmBaseUrl, realmName, clientId);
+ this.dataDirectoryName = dataDirectoryName;
+ this.certsDirectory = new DockerComposeCertsDirectory(certsDirectoryName, realmCert, registryCertFilename, registryKeyFilename, idpCertTrustChainFilename, realmName);
+ }
+
+ public DockerComposeYamlFile getYamlFile() {
+ return yamlFile;
+ }
+
+ public String getDataDirectoryName() {
+ return dataDirectoryName;
+ }
+
+ public DockerComposeCertsDirectory getCertsDirectory() {
+ return certsDirectory;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java
new file mode 100644
index 0000000..72ade31
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java
@@ -0,0 +1,148 @@
+package org.keycloak.protocol.docker.installation;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.ClientInstallationProvider;
+import org.keycloak.protocol.docker.DockerAuthV2Protocol;
+import org.keycloak.protocol.docker.installation.compose.DockerComposeZipContent;
+
+import javax.ws.rs.core.Response;
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URL;
+import java.security.cert.Certificate;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public class DockerComposeYamlInstallationProvider implements ClientInstallationProvider {
+ private static Logger log = Logger.getLogger(DockerComposeYamlInstallationProvider.class);
+
+ public static final String ROOT_DIR = "keycloak-docker-compose-yaml/";
+
+ @Override
+ public ClientInstallationProvider create(final KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(final Config.Scope config) {
+ // no-op
+ }
+
+ @Override
+ public void postInit(final KeycloakSessionFactory factory) {
+ // no-op
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public String getId() {
+ return "docker-v2-compose-yaml";
+ }
+
+ @Override
+ public Response generateInstallation(final KeycloakSession session, final RealmModel realm, final ClientModel client, final URI serverBaseUri) {
+ final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+ final ZipOutputStream zipOutput = new ZipOutputStream(byteStream);
+
+ try {
+ return generateInstallation(zipOutput, byteStream, session.keys().getActiveRsaKey(realm).getCertificate(), session.getContext().getAuthServerUrl().toURL(), realm.getName(), client.getClientId());
+ } catch (final IOException e) {
+ try {
+ zipOutput.close();
+ } catch (final IOException ex) {
+ // do nothing, already in an exception
+ }
+ try {
+ byteStream.close();
+ } catch (final IOException ex) {
+ // do nothing, already in an exception
+ }
+ throw new RuntimeException("Error occurred during attempt to generate docker-compose yaml installation files", e);
+ }
+ }
+
+ public Response generateInstallation(final ZipOutputStream zipOutput, final ByteArrayOutputStream byteStream, final Certificate realmCert, final URL realmBaseURl,
+ final String realmName, final String clientName) throws IOException {
+ final DockerComposeZipContent zipContent = new DockerComposeZipContent(realmCert, realmBaseURl, realmName, clientName);
+
+ zipOutput.putNextEntry(new ZipEntry(ROOT_DIR));
+
+ // Write docker compose file
+ zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + "docker-compose.yaml"));
+ zipOutput.write(zipContent.getYamlFile().generateDockerComposeFileBytes());
+ zipOutput.closeEntry();
+
+ // Write data directory
+ zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + zipContent.getDataDirectoryName() + "/"));
+ zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + zipContent.getDataDirectoryName() + "/.gitignore"));
+ zipOutput.write("*".getBytes());
+ zipOutput.closeEntry();
+
+ // Write certificates
+ final String certsDirectory = ROOT_DIR + zipContent.getCertsDirectory().getDirectoryName() + "/";
+ zipOutput.putNextEntry(new ZipEntry(certsDirectory));
+ zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getLocalhostCertFile().getKey()));
+ zipOutput.write(zipContent.getCertsDirectory().getLocalhostCertFile().getValue());
+ zipOutput.closeEntry();
+ zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getLocalhostKeyFile().getKey()));
+ zipOutput.write(zipContent.getCertsDirectory().getLocalhostKeyFile().getValue());
+ zipOutput.closeEntry();
+ zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getIdpTrustChainFile().getKey()));
+ zipOutput.write(zipContent.getCertsDirectory().getIdpTrustChainFile().getValue());
+ zipOutput.closeEntry();
+
+ // Write README to .zip
+ zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + "README.md"));
+ final String readmeContent = new BufferedReader(new InputStreamReader(DockerComposeYamlInstallationProvider.class.getResourceAsStream("/DockerComposeYamlReadme.md"))).lines().collect(Collectors.joining("\n"));
+ zipOutput.write(readmeContent.getBytes());
+ zipOutput.closeEntry();
+
+ zipOutput.close();
+ byteStream.close();
+
+ return Response.ok(byteStream.toByteArray(), getMediaType()).build();
+ }
+
+ @Override
+ public String getProtocol() {
+ return DockerAuthV2Protocol.LOGIN_PROTOCOL;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Docker Compose YAML";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Produces a zip file that can be used to stand up a development registry on localhost";
+ }
+
+ @Override
+ public String getFilename() {
+ return "keycloak-docker-compose-yaml.zip";
+ }
+
+ @Override
+ public String getMediaType() {
+ return "application/zip";
+ }
+
+ @Override
+ public boolean isDownloadOnly() {
+ return true;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/DockerRegistryConfigFileInstallationProvider.java b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerRegistryConfigFileInstallationProvider.java
new file mode 100644
index 0000000..ba4440a
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerRegistryConfigFileInstallationProvider.java
@@ -0,0 +1,81 @@
+package org.keycloak.protocol.docker.installation;
+
+import org.keycloak.Config;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.ClientInstallationProvider;
+import org.keycloak.protocol.docker.DockerAuthV2Protocol;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+
+public class DockerRegistryConfigFileInstallationProvider implements ClientInstallationProvider {
+
+ @Override
+ public ClientInstallationProvider create(final KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(final Config.Scope config) {
+ // no-op
+ }
+
+ @Override
+ public void postInit(final KeycloakSessionFactory factory) {
+ // no-op
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public String getId() {
+ return "docker-v2-registry-config-file";
+ }
+
+ @Override
+ public Response generateInstallation(final KeycloakSession session, final RealmModel realm, final ClientModel client, final URI serverBaseUri) {
+ final StringBuilder responseString = new StringBuilder("auth:\n")
+ .append(" token:\n")
+ .append(" realm: ").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append("/protocol/").append(DockerAuthV2Protocol.LOGIN_PROTOCOL).append("/auth\n")
+ .append(" service: ").append(client.getClientId()).append("\n")
+ .append(" issuer: ").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append("\n");
+ return Response.ok(responseString.toString(), MediaType.TEXT_PLAIN_TYPE).build();
+ }
+
+ @Override
+ public String getProtocol() {
+ return DockerAuthV2Protocol.LOGIN_PROTOCOL;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Registry Config File";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Provides a registry configuration file snippet for use with this client";
+ }
+
+ @Override
+ public String getFilename() {
+ return "config.yml";
+ }
+
+ @Override
+ public String getMediaType() {
+ return MediaType.TEXT_PLAIN;
+ }
+
+ @Override
+ public boolean isDownloadOnly() {
+ return false;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/DockerVariableOverrideInstallationProvider.java b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerVariableOverrideInstallationProvider.java
new file mode 100644
index 0000000..055d9ac
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerVariableOverrideInstallationProvider.java
@@ -0,0 +1,81 @@
+package org.keycloak.protocol.docker.installation;
+
+import org.keycloak.Config;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.ClientInstallationProvider;
+import org.keycloak.protocol.docker.DockerAuthV2Protocol;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+
+public class DockerVariableOverrideInstallationProvider implements ClientInstallationProvider {
+
+ @Override
+ public ClientInstallationProvider create(final KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(final Config.Scope config) {
+ // no-op
+ }
+
+ @Override
+ public void postInit(final KeycloakSessionFactory factory) {
+ // no-op
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public String getId() {
+ return "docker-v2-variable-override";
+ }
+
+ // TODO "auth" is not guaranteed to be the endpoint, fix it
+ @Override
+ public Response generateInstallation(final KeycloakSession session, final RealmModel realm, final ClientModel client, final URI serverBaseUri) {
+ final StringBuilder builder = new StringBuilder()
+ .append("-e REGISTRY_AUTH_TOKEN_REALM=").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append("/protocol/").append(DockerAuthV2Protocol.LOGIN_PROTOCOL).append("/auth \\\n")
+ .append("-e REGISTRY_AUTH_TOKEN_SERVICE=").append(client.getClientId()).append(" \\\n")
+ .append("-e REGISTRY_AUTH_TOKEN_ISSUER=").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append(" \\\n");
+ return Response.ok(builder.toString(), MediaType.TEXT_PLAIN_TYPE).build();
+ }
+
+ @Override
+ public String getProtocol() {
+ return DockerAuthV2Protocol.LOGIN_PROTOCOL;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Variable Override";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Configures environment variable overrides, typically used with a docker-compose.yaml configuration for a docker registry";
+ }
+
+ @Override
+ public String getFilename() {
+ return "docker-env.txt";
+ }
+
+ @Override
+ public String getMediaType() {
+ return MediaType.TEXT_PLAIN;
+ }
+
+ @Override
+ public boolean isDownloadOnly() {
+ return false;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/mapper/AllowAllDockerProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/docker/mapper/AllowAllDockerProtocolMapper.java
new file mode 100644
index 0000000..398eeb6
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/mapper/AllowAllDockerProtocolMapper.java
@@ -0,0 +1,52 @@
+package org.keycloak.protocol.docker.mapper;
+
+import org.keycloak.models.AuthenticatedClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.docker.DockerAuthV2Protocol;
+import org.keycloak.representations.docker.DockerAccess;
+import org.keycloak.representations.docker.DockerResponseToken;
+
+/**
+ * Populates token with requested scope. If more scopes are present than what has been requested, they will be removed.
+ */
+public class AllowAllDockerProtocolMapper extends DockerAuthV2ProtocolMapper implements DockerAuthV2AttributeMapper {
+
+ public static final String PROVIDER_ID = "docker-v2-allow-all-mapper";
+
+ @Override
+ public String getDisplayType() {
+ return "Allow All";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Allows all grants, returning the full set of requested access attributes as permitted attributes.";
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public boolean appliesTo(final DockerResponseToken responseToken) {
+ return true;
+ }
+
+ @Override
+ public DockerResponseToken transformDockerResponseToken(final DockerResponseToken responseToken, final ProtocolMapperModel mappingModel,
+ final KeycloakSession session, final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
+
+ responseToken.getAccessItems().clear();
+
+ final String requestedScope = clientSession.getNote(DockerAuthV2Protocol.SCOPE_PARAM);
+ if (requestedScope != null) {
+ final DockerAccess allRequestedAccess = new DockerAccess(requestedScope);
+ responseToken.getAccessItems().add(allRequestedAccess);
+ }
+
+ return responseToken;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2AttributeMapper.java b/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2AttributeMapper.java
new file mode 100644
index 0000000..320686b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2AttributeMapper.java
@@ -0,0 +1,15 @@
+package org.keycloak.protocol.docker.mapper;
+
+import org.keycloak.models.AuthenticatedClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.representations.docker.DockerResponseToken;
+
+public interface DockerAuthV2AttributeMapper {
+
+ boolean appliesTo(DockerResponseToken responseToken);
+
+ DockerResponseToken transformDockerResponseToken(DockerResponseToken responseToken, ProtocolMapperModel mappingModel,
+ KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2ProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2ProtocolMapper.java
new file mode 100644
index 0000000..69ccd00
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2ProtocolMapper.java
@@ -0,0 +1,51 @@
+package org.keycloak.protocol.docker.mapper;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.protocol.ProtocolMapper;
+import org.keycloak.protocol.docker.DockerAuthV2Protocol;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.Collections;
+import java.util.List;
+
+public abstract class DockerAuthV2ProtocolMapper implements ProtocolMapper {
+
+ public static final String DOCKER_AUTH_V2_CATEGORY = "Docker Auth Mapper";
+
+ @Override
+ public String getProtocol() {
+ return DockerAuthV2Protocol.LOGIN_PROTOCOL;
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return DOCKER_AUTH_V2_CATEGORY;
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public final ProtocolMapper create(final KeycloakSession session) {
+ throw new UnsupportedOperationException("The create method is not supported by this mapper");
+ }
+
+ @Override
+ public void init(final Config.Scope config) {
+ // no-op
+ }
+
+ @Override
+ public void postInit(final KeycloakSessionFactory factory) {
+ // no-op
+ }
+}
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 3a7e4c0..38dbc8f 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
@@ -49,7 +49,6 @@ import org.keycloak.util.TokenUtil;
import javax.ws.rs.GET;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
-
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -169,21 +168,6 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
return this;
}
-
- private void checkSsl() {
- if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
- event.error(Errors.SSL_REQUIRED);
- throw new ErrorPageException(session, Messages.HTTPS_REQUIRED);
- }
- }
-
- private void checkRealm() {
- if (!realm.isEnabled()) {
- event.error(Errors.REALM_DISABLED);
- throw new ErrorPageException(session, Messages.REALM_NOT_ENABLED);
- }
- }
-
private void checkClient(String clientId) {
if (clientId == null) {
event.error(Errors.INVALID_REQUEST);
@@ -288,24 +272,24 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
private Response checkPKCEParams() {
String codeChallenge = request.getCodeChallenge();
String codeChallengeMethod = request.getCodeChallengeMethod();
-
+
// PKCE not adopted to OAuth2 Implicit Grant and OIDC Implicit Flow,
// adopted to OAuth2 Authorization Code Grant and OIDC Authorization Code Flow, Hybrid Flow
// Namely, flows using authorization code.
if (parsedResponseType.isImplicitFlow()) return null;
-
+
if (codeChallenge == null && codeChallengeMethod != null) {
logger.info("PKCE supporting Client without code challenge");
event.error(Errors.INVALID_REQUEST);
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Missing parameter: code_challenge");
}
-
+
// based on code_challenge value decide whether this client(RP) supports PKCE
if (codeChallenge == null) {
logger.debug("PKCE non-supporting Client");
return null;
}
-
+
if (codeChallengeMethod != null) {
// https://tools.ietf.org/html/rfc7636#section-4.2
// plain or S256
@@ -319,13 +303,13 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
// default code_challenge_method is plane
codeChallengeMethod = OIDCLoginProtocol.PKCE_METHOD_PLAIN;
}
-
+
if (!isValidPkceCodeChallenge(codeChallenge)) {
logger.infof("PKCE supporting Client with invalid code challenge specified in PKCE, codeChallenge = %s", codeChallenge);
event.error(Errors.INVALID_REQUEST);
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: code_challenge");
}
-
+
return null;
}
@@ -449,7 +433,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
String flowId = flow.getId();
AuthenticationProcessor processor = createProcessor(authenticationSession, flowId, LoginActionsService.RESET_CREDENTIALS_PATH);
- authenticationSession.setClientNote(APP_INITIATED_FLOW, LoginActionsService.REGISTRATION_PATH);
+ authenticationSession.setClientNote(APP_INITIATED_FLOW, LoginActionsService.RESET_CREDENTIALS_PATH);
return processor.authenticate();
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java
index b4f8622..a478169 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java
@@ -17,6 +17,7 @@
package org.keycloak.protocol.oidc.endpoints;
+import org.keycloak.common.Version;
import org.keycloak.common.util.UriUtils;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
@@ -28,8 +29,10 @@ import org.keycloak.utils.MediaType;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
@@ -42,25 +45,25 @@ import java.util.Set;
public class LoginStatusIframeEndpoint {
@Context
- private UriInfo uriInfo;
-
- @Context
private KeycloakSession session;
- private RealmModel realm;
-
- public LoginStatusIframeEndpoint(RealmModel realm) {
- this.realm = realm;
- }
-
@GET
@Produces(MediaType.TEXT_HTML_UTF_8)
- public Response getLoginStatusIframe(@QueryParam("client_id") String client_id,
- @QueryParam("origin") String origin) {
+ public Response getLoginStatusIframe(@QueryParam("version") String version) {
+ CacheControl cacheControl;
+ if (version != null) {
+ if (!version.equals(Version.RESOURCES_VERSION)) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+ cacheControl = CacheControlUtil.getDefaultCacheControl();
+ } else {
+ cacheControl = CacheControlUtil.noCache();
+ }
+
InputStream resource = getClass().getClassLoader().getResourceAsStream("login-status-iframe.html");
if (resource != null) {
P3PHelper.addP3PHeader(session);
- return Response.ok(resource).cacheControl(CacheControlUtil.getDefaultCacheControl()).build();
+ return Response.ok(resource).cacheControl(cacheControl).build();
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}
@@ -70,6 +73,7 @@ public class LoginStatusIframeEndpoint {
@Path("init")
public Response preCheck(@QueryParam("client_id") String clientId, @QueryParam("origin") String origin) {
try {
+ UriInfo uriInfo = session.getContext().getUri();
RealmModel realm = session.getContext().getRealm();
ClientModel client = session.realms().getClientByClientId(clientId, realm);
if (client != null) {
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 4870415..fef17c6 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -36,6 +36,7 @@ import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@@ -52,6 +53,7 @@ import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors;
+import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
@@ -65,6 +67,7 @@ import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.Map;
+import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.security.MessageDigest;
@@ -80,7 +83,7 @@ public class TokenEndpoint {
private Map<String, String> clientAuthAttributes;
private enum Action {
- AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS
+ AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS, TOKEN_EXCHANGE
}
// https://tools.ietf.org/html/rfc7636#section-4.2
@@ -134,6 +137,8 @@ public class TokenEndpoint {
return buildResourceOwnerPasswordCredentialsGrant();
case CLIENT_CREDENTIALS:
return buildClientCredentialsGrant();
+ case TOKEN_EXCHANGE:
+ return buildTokenExchange();
}
throw new RuntimeException("Unknown action " + action);
@@ -197,6 +202,10 @@ public class TokenEndpoint {
} else if (grantType.equals(OAuth2Constants.CLIENT_CREDENTIALS)) {
event.event(EventType.CLIENT_LOGIN);
action = Action.CLIENT_CREDENTIALS;
+ } else if (grantType.equals(OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)) {
+ event.event(EventType.TOKEN_EXCHANGE);
+ action = Action.TOKEN_EXCHANGE;
+
} else {
throw new ErrorResponseException(Errors.INVALID_REQUEST, "Invalid " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST);
}
@@ -258,6 +267,7 @@ public class TokenEndpoint {
}
event.user(userSession.getUser());
+
event.session(userSession.getId());
String redirectUri = clientSession.getNote(OIDCLoginProtocol.REDIRECT_URI_PARAM);
@@ -376,6 +386,7 @@ public class TokenEndpoint {
}
} catch (OAuthErrorException e) {
+ logger.trace(e.getMessage(), e);
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.BAD_REQUEST);
}
@@ -398,9 +409,16 @@ public class TokenEndpoint {
logger.debugf("Adapter Session '%s' saved in ClientSession for client '%s'. Host is '%s'", adapterSessionId, client.getClientId(), adapterSessionHost);
event.detail(AdapterConstants.CLIENT_SESSION_STATE, adapterSessionId);
- clientSession.setNote(AdapterConstants.CLIENT_SESSION_STATE, adapterSessionId);
+ String oldClientSessionState = clientSession.getNote(AdapterConstants.CLIENT_SESSION_STATE);
+ if (!adapterSessionId.equals(oldClientSessionState)) {
+ clientSession.setNote(AdapterConstants.CLIENT_SESSION_STATE, adapterSessionId);
+ }
+
event.detail(AdapterConstants.CLIENT_SESSION_HOST, adapterSessionHost);
- clientSession.setNote(AdapterConstants.CLIENT_SESSION_HOST, adapterSessionHost);
+ String oldClientSessionHost = clientSession.getNote(AdapterConstants.CLIENT_SESSION_HOST);
+ if (!Objects.equals(adapterSessionHost, oldClientSessionHost)) {
+ clientSession.setNote(AdapterConstants.CLIENT_SESSION_HOST, adapterSessionHost);
+ }
}
}
@@ -543,6 +561,100 @@ public class TokenEndpoint {
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
+ public Response buildTokenExchange() {
+ event.detail(Details.AUTH_METHOD, "oauth_credentials");
+
+ String scope = formParams.getFirst(OAuth2Constants.SCOPE);
+ String subjectToken = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN);
+ String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
+ AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, uriInfo, clientConnection, true, true, false, subjectToken, headers);
+ if (authResult == null) {
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.BAD_REQUEST);
+ }
+
+ String audience = formParams.getFirst(OAuth2Constants.AUDIENCE);
+ if (audience == null) {
+ event.error(Errors.INVALID_REQUEST);
+ throw new ErrorResponseException("invalid_audience", "No audience specified", Response.Status.BAD_REQUEST);
+
+ }
+ ClientModel targetClient = null;
+ if (audience != null) {
+ targetClient = realm.getClientByClientId(audience);
+ }
+ if (targetClient == null) {
+ event.error(Errors.INVALID_CLIENT);
+ throw new ErrorResponseException("invalid_client", "Client authentication ended, but client is null", Response.Status.BAD_REQUEST);
+ }
+
+ if (targetClient.isConsentRequired()) {
+ event.error(Errors.CONSENT_DENIED);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
+ }
+
+ boolean exchangeFromAllowed = false;
+ for (String aud : authResult.getToken().getAudience()) {
+ ClientModel audClient = realm.getClientByClientId(aud);
+ if (audClient == null) continue;
+ if (audClient.equals(client)) {
+ exchangeFromAllowed = true;
+ break;
+ }
+ if (AdminPermissions.management(session, realm).clients().canExchangeFrom(client, audClient)) {
+ exchangeFromAllowed = true;
+ break;
+ }
+ }
+ if (!exchangeFromAllowed) {
+ logger.debug("Client does not have exchange rights for audience of provided token");
+ event.error(Errors.NOT_ALLOWED);
+ throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
+ }
+ if (!AdminPermissions.management(session, realm).clients().canExchangeTo(client, targetClient)) {
+ logger.debug("Client does not have exchange rights for target audience");
+ event.error(Errors.NOT_ALLOWED);
+ throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
+ }
+
+ AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, targetClient, false);
+ authSession.setAuthenticatedUser(authResult.getUser());
+ authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+ authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
+
+ UserSessionModel userSession = authResult.getSession();
+ event.session(userSession);
+
+ AuthenticationManager.setRolesAndMappersInSession(authSession);
+ AuthenticatedClientSessionModel clientSession = TokenManager.attachAuthenticationSession(session, userSession, authSession);
+
+ // Notes about client details
+ userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId());
+ userSession.setNote(ServiceAccountConstants.CLIENT_HOST, clientConnection.getRemoteHost());
+ userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, clientConnection.getRemoteAddr());
+
+ updateUserSessionFromClientAuth(userSession);
+
+ TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, targetClient, event, session, userSession, clientSession)
+ .generateAccessToken()
+ .generateRefreshToken();
+ responseBuilder.getAccessToken().issuedFor(client.getClientId());
+ responseBuilder.getRefreshToken().issuedFor(client.getClientId());
+
+ String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE);
+ if (TokenUtil.isOIDCRequest(scopeParam)) {
+ responseBuilder.generateIDToken();
+ }
+
+ AccessTokenResponse res = responseBuilder.build();
+
+ event.success();
+
+ return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
+ }
+
+
// https://tools.ietf.org/html/rfc7636#section-4.1
private boolean isValidPkceCodeVerifier(String codeVerifier) {
if (codeVerifier.length() < OIDCLoginProtocol.PKCE_CODE_VERIFIER_MIN_LENGTH) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
index 1b8817d..9571fde 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
@@ -42,6 +42,7 @@ import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.utils.MediaType;
@@ -139,18 +140,6 @@ public class UserInfoEndpoint {
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Token invalid: " + e.getMessage(), Response.Status.UNAUTHORIZED);
}
- UserSessionModel userSession = findValidSession(token, event);
-
- UserModel userModel = userSession.getUser();
- if (userModel == null) {
- event.error(Errors.USER_NOT_FOUND);
- throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User not found", Response.Status.BAD_REQUEST);
- }
-
- event.user(userModel)
- .detail(Details.USERNAME, userModel.getUsername());
-
-
ClientModel clientModel = realm.getClientByClientId(token.getIssuedFor());
if (clientModel == null) {
event.error(Errors.CLIENT_NOT_FOUND);
@@ -164,12 +153,21 @@ public class UserInfoEndpoint {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client disabled", Response.Status.BAD_REQUEST);
}
- AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientModel.getId());
- if (clientSession == null) {
- event.error(Errors.SESSION_EXPIRED);
- throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED);
+ UserSessionModel userSession = findValidSession(token, event, clientModel);
+
+ UserModel userModel = userSession.getUser();
+ if (userModel == null) {
+ event.error(Errors.USER_NOT_FOUND);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User not found", Response.Status.BAD_REQUEST);
}
+ event.user(userModel)
+ .detail(Details.USERNAME, userModel.getUsername());
+
+
+ // Existence of authenticatedClientSession for our client already handled before
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientModel.getId());
+
AccessToken userInfo = new AccessToken();
tokenManager.transformUserInfoAccessToken(session, userInfo, realm, clientModel, userModel, userSession, clientSession);
@@ -209,14 +207,14 @@ public class UserInfoEndpoint {
}
- private UserSessionModel findValidSession(AccessToken token, EventBuilder event) {
- UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
+ private UserSessionModel findValidSession(AccessToken token, EventBuilder event, ClientModel client) {
+ UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId());
UserSessionModel offlineUserSession = null;
if (AuthenticationManager.isSessionValid(realm, userSession)) {
event.session(userSession);
return userSession;
} else {
- offlineUserSession = session.sessions().getOfflineUserSession(realm, token.getSessionState());
+ offlineUserSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, client.getId());
if (AuthenticationManager.isOfflineSessionValid(realm, offlineUserSession)) {
event.session(offlineUserSession);
return offlineUserSession;
@@ -225,7 +223,7 @@ public class UserInfoEndpoint {
if (userSession == null && offlineUserSession == null) {
event.error(Errors.USER_SESSION_NOT_FOUND);
- throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User session not found", Response.Status.BAD_REQUEST);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User session not found or doesn't have client attached on it", Response.Status.UNAUTHORIZED);
}
if (userSession != null) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java
index 1e9b3e2..c156092 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java
@@ -26,8 +26,10 @@ import org.keycloak.representations.IDToken;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
/**
* Set the 'name' claim to be first + last name.
@@ -73,9 +75,12 @@ public class FullNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
UserModel user = userSession.getUser();
- String first = user.getFirstName() == null ? "" : user.getFirstName() + " ";
- String last = user.getLastName() == null ? "" : user.getLastName();
- token.getOtherClaims().put("name", first + last);
+ List<String> parts = new LinkedList<>();
+ Optional.ofNullable(user.getFirstName()).filter(s -> !s.isEmpty()).ifPresent(parts::add);
+ Optional.ofNullable(user.getLastName()).filter(s -> !s.isEmpty()).ifPresent(parts::add);
+ if (!parts.isEmpty()) {
+ token.getOtherClaims().put("name", String.join(" ", parts));
+ }
}
public static ProtocolMapperModel create(String name,
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 b535636..160013f 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
@@ -171,7 +171,7 @@ public class OIDCLoginProtocolService {
@Path("login-status-iframe.html")
public Object getLoginStatusIframe() {
- LoginStatusIframeEndpoint endpoint = new LoginStatusIframeEndpoint(realm);
+ LoginStatusIframeEndpoint endpoint = new LoginStatusIframeEndpoint();
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
return endpoint;
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 07aec65..6a26c69 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -59,6 +59,7 @@ import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
@@ -120,17 +121,10 @@ public class TokenManager {
}
public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, AccessToken oldToken, HttpHeaders headers) throws OAuthErrorException {
- UserModel user = session.users().getUserById(oldToken.getSubject(), realm);
- if (user == null) {
- throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown user");
- }
-
- if (!user.isEnabled()) {
- throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled");
- }
-
UserSessionModel userSession = null;
- if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) {
+ boolean offline = TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType());
+
+ if (offline) {
UserSessionManager sessionManager = new UserSessionManager(session);
userSession = sessionManager.findOfflineUserSession(realm, oldToken.getSessionState());
@@ -142,6 +136,8 @@ public class TokenManager {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline session not active", "Offline session not active");
}
+ } else {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline user session not found", "Offline user session not found");
}
} else {
// Find userSession regularly for online tokens
@@ -152,13 +148,28 @@ public class TokenManager {
}
}
- if (userSession == null) {
- throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline user session not found", "Offline user session not found");
+ UserModel user = userSession.getUser();
+ if (user == null) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown user");
+ }
+
+ if (!user.isEnabled()) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled");
}
ClientModel client = session.getContext().getClient();
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+ // Can theoretically happen in cross-dc environment. Try to see if userSession with our client is available in remoteCache
+ if (clientSession == null) {
+ userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSession.getId(), offline, client.getId());
+ if (userSession != null) {
+ clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+ } else {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session doesn't have required client", "Session doesn't have required client");
+ }
+ }
+
if (!client.getClientId().equals(oldToken.getIssuedFor())) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Unmatching clients", "Unmatching clients");
}
@@ -202,21 +213,15 @@ public class TokenManager {
return false;
}
- UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
+ UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId());
if (AuthenticationManager.isSessionValid(realm, userSession)) {
- AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
- if (clientSession != null) {
- return true;
- }
+ return true;
}
- userSession = session.sessions().getOfflineUserSession(realm, token.getSessionState());
+ userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, client.getId());
if (AuthenticationManager.isOfflineSessionValid(realm, userSession)) {
- AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
- if (clientSession != null) {
- return true;
- }
+ return true;
}
return false;
@@ -678,6 +683,18 @@ public class TokenManager {
this.clientSession = clientSession;
}
+ public AccessToken getAccessToken() {
+ return accessToken;
+ }
+
+ public RefreshToken getRefreshToken() {
+ return refreshToken;
+ }
+
+ public IDToken getIdToken() {
+ return idToken;
+ }
+
public AccessTokenResponseBuilder accessToken(AccessToken accessToken) {
this.accessToken = accessToken;
return this;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java
index f2fdd0c..9027ed5 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java
@@ -91,7 +91,7 @@ public abstract class OIDCRedirectUriBuilder {
@Override
public OIDCRedirectUriBuilder addParam(String paramName, String paramValue) {
- String param = paramName + "=" + Encode.encodeQueryParam(paramValue);
+ String param = paramName + "=" + Encode.encodeQueryParamAsIs(paramValue);
if (fragment == null) {
fragment = new StringBuilder(param);
} else {
diff --git a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java
index b9c07ec..2261d52 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java
@@ -127,7 +127,7 @@ public class SamlIDPDescriptorClientInstallation implements ClientInstallationPr
@Override
public String getHelpText() {
- return "SAML Metadata IDSSODescriptor tailored for the client. This is special because not every client may require things like digital signatures";
+ return "SAML Metadata IDPSSODescriptor tailored for the client. This is special because not every client may require things like digital signatures";
}
@Override
diff --git a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java
index bd109e6..8a39de0 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java
@@ -47,8 +47,10 @@ public class SamlSPDescriptorClientInstallation implements ClientInstallationPro
String nameIdFormat = samlClient.getNameIDFormat();
if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT;
String spCertificate = SPMetadataDescriptor.xmlKeyInfo(" ", null, samlClient.getClientSigningCertificate(), KeyTypes.SIGNING.value(), true);
+ String encCertificate = SPMetadataDescriptor.xmlKeyInfo(" ", null, samlClient.getClientEncryptingCertificate(), KeyTypes.ENCRYPTION.value(), true);
return SPMetadataDescriptor.getSPDescriptor(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get(), assertionUrl, logoutUrl,
- samlClient.requiresClientSignature(), samlClient.requiresAssertionSignature(), client.getClientId(), nameIdFormat, spCertificate);
+ samlClient.requiresClientSignature(), samlClient.requiresAssertionSignature(), samlClient.requiresEncryption(),
+ client.getClientId(), nameIdFormat, spCertificate, encCertificate);
}
@Override
diff --git a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java
old mode 100755
new mode 100644
index f21eff3..f6821b6
--- a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java
@@ -1,192 +1,127 @@
-/*
- * Copyright 2016 Red Hat, Inc. and/or its affiliates
- * and other contributors as indicated by the @author tags.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
package org.keycloak.protocol.saml.profile.ecp.authenticator;
import org.jboss.resteasy.spi.HttpRequest;
-import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
-import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.common.util.Base64;
import org.keycloak.events.Errors;
-import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
-import org.keycloak.provider.ProviderConfigProperty;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
-/**
- * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
- */
-public class HttpBasicAuthenticator implements AuthenticatorFactory {
+public class HttpBasicAuthenticator implements Authenticator {
- public static final String PROVIDER_ID = "http-basic-authenticator";
+ private static final String BASIC = "Basic";
+ private static final String BASIC_PREFIX = BASIC + " ";
@Override
- public String getDisplayType() {
- return "HTTP Basic Authentication";
+ public void authenticate(final AuthenticationFlowContext context) {
+ final HttpRequest httpRequest = context.getHttpRequest();
+ final HttpHeaders httpHeaders = httpRequest.getHttpHeaders();
+ final String[] usernameAndPassword = getUsernameAndPassword(httpHeaders);
+
+ context.attempted();
+
+ if (usernameAndPassword != null) {
+ final RealmModel realm = context.getRealm();
+ final String username = usernameAndPassword[0];
+ final UserModel user = context.getSession().users().getUserByUsername(username, realm);
+
+ if (user != null) {
+ final String password = usernameAndPassword[1];
+ final boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password));
+
+ if (valid) {
+ if (user.isEnabled()) {
+ userSuccessAction(context, user);
+ } else {
+ userDisabledAction(context, realm, user);
+ }
+ } else {
+ notValidCredentialsAction(context, realm, user);
+ }
+ } else {
+ nullUserAction(context, realm, username);
+ }
+ }
}
- @Override
- public String getReferenceCategory() {
- return null;
+ protected void userSuccessAction(AuthenticationFlowContext context, UserModel user) {
+ context.getAuthenticationSession().setAuthenticatedUser(user);
+ context.success();
}
- @Override
- public boolean isConfigurable() {
- return false;
+ protected void userDisabledAction(AuthenticationFlowContext context, RealmModel realm, UserModel user) {
+ userSuccessAction(context, user);
}
- @Override
- public Requirement[] getRequirementChoices() {
- return new Requirement[0];
+ protected void nullUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String user) {
+ // no-op by default
}
- @Override
- public boolean isUserSetupAllowed() {
- return false;
+ protected void notValidCredentialsAction(final AuthenticationFlowContext context, final RealmModel realm, final UserModel user) {
+ context.getEvent().user(user);
+ context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
+ context.failure(AuthenticationFlowError.INVALID_USER, Response.status(Response.Status.UNAUTHORIZED)
+ .header(HttpHeaders.WWW_AUTHENTICATE, BASIC_PREFIX + "realm=\"" + realm.getName() + "\"")
+ .build());
}
- @Override
- public String getHelpText() {
- return "Validates username and password from Authorization HTTP header";
- }
+ private String[] getUsernameAndPassword(final HttpHeaders httpHeaders) {
+ final List<String> authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION);
- @Override
- public List<ProviderConfigProperty> getConfigProperties() {
- return null;
- }
+ if (authHeaders == null || authHeaders.size() == 0) {
+ return null;
+ }
- @Override
- public Authenticator create(KeycloakSession session) {
- return new Authenticator() {
-
- private static final String BASIC = "Basic";
- private static final String BASIC_PREFIX = BASIC + " ";
-
- @Override
- public void authenticate(AuthenticationFlowContext context) {
- HttpRequest httpRequest = context.getHttpRequest();
- HttpHeaders httpHeaders = httpRequest.getHttpHeaders();
- String[] usernameAndPassword = getUsernameAndPassword(httpHeaders);
-
- context.attempted();
-
- if (usernameAndPassword != null) {
- RealmModel realm = context.getRealm();
- UserModel user = context.getSession().users().getUserByUsername(usernameAndPassword[0], realm);
-
- if (user != null) {
- String password = usernameAndPassword[1];
- boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password));
-
- if (valid) {
- context.getAuthenticationSession().setAuthenticatedUser(user);
- context.success();
- } else {
- context.getEvent().user(user);
- context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
- context.failure(AuthenticationFlowError.INVALID_USER, Response.status(Response.Status.UNAUTHORIZED)
- .header(HttpHeaders.WWW_AUTHENTICATE, BASIC_PREFIX + "realm=\"" + realm.getName() + "\"")
- .build());
- }
- }
- }
- }
+ String credentials = null;
- private String[] getUsernameAndPassword(HttpHeaders httpHeaders) {
- List<String> authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION);
+ for (final String authHeader : authHeaders) {
+ if (authHeader.startsWith(BASIC_PREFIX)) {
+ final String[] split = authHeader.trim().split("\\s+");
- if (authHeaders == null || authHeaders.size() == 0) {
- return null;
- }
-
- String credentials = null;
-
- for (String authHeader : authHeaders) {
- if (authHeader.startsWith(BASIC_PREFIX)) {
- String[] split = authHeader.trim().split("\\s+");
-
- if (split == null || split.length != 2) return null;
-
- credentials = split[1];
- }
- }
-
- try {
- return new String(Base64.decode(credentials)).split(":");
- } catch (IOException e) {
- throw new RuntimeException("Failed to parse credentials.", e);
- }
- }
-
- @Override
- public void action(AuthenticationFlowContext context) {
-
- }
-
- @Override
- public boolean requiresUser() {
- return false;
- }
-
- @Override
- public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
- return false;
- }
-
- @Override
- public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+ if (split == null || split.length != 2) return null;
+ credentials = split[1];
}
+ }
- @Override
- public void close() {
-
- }
- };
+ try {
+ return new String(Base64.decode(credentials)).split(":");
+ } catch (final IOException e) {
+ throw new RuntimeException("Failed to parse credentials.", e);
+ }
}
@Override
- public void init(Config.Scope config) {
+ public void action(final AuthenticationFlowContext context) {
}
@Override
- public void postInit(KeycloakSessionFactory factory) {
+ public boolean requiresUser() {
+ return false;
+ }
+ @Override
+ public boolean configuredFor(final KeycloakSession session, final RealmModel realm, final UserModel user) {
+ return false;
}
@Override
- public void close() {
+ public void setRequiredActions(final KeycloakSession session, final RealmModel realm, final UserModel user) {
}
@Override
- public String getId() {
- return PROVIDER_ID;
+ public void close() {
+
}
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticatorFactory.java b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticatorFactory.java
new file mode 100755
index 0000000..01adca2
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticatorFactory.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.protocol.saml.profile.ecp.authenticator;
+
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.Config;
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.common.util.Base64;
+import org.keycloak.events.Errors;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticationExecutionModel.Requirement;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class HttpBasicAuthenticatorFactory implements AuthenticatorFactory {
+
+ public static final String PROVIDER_ID = "http-basic-authenticator";
+
+ @Override
+ public String getDisplayType() {
+ return "HTTP Basic Authentication";
+ }
+
+ @Override
+ public String getReferenceCategory() {
+ return "basic";
+ }
+
+ @Override
+ public boolean isConfigurable() {
+ return false;
+ }
+
+ private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+ AuthenticationExecutionModel.Requirement.REQUIRED,
+ Requirement.ALTERNATIVE,
+ Requirement.OPTIONAL,
+ AuthenticationExecutionModel.Requirement.DISABLED
+ };
+
+ @Override
+ public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+ return REQUIREMENT_CHOICES;
+ }
+
+ @Override
+ public boolean isUserSetupAllowed() {
+ return false;
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Validates username and password from Authorization HTTP header";
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return null;
+ }
+
+ @Override
+ public Authenticator create(final KeycloakSession session) {
+ return new HttpBasicAuthenticator();
+ }
+
+ @Override
+ public void init(final Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(final KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
index a8218c1..66a7609 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -190,16 +190,9 @@ public class SamlProtocol implements LoginProtocol {
}
binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
}
- if (samlClient.requiresEncryption()) {
- PublicKey publicKey;
- try {
- publicKey = SamlProtocolUtils.getEncryptionValidationKey(client);
- } catch (Exception e) {
- logger.error("failed", e);
- return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
- }
- binding.encrypt(publicKey);
- }
+ // There is no support for encrypting status messages in SAML.
+ // Only assertions, attributes, base ID and name ID can be encrypted
+ // See Chapter 6 of saml-core-2.0-os.pdf
Document document = builder.buildDocument();
return buildErrorResponse(authSession, binding, document);
} catch (Exception e) {
@@ -457,7 +450,7 @@ public class SamlProtocol implements LoginProtocol {
if (samlClient.requiresEncryption()) {
PublicKey publicKey = null;
try {
- publicKey = SamlProtocolUtils.getEncryptionValidationKey(client);
+ publicKey = SamlProtocolUtils.getEncryptionKey(client);
} catch (Exception e) {
logger.error("failed", e);
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
index 026a54a..7ab97a4 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
@@ -103,7 +103,7 @@ public class SamlProtocolUtils {
* @return Public key for encryption.
* @throws VerificationException
*/
- public static PublicKey getEncryptionValidationKey(ClientModel client) throws VerificationException {
+ public static PublicKey getEncryptionKey(ClientModel client) throws VerificationException {
return getPublicKey(client, SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE);
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
index e0ac524..589dde3 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -138,6 +138,13 @@ public class SamlService extends AuthorizationEndpointBase {
protected Response handleSamlResponse(String samlResponse, String relayState) {
event.event(EventType.LOGOUT);
SAMLDocumentHolder holder = extractResponseDocument(samlResponse);
+
+ if (! (holder.getSamlObject() instanceof StatusResponseType)) {
+ event.detail(Details.REASON, "invalid_saml_response");
+ event.error(Errors.INVALID_SAML_RESPONSE);
+ return ErrorPage.error(session, Messages.INVALID_REQUEST);
+ }
+
StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
// validate destination
if (statusResponse.getDestination() != null && !uriInfo.getAbsolutePath().toString().equals(statusResponse.getDestination())) {
@@ -178,6 +185,12 @@ public class SamlService extends AuthorizationEndpointBase {
SAML2Object samlObject = documentHolder.getSamlObject();
+ if (! (samlObject instanceof RequestAbstractType)) {
+ event.event(EventType.LOGIN);
+ event.error(Errors.INVALID_SAML_AUTHN_REQUEST);
+ return ErrorPage.error(session, Messages.INVALID_REQUEST);
+ }
+
RequestAbstractType requestAbstractType = (RequestAbstractType) samlObject;
String issuer = requestAbstractType.getIssuer().getValue();
ClientModel client = realm.getClientByClientId(issuer);
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java b/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java
index 0083fdc..83b54dd 100644
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java
@@ -24,6 +24,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.services.managers.UserSessionCrossDCManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -54,7 +55,9 @@ public class SamlSessionUtils {
return null;
}
- UserSessionModel userSession = session.sessions().getUserSession(realm, parts[0]);
+ String userSessionId = parts[0];
+ String clientUUID = parts[1];
+ UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSessionId, false, clientUUID);
if (userSession == null) {
return null;
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
index da693aa..d6683f1 100755
--- a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
@@ -97,9 +97,12 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
auth.requireView(client);
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
+ if (client.getSecret() != null) {
+ rep.setSecret(client.getSecret());
+ }
if (auth.isRegistrationAccessToken()) {
- String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client, auth.getRegistrationAuth());
+ String registrationAccessToken = ClientRegistrationTokenUtils.updateTokenSignature(session, auth);
rep.setRegistrationAccessToken(registrationAccessToken);
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java
index dfed5aa..88986b5 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java
@@ -60,6 +60,8 @@ public class ClientRegistrationAuth {
private RealmModel realm;
private JsonWebToken jwt;
private ClientInitialAccessModel initialAccessModel;
+ private String kid;
+ private String token;
public ClientRegistrationAuth(KeycloakSession session, ClientRegistrationProvider provider, EventBuilder event) {
this.session = session;
@@ -81,10 +83,13 @@ public class ClientRegistrationAuth {
return;
}
- ClientRegistrationTokenUtils.TokenVerification tokenVerification = ClientRegistrationTokenUtils.verifyToken(session, realm, uri, split[1]);
+ token = split[1];
+
+ ClientRegistrationTokenUtils.TokenVerification tokenVerification = ClientRegistrationTokenUtils.verifyToken(session, realm, uri, token);
if (tokenVerification.getError() != null) {
throw unauthorized(tokenVerification.getError().getMessage());
}
+ kid = tokenVerification.getKid();
jwt = tokenVerification.getJwt();
if (isInitialAccessToken()) {
@@ -95,6 +100,18 @@ public class ClientRegistrationAuth {
}
}
+ public String getToken() {
+ return token;
+ }
+
+ public String getKid() {
+ return kid;
+ }
+
+ public JsonWebToken getJwt() {
+ return jwt;
+ }
+
private boolean isBearerToken() {
return jwt != null && TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType());
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
index e2d4846..270ca2a 100755
--- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
@@ -44,6 +44,27 @@ public class ClientRegistrationTokenUtils {
public static final String TYPE_INITIAL_ACCESS_TOKEN = "InitialAccessToken";
public static final String TYPE_REGISTRATION_ACCESS_TOKEN = "RegistrationAccessToken";
+ public static String updateTokenSignature(KeycloakSession session, ClientRegistrationAuth auth) {
+ KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(session.getContext().getRealm());
+
+ if (keys.getKid().equals(auth.getKid())) {
+ return auth.getToken();
+ } else {
+ RegistrationAccessToken regToken = new RegistrationAccessToken();
+ regToken.setRegistrationAuth(auth.getRegistrationAuth().toString().toLowerCase());
+
+ regToken.type(auth.getJwt().getType());
+ regToken.id(auth.getJwt().getId());
+ regToken.issuedAt(Time.currentTime());
+ regToken.expiration(0);
+ regToken.issuer(auth.getJwt().getIssuer());
+ regToken.audience(auth.getJwt().getIssuer());
+
+ String token = new JWSBuilder().kid(keys.getKid()).jsonContent(regToken).rsa256(keys.getPrivateKey());
+ return token;
+ }
+ }
+
public static String updateRegistrationAccessToken(KeycloakSession session, ClientModel client, RegistrationAuth registrationAuth) {
return updateRegistrationAccessToken(session, session.getContext().getRealm(), session.getContext().getUri(), client, registrationAuth);
}
@@ -75,7 +96,8 @@ public class ClientRegistrationTokenUtils {
return TokenVerification.error(new RuntimeException("Invalid token", e));
}
- PublicKey publicKey = session.keys().getRsaPublicKey(realm, input.getHeader().getKeyId());
+ String kid = input.getHeader().getKeyId();
+ PublicKey publicKey = session.keys().getRsaPublicKey(realm, kid);
if (!RSAProvider.verify(input, publicKey)) {
return TokenVerification.error(new RuntimeException("Failed verify token"));
@@ -102,7 +124,7 @@ public class ClientRegistrationTokenUtils {
return TokenVerification.error(new RuntimeException("Invalid type of token"));
}
- return TokenVerification.success(jwt);
+ return TokenVerification.success(kid, jwt);
}
private static String setupToken(JsonWebToken jwt, KeycloakSession session, RealmModel realm, UriInfo uri, String id, String type, int expiration) {
@@ -127,22 +149,28 @@ public class ClientRegistrationTokenUtils {
protected static class TokenVerification {
+ private final String kid;
private final JsonWebToken jwt;
private final RuntimeException error;
- public static TokenVerification success(JsonWebToken jwt) {
- return new TokenVerification(jwt, null);
+ public static TokenVerification success(String kid, JsonWebToken jwt) {
+ return new TokenVerification(kid, jwt, null);
}
public static TokenVerification error(RuntimeException error) {
- return new TokenVerification(null, error);
+ return new TokenVerification(null,null, error);
}
- private TokenVerification(JsonWebToken jwt, RuntimeException error) {
+ private TokenVerification(String kid, JsonWebToken jwt, RuntimeException error) {
+ this.kid = kid;
this.jwt = jwt;
this.error = error;
}
+ public String getKid() {
+ return kid;
+ }
+
public JsonWebToken getJwt() {
return jwt;
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java b/services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java
index eca5ca1..bad5bc4 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java
@@ -17,8 +17,6 @@
package org.keycloak.services.clientregistration.policy;
-import org.keycloak.services.clientregistration.RegistrationAccessToken;
-
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
index 0869c5d..21da694 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
@@ -112,6 +112,7 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
public void deploy(ProviderManager pm) {
Map<Class<? extends Provider>, Map<String, ProviderFactory>> copy = getFactoriesCopy();
Map<Class<? extends Provider>, Map<String, ProviderFactory>> newFactories = loadFactories(pm);
+ List<ProviderFactory> deployed = new LinkedList<>();
List<ProviderFactory> undeployed = new LinkedList<>();
for (Map.Entry<Class<? extends Provider>, Map<String, ProviderFactory>> entry : newFactories.entrySet()) {
@@ -120,6 +121,7 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
copy.put(entry.getKey(), entry.getValue());
} else {
for (ProviderFactory f : entry.getValue().values()) {
+ deployed.add(f);
ProviderFactory old = current.remove(f.getId());
if (old != null) undeployed.add(old);
}
@@ -131,6 +133,9 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
for (ProviderFactory factory : undeployed) {
factory.close();
}
+ for (ProviderFactory factory : deployed) {
+ factory.postInit(this);
+ }
}
@Override
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 6c91759..bc28fc4 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -192,16 +192,12 @@ public class AuthenticationManager {
// Logout all clientSessions of this user and client
public static void backchannelUserFromClient(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client, UriInfo uriInfo, HttpHeaders headers) {
- String clientId = client.getId();
-
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
for (UserSessionModel userSession : userSessions) {
- Collection<AuthenticatedClientSessionModel> clientSessions = userSession.getAuthenticatedClientSessions().values();
- for (AuthenticatedClientSessionModel clientSession : clientSessions) {
- if (clientSession.getClient().getId().equals(clientId)) {
- AuthenticationManager.backchannelLogoutClientSession(session, realm, clientSession, userSession, uriInfo, headers);
- TokenManager.dettachClientSession(session.sessions(), realm, clientSession);
- }
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+ if (clientSession != null) {
+ AuthenticationManager.backchannelLogoutClientSession(session, realm, clientSession, userSession, uriInfo, headers);
+ TokenManager.dettachClientSession(session.sessions(), realm, clientSession);
}
}
}
diff --git a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
index a975aa5..3d0c9ca 100644
--- a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
+++ b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
@@ -21,7 +21,6 @@ import java.util.HashMap;
import java.util.Map;
import org.jboss.logging.Logger;
-import org.keycloak.OAuth2Constants;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@@ -120,9 +119,13 @@ class CodeGenerateUtil {
String userSessionId = parts[2];
String clientUUID = parts[3];
- UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId);
+ UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClientAndCodeToTokenAction(realm, userSessionId, clientUUID);
if (userSession == null) {
- return null;
+ // TODO:mposolda Temporary workaround needed to track if code is invalid or was already used. Will be good to remove once used OAuth codes are tracked through one-time cache
+ userSession = session.sessions().getUserSession(realm, userSessionId);
+ if (userSession == null) {
+ return null;
+ }
}
return userSession.getAuthenticatedClientSessions().get(clientUUID);
diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
index 9aa4b69..3d71c2a 100755
--- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
@@ -135,20 +135,6 @@ public class ResourceAdminManager {
}
}
- public void logoutUserFromClient(URI requestUri, RealmModel realm, ClientModel resource, UserModel user) {
- List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
- List<AuthenticatedClientSessionModel> ourAppClientSessions = new LinkedList<>();
- if (userSessions != null) {
- for (UserSessionModel userSession : userSessions) {
- AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(resource.getId());
- if (clientSession != null) {
- ourAppClientSessions.add(clientSession);
- }
- }
- }
-
- logoutClientSessions(requestUri, realm, resource, ourAppClientSessions);
- }
public boolean logoutClientSession(URI requestUri, RealmModel realm, ClientModel resource, AuthenticatedClientSessionModel clientSession) {
return logoutClientSessions(requestUri, realm, resource, Arrays.asList(clientSession));
diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java
new file mode 100644
index 0000000..11795e5
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.services.managers;
+
+import java.util.Map;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.AuthenticatedClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.sessions.CommonClientSessionModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserSessionCrossDCManager {
+
+ private static final Logger logger = Logger.getLogger(UserSessionCrossDCManager.class);
+
+ private final KeycloakSession kcSession;
+
+ public UserSessionCrossDCManager(KeycloakSession session) {
+ this.kcSession = session;
+ }
+
+
+ // get userSession if it has "authenticatedClientSession" of specified client attached to it. Otherwise download it from remoteCache
+ public UserSessionModel getUserSessionWithClient(RealmModel realm, String id, boolean offline, String clientUUID) {
+ return kcSession.sessions().getUserSessionWithPredicate(realm, id, offline, userSession -> userSession.getAuthenticatedClientSessions().containsKey(clientUUID));
+ }
+
+
+ // get userSession if it has "authenticatedClientSession" of specified client attached to it and there is "CODE_TO_TOKEN" action. Otherwise download it from remoteCache
+ // TODO Probably remove this method once AuthenticatedClientSession.getAction is removed and information is moved to OAuth code JWT instead
+ public UserSessionModel getUserSessionWithClientAndCodeToTokenAction(RealmModel realm, String id, String clientUUID) {
+
+ return kcSession.sessions().getUserSessionWithPredicate(realm, id, false, (UserSessionModel userSession) -> {
+
+ Map<String, AuthenticatedClientSessionModel> authSessions = userSession.getAuthenticatedClientSessions();
+ if (!authSessions.containsKey(clientUUID)) {
+ return false;
+ }
+
+ AuthenticatedClientSessionModel authSession = authSessions.get(clientUUID);
+ return CommonClientSessionModel.Action.CODE_TO_TOKEN.toString().equals(authSession.getAction());
+
+ });
+ }
+
+
+ // Just check if userSession also exists on remoteCache. It can happen that logout happened on 2nd DC and userSession is already removed on remoteCache and this DC wasn't yet notified
+ public UserSessionModel getUserSessionIfExistsRemotely(RealmModel realm, String id) {
+ UserSessionModel userSession = kcSession.sessions().getUserSession(realm, id);
+
+ // This will remove userSession "locally" if it doesn't exists on remoteCache
+ kcSession.sessions().getUserSessionWithPredicate(realm, id, false, (UserSessionModel userSession2) -> {
+ return userSession2 == null;
+ });
+
+ return kcSession.sessions().getUserSession(realm, id);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
index cec46f2..797301c 100755
--- a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
@@ -171,7 +171,7 @@ public class UserStorageSyncManager {
}
UserStorageProviderClusterEvent event = UserStorageProviderClusterEvent.createEvent(removed, realm.getId(), provider);
- session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event, false);
+ session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event, false, ClusterProvider.DCNotify.ALL_DCS);
}
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 295f07b..180694a 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -35,6 +35,10 @@ public class Messages {
public static final String EXPIRED_ACTION = "expiredActionMessage";
+ public static final String EXPIRED_ACTION_TOKEN_NO_SESSION = "expiredActionTokenNoSessionMessage";
+
+ public static final String EXPIRED_ACTION_TOKEN_SESSION_EXISTS = "expiredActionTokenSessionExistsMessage";
+
public static final String MISSING_FIRST_NAME = "missingFirstNameMessage";
public static final String MISSING_LAST_NAME = "missingLastNameMessage";
@@ -156,6 +160,12 @@ public class Messages {
public static final String IDENTITY_PROVIDER_LINK_SUCCESS = "identityProviderLinkSuccess";
+ public static final String CONFIRM_ACCOUNT_LINKING = "confirmAccountLinking";
+
+ public static final String CONFIRM_EMAIL_ADDRESS_VERIFICATION = "confirmEmailAddressVerification";
+
+ public static final String CONFIRM_EXECUTION_OF_ACTIONS = "confirmExecutionOfActions";
+
public static final String STALE_CODE = "staleCodeMessage";
public static final String STALE_CODE_ACCOUNT = "staleCodeAccountMessage";
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 9dd9c4b..ac9bf80 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -206,7 +206,7 @@ public class AccountService extends AbstractSecuredLocalService {
setReferrerOnPage();
- UserSessionModel userSession = auth.getClientSession().getUserSession();
+ UserSessionModel userSession = auth.getSession();
AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, userSession.getId());
if (authSession != null) {
String forwardedError = authSession.getAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
@@ -663,7 +663,7 @@ public class AccountService extends AbstractSecuredLocalService {
EventBuilder errorEvent = event.clone().event(EventType.UPDATE_PASSWORD_ERROR)
.client(auth.getClient())
- .user(auth.getClientSession().getUserSession().getUser());
+ .user(auth.getSession().getUser());
if (requireCurrent) {
if (Validation.isBlank(password)) {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
index e7d611e..02063fa 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
@@ -562,9 +562,9 @@ public class ClientResource {
@NoCache
public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) {
auth.clients().requireManage(client);
- if (ref.isEnabled()) {
- AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
- permissions.clients().setPermissionsEnabled(client, ref.isEnabled());
+ AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
+ permissions.clients().setPermissionsEnabled(client, ref.isEnabled());
+ if (ref.isEnabled()) {
return toMgmtRef(client, permissions);
} else {
return new ManagementPermissionReference();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
index decb4da..c0ea7df 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
@@ -33,6 +33,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ForbiddenException;
@@ -188,7 +189,15 @@ public class ClientsResource {
if (Profile.isFeatureEnabled(Profile.Feature.AUTHORIZATION)) {
if (TRUE.equals(rep.getAuthorizationServicesEnabled())) {
- getAuthorizationService(clientModel).enable(true);
+ AuthorizationService authorizationService = getAuthorizationService(clientModel);
+
+ authorizationService.enable(true);
+
+ ResourceServerRepresentation authorizationSettings = rep.getAuthorizationSettings();
+
+ if (authorizationSettings != null) {
+ authorizationService.resourceServer().importSettings(uriInfo, authorizationSettings);
+ }
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java
index 3de46b0..0c0ed89 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java
@@ -263,9 +263,9 @@ public class GroupResource {
@NoCache
public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) {
auth.groups().requireManage(group);
+ AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
+ permissions.groups().setPermissionsEnabled(group, ref.isEnabled());
if (ref.isEnabled()) {
- AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
- permissions.groups().setPermissionsEnabled(group, ref.isEnabled());
return toMgmtRef(group, permissions);
} else {
return new ManagementPermissionReference();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
index 6c17794..a391c1d 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
@@ -129,6 +129,7 @@ public class ServerInfoAdminResource {
for (String name : providerIds) {
ProviderRepresentation provider = new ProviderRepresentation();
ProviderFactory<?> pi = session.getKeycloakSessionFactory().getProviderFactory(spi.getProviderClass(), name);
+ provider.setOrder(pi.order());
if (ServerInfoAwareProviderFactory.class.isAssignableFrom(pi.getClass())) {
provider.setOperationalInfo(((ServerInfoAwareProviderFactory) pi).getOperationalInfo());
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java
index 2a94132..d8eb94a 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java
@@ -18,6 +18,7 @@ package org.keycloak.services.resources.admin.permissions;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.models.ClientModel;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -26,6 +27,10 @@ import org.keycloak.authorization.model.ResourceServer;
public interface AdminPermissionManagement {
public static final String MANAGE_SCOPE = "manage";
public static final String VIEW_SCOPE = "view";
+ public static final String EXCHANGE_FROM_SCOPE="exchange-from";
+ public static final String EXCHANGE_TO_SCOPE="exchange-to";
+
+ ClientModel getRealmManagementClient();
AuthorizationProvider authz();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java
index f809e1d..705b258 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java
@@ -46,6 +46,10 @@ public class AdminPermissions {
return new MgmtPermissions(session, auth);
}
+ public static RealmsPermissionEvaluator realms(KeycloakSession session, RealmModel adminsRealm, UserModel admin) {
+ return new MgmtPermissions(session, adminsRealm, admin);
+ }
+
public static AdminPermissionManagement management(KeycloakSession session, RealmModel realm) {
return new MgmtPermissions(session, realm);
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java
index 8a6b76d..ccf9679 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java
@@ -41,6 +41,14 @@ public interface ClientPermissionManagement {
Map<String, String> getPermissions(ClientModel client);
+ boolean canExchangeFrom(ClientModel authorizedClient, ClientModel from);
+
+ boolean canExchangeTo(ClientModel authorizedClient, ClientModel to);
+
+ Policy exchangeFromPermission(ClientModel client);
+
+ Policy exchangeToPermission(ClientModel client);
+
Policy mapRolesPermission(ClientModel client);
Policy mapRolesClientScopePermission(ClientModel client);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
index 2b1e234..bbb7bf4 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
@@ -18,23 +18,35 @@ package org.keycloak.services.resources.admin.permissions;
import org.jboss.logging.Logger;
import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.attribute.Attributes;
+import org.keycloak.authorization.common.ClientModelIdentity;
+import org.keycloak.authorization.common.DefaultEvaluationContext;
+import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.policy.evaluation.EvaluationContext;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
+import org.keycloak.representations.AccessToken;
import org.keycloak.services.ForbiddenException;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
+import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_FROM_SCOPE;
+import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE;
+
/**
* Manages default policies for all users.
*
@@ -79,6 +91,14 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
return MAP_ROLES_COMPOSITE_SCOPE + ".permission.client." + client.getId();
}
+ private String getExchangeToPermissionName(ClientModel client) {
+ return EXCHANGE_TO_SCOPE + ".permission.client." + client.getId();
+ }
+
+ private String getExchangeFromPermissionName(ClientModel client) {
+ return EXCHANGE_FROM_SCOPE + ".permission.client." + client.getId();
+ }
+
private void initialize(ClientModel client) {
ResourceServer server = root.findOrCreateResourceServer(client);
Scope manageScope = manageScope(server);
@@ -93,18 +113,11 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
if (mapRoleScope == null) {
mapRoleScope = authz.getStoreFactory().getScopeStore().create(MAP_ROLES_SCOPE, server);
}
- Scope mapRoleClientScope = authz.getStoreFactory().getScopeStore().findByName(MAP_ROLES_CLIENT_SCOPE, server.getId());
- if (mapRoleClientScope == null) {
- mapRoleClientScope = authz.getStoreFactory().getScopeStore().create(MAP_ROLES_CLIENT_SCOPE, server);
- }
- Scope mapRoleCompositeScope = authz.getStoreFactory().getScopeStore().findByName(MAP_ROLES_COMPOSITE_SCOPE, server.getId());
- if (mapRoleCompositeScope == null) {
- mapRoleCompositeScope = authz.getStoreFactory().getScopeStore().create(MAP_ROLES_COMPOSITE_SCOPE, server);
- }
- Scope configureScope = authz.getStoreFactory().getScopeStore().findByName(CONFIGURE_SCOPE, server.getId());
- if (configureScope == null) {
- configureScope = authz.getStoreFactory().getScopeStore().create(CONFIGURE_SCOPE, server);
- }
+ Scope mapRoleClientScope = root.initializeScope(MAP_ROLES_CLIENT_SCOPE, server);
+ Scope mapRoleCompositeScope = root.initializeScope(MAP_ROLES_COMPOSITE_SCOPE, server);
+ Scope configureScope = root.initializeScope(CONFIGURE_SCOPE, server);
+ Scope exchangeFromScope = root.initializeScope(EXCHANGE_FROM_SCOPE, server);
+ Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
String resourceName = getResourceName(client);
Resource resource = authz.getStoreFactory().getResourceStore().findByName(resourceName, server.getId());
@@ -118,6 +131,8 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
scopeset.add(mapRoleScope);
scopeset.add(mapRoleClientScope);
scopeset.add(mapRoleCompositeScope);
+ scopeset.add(exchangeFromScope);
+ scopeset.add(exchangeToScope);
resource.updateScopes(scopeset);
}
String managePermissionName = getManagePermissionName(client);
@@ -150,6 +165,16 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
if (mapRoleCompositePermission == null) {
Helper.addEmptyScopePermission(authz, server, mapRoleCompositePermissionName, resource, mapRoleCompositeScope);
}
+ String exchangeToPermissionName = getExchangeToPermissionName(client);
+ Policy exchangeToPermission = authz.getStoreFactory().getPolicyStore().findByName(exchangeToPermissionName, server.getId());
+ if (exchangeToPermission == null) {
+ Helper.addEmptyScopePermission(authz, server, exchangeToPermissionName, resource, exchangeToScope);
+ }
+ String exchangeFromPermissionName = getExchangeFromPermissionName(client);
+ Policy exchangeFromPermission = authz.getStoreFactory().getPolicyStore().findByName(exchangeFromPermissionName, server.getId());
+ if (exchangeFromPermission == null) {
+ Helper.addEmptyScopePermission(authz, server, exchangeFromPermissionName, resource, exchangeFromScope);
+ }
}
private void deletePolicy(String name, ResourceServer server) {
@@ -169,6 +194,8 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
deletePolicy(getMapRolesClientScopePermissionName(client), server);
deletePolicy(getMapRolesCompositePermissionName(client), server);
deletePolicy(getConfigurePermissionName(client), server);
+ deletePolicy(getExchangeToPermissionName(client), server);
+ deletePolicy(getExchangeFromPermissionName(client), server);
Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(client), server.getId());;
if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId());
}
@@ -196,6 +223,14 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
return authz.getStoreFactory().getScopeStore().findByName(AdminPermissionManagement.MANAGE_SCOPE, server.getId());
}
+ private Scope exchangeFromScope(ResourceServer server) {
+ return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_FROM_SCOPE, server.getId());
+ }
+
+ private Scope exchangeToScope(ResourceServer server) {
+ return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId());
+ }
+
private Scope configureScope(ResourceServer server) {
return authz.getStoreFactory().getScopeStore().findByName(CONFIGURE_SCOPE, server.getId());
}
@@ -271,16 +306,119 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
@Override
public Map<String, String> getPermissions(ClientModel client) {
- Map<String, String> scopes = new HashMap<>();
- scopes.put(MAP_ROLES_SCOPE, mapRolesPermission(client).getId());
- scopes.put(MAP_ROLES_CLIENT_SCOPE, mapRolesClientScopePermission(client).getId());
- scopes.put(MAP_ROLES_COMPOSITE_SCOPE, mapRolesCompositePermission(client).getId());
+ initialize(client);
+ Map<String, String> scopes = new LinkedHashMap<>();
scopes.put(AdminPermissionManagement.VIEW_SCOPE, viewPermission(client).getId());
scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission(client).getId());
scopes.put(CONFIGURE_SCOPE, configurePermission(client).getId());
+ scopes.put(MAP_ROLES_SCOPE, mapRolesPermission(client).getId());
+ scopes.put(MAP_ROLES_CLIENT_SCOPE, mapRolesClientScopePermission(client).getId());
+ scopes.put(MAP_ROLES_COMPOSITE_SCOPE, mapRolesCompositePermission(client).getId());
+ scopes.put(EXCHANGE_FROM_SCOPE, exchangeFromPermission(client).getId());
+ scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(client).getId());
return scopes;
}
+ @Override
+ public boolean canExchangeFrom(ClientModel authorizedClient, ClientModel from) {
+ if (!authorizedClient.equals(from)) {
+ ResourceServer server = resourceServer(from);
+ if (server == null) {
+ logger.debug("No resource server set up for target client");
+ return false;
+ }
+
+ Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(from), server.getId());
+ if (resource == null) {
+ logger.debug("No resource object set up for target client");
+ return false;
+ }
+
+ Policy policy = authz.getStoreFactory().getPolicyStore().findByName(getExchangeFromPermissionName(from), server.getId());
+ if (policy == null) {
+ logger.debug("No permission object set up for target client");
+ return false;
+ }
+
+ Set<Policy> associatedPolicies = policy.getAssociatedPolicies();
+ // if no policies attached to permission then just do default behavior
+ if (associatedPolicies == null || associatedPolicies.isEmpty()) {
+ logger.debug("No policies set up for permission on target client");
+ return false;
+ }
+
+ Scope scope = exchangeFromScope(server);
+ if (scope == null) {
+ logger.debug(EXCHANGE_FROM_SCOPE + " not initialized");
+ return false;
+ }
+ ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient);
+ EvaluationContext context = new DefaultEvaluationContext(identity, session) {
+ @Override
+ public Map<String, Collection<String>> getBaseAttributes() {
+ Map<String, Collection<String>> attributes = super.getBaseAttributes();
+ attributes.put("kc.client.id", Arrays.asList(authorizedClient.getClientId()));
+ return attributes;
+ }
+
+ };
+ return root.evaluatePermission(resource, scope, server, context);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean canExchangeTo(ClientModel authorizedClient, ClientModel to) {
+
+ if (!authorizedClient.equals(to)) {
+ ResourceServer server = resourceServer(to);
+ if (server == null) {
+ logger.debug("No resource server set up for target client");
+ return false;
+ }
+
+ Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(to), server.getId());
+ if (resource == null) {
+ logger.debug("No resource object set up for target client");
+ return false;
+ }
+
+ Policy policy = authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(to), server.getId());
+ if (policy == null) {
+ logger.debug("No permission object set up for target client");
+ return false;
+ }
+
+ Set<Policy> associatedPolicies = policy.getAssociatedPolicies();
+ // if no policies attached to permission then just do default behavior
+ if (associatedPolicies == null || associatedPolicies.isEmpty()) {
+ logger.debug("No policies set up for permission on target client");
+ return false;
+ }
+
+ Scope scope = exchangeToScope(server);
+ if (scope == null) {
+ logger.debug(EXCHANGE_TO_SCOPE + " not initialized");
+ return false;
+ }
+ ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient);
+ EvaluationContext context = new DefaultEvaluationContext(identity, session) {
+ @Override
+ public Map<String, Collection<String>> getBaseAttributes() {
+ Map<String, Collection<String>> attributes = super.getBaseAttributes();
+ attributes.put("kc.client.id", Arrays.asList(authorizedClient.getClientId()));
+ return attributes;
+ }
+
+ };
+ return root.evaluatePermission(resource, scope, server, context);
+ }
+ return true;
+ }
+
+
+
+
@Override
public boolean canManage(ClientModel client) {
@@ -464,6 +602,20 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
}
@Override
+ public Policy exchangeFromPermission(ClientModel client) {
+ ResourceServer server = resourceServer(client);
+ if (server == null) return null;
+ return authz.getStoreFactory().getPolicyStore().findByName(getExchangeFromPermissionName(client), server.getId());
+ }
+
+ @Override
+ public Policy exchangeToPermission(ClientModel client) {
+ ResourceServer server = resourceServer(client);
+ if (server == null) return null;
+ return authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(client), server.getId());
+ }
+
+ @Override
public Policy mapRolesPermission(ClientModel client) {
ResourceServer server = resourceServer(client);
if (server == null) return null;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java
index a7e9f37..b20d462 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java
@@ -31,6 +31,7 @@ import org.keycloak.services.ForbiddenException;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
@@ -184,9 +185,13 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
authz.getStoreFactory().getPolicyStore().delete(manageMembersPermission.getId());
}
Policy viewMembersPermission = viewMembersPermission(group);
- if (manageMembersPermission == null) {
+ if (viewMembersPermission == null) {
authz.getStoreFactory().getPolicyStore().delete(viewMembersPermission.getId());
}
+ Policy manageMembershipPermission = manageMembershipPermission(group);
+ if (manageMembershipPermission != null) {
+ authz.getStoreFactory().getPolicyStore().delete(manageMembershipPermission.getId());
+ }
Resource resource = groupResource(group);
if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId());
}
@@ -242,11 +247,12 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
@Override
public Map<String, String> getPermissions(GroupModel group) {
- Map<String, String> scopes = new HashMap<>();
+ initialize(group);
+ Map<String, String> scopes = new LinkedHashMap<>();
scopes.put(AdminPermissionManagement.VIEW_SCOPE, viewPermission(group).getId());
scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission(group).getId());
- scopes.put(MANAGE_MEMBERS_SCOPE, manageMembersPermission(group).getId());
scopes.put(VIEW_MEMBERS_SCOPE, viewMembersPermission(group).getId());
+ scopes.put(MANAGE_MEMBERS_SCOPE, manageMembersPermission(group).getId());
scopes.put(MANAGE_MEMBERSHIP_SCOPE, manageMembershipPermission(group).getId());
return scopes;
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
index 2df4953..fe4a11f 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
@@ -107,6 +107,14 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
this.identity = new KeycloakIdentity(auth.getToken(), session);
}
}
+
+ MgmtPermissions(KeycloakSession session, RealmModel adminsRealm, UserModel admin) {
+ this.session = session;
+ this.admin = admin;
+ this.adminsRealm = adminsRealm;
+ this.identity = new UserModelIdentity(adminsRealm, admin);
+ }
+
MgmtPermissions(KeycloakSession session, RealmModel realm, RealmModel adminsRealm, UserModel admin) {
this(session, realm);
this.admin = admin;
@@ -114,6 +122,7 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
this.identity = new UserModelIdentity(realm, admin);
}
+ @Override
public ClientModel getRealmManagementClient() {
ClientModel client = null;
if (realm.getName().equals(Config.getAdminRealm())) {
@@ -268,6 +277,14 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
return scope;
}
+ public Scope initializeScope(String name, ResourceServer server) {
+ Scope scope = authz.getStoreFactory().getScopeStore().findByName(name, server.getId());
+ if (scope == null) {
+ scope = authz.getStoreFactory().getScopeStore().create(name, server);
+ }
+ return scope;
+ }
+
public Scope realmManageScope() {
@@ -298,10 +315,14 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
}
public boolean evaluatePermission(Resource resource, Scope scope, ResourceServer resourceServer, Identity identity) {
+ EvaluationContext context = new DefaultEvaluationContext(identity, session);
+ return evaluatePermission(resource, scope, resourceServer, context);
+ }
+
+ public boolean evaluatePermission(Resource resource, Scope scope, ResourceServer resourceServer, EvaluationContext context) {
RealmModel oldRealm = session.getContext().getRealm();
try {
session.getContext().setRealm(realm);
- EvaluationContext context = new DefaultEvaluationContext(identity, session);
DecisionResult decisionCollector = new DecisionResult();
List<ResourcePermission> permissions = Permissions.permission(resourceServer, resource, scope);
PermissionEvaluator from = authz.evaluators().from(permissions, context);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java
index 091d7a5..0e12861 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java
@@ -36,6 +36,7 @@ import org.keycloak.services.ForbiddenException;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
@@ -87,7 +88,8 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
@Override
public Map<String, String> getPermissions(RoleModel role) {
- Map<String, String> scopes = new HashMap<>();
+ initialize(role);
+ Map<String, String> scopes = new LinkedHashMap<>();
scopes.put(RolePermissionManagement.MAP_ROLE_SCOPE, mapRolePermission(role).getId());
scopes.put(RolePermissionManagement.MAP_ROLE_CLIENT_SCOPE_SCOPE, mapClientScopePermission(role).getId());
scopes.put(RolePermissionManagement.MAP_ROLE_COMPOSITE_SCOPE, mapCompositePermission(role).getId());
@@ -136,10 +138,13 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
if (root.admin().hasRole(role)) return true;
ClientModel adminClient = root.getRealmManagementClient();
+ // is this an admin role in 'realm-management' client of the realm we are managing?
if (adminClient.equals(role.getContainer())) {
// if this is realm admin role, then check to see if admin has similar permissions
// we do this so that the authz service is invoked
- if (role.getName().equals(AdminRoles.MANAGE_CLIENTS)) {
+ if (role.getName().equals(AdminRoles.MANAGE_CLIENTS)
+ || role.getName().equals(AdminRoles.CREATE_CLIENT)
+ ) {
if (!root.clients().canManage()) {
return adminConflictMessage(role);
} else {
@@ -151,6 +156,9 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
} else {
return true;
}
+
+ } else if (role.getName().equals(AdminRoles.QUERY_REALMS)) {
+ return true;
} else if (role.getName().equals(AdminRoles.QUERY_CLIENTS)) {
return true;
} else if (role.getName().equals(AdminRoles.QUERY_USERS)) {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java
index 149e526..3ac26ed 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java
@@ -34,6 +34,7 @@ import org.keycloak.services.ForbiddenException;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
@@ -121,9 +122,10 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme
@Override
public Map<String, String> getPermissions() {
- Map<String, String> scopes = new HashMap<>();
- scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission().getId());
+ initialize();
+ Map<String, String> scopes = new LinkedHashMap<>();
scopes.put(AdminPermissionManagement.VIEW_SCOPE, viewPermission().getId());
+ scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission().getId());
scopes.put(MAP_ROLES_SCOPE, mapRolesPermission().getId());
scopes.put(MANAGE_GROUP_MEMBERSHIP_SCOPE, manageGroupMembershipPermission().getId());
scopes.put(IMPERSONATE_SCOPE, adminImpersonatingPermission().getId());
@@ -157,32 +159,32 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme
ResourceServer server = root.realmResourceServer();
if (server == null) return;
Policy policy = managePermission();
- if (policy == null) {
+ if (policy != null) {
authz.getStoreFactory().getPolicyStore().delete(policy.getId());
}
policy = viewPermission();
- if (policy == null) {
+ if (policy != null) {
authz.getStoreFactory().getPolicyStore().delete(policy.getId());
}
policy = mapRolesPermission();
- if (policy == null) {
+ if (policy != null) {
authz.getStoreFactory().getPolicyStore().delete(policy.getId());
}
policy = manageGroupMembershipPermission();
- if (policy == null) {
+ if (policy != null) {
authz.getStoreFactory().getPolicyStore().delete(policy.getId());
}
policy = adminImpersonatingPermission();
- if (policy == null) {
+ if (policy != null) {
authz.getStoreFactory().getPolicyStore().delete(policy.getId());
}
policy = userImpersonatedPermission();
- if (policy == null) {
+ if (policy != null) {
authz.getStoreFactory().getPolicyStore().delete(policy.getId());
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 28392f7..ebc89be 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -16,6 +16,7 @@
*/
package org.keycloak.services.resources.admin;
+import com.fasterxml.jackson.core.type.TypeReference;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException;
@@ -29,6 +30,7 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.PemUtils;
+import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Event;
import org.keycloak.events.EventQuery;
import org.keycloak.events.EventStoreProvider;
@@ -50,6 +52,7 @@ import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.UserCache;
@@ -102,9 +105,9 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import java.util.regex.PatternSyntaxException;
import static org.keycloak.models.utils.StripSecretsUtils.stripForExport;
+import static org.keycloak.util.JsonSerialization.readValue;
/**
* Base resource class for the admin REST api of one realm
@@ -811,6 +814,35 @@ public class RealmAdminResource {
return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST);
}
+ /**
+ * Test SMTP connection with current logged in user
+ *
+ * @param config SMTP server configuration
+ * @return
+ * @throws Exception
+ */
+ @Path("testSMTPConnection/{config}")
+ @POST
+ @NoCache
+ public Response testSMTPConnection(final @PathParam("config") String config) throws Exception {
+ Map<String, String> settings = readValue(config, new TypeReference<Map<String, String>>() {
+ });
+
+ try {
+ UserModel user = auth.adminAuth().getUser();
+ if (user.getEmail() == null) {
+ return ErrorResponse.error("Logged in user does not have an e-mail.", Response.Status.INTERNAL_SERVER_ERROR);
+ }
+ session.getProvider(EmailTemplateProvider.class).sendSmtpTestEmail(settings, user);
+ } catch (Exception e) {
+ e.printStackTrace();
+ logger.errorf("Failed to send email \n %s", e.getCause());
+ return ErrorResponse.error("Failed to send email", Response.Status.INTERNAL_SERVER_ERROR);
+ }
+
+ return Response.noContent().build();
+ }
+
@Path("identity-provider")
public IdentityProvidersResource getIdentityProviderResource() {
return new IdentityProvidersResource(realm, session, this.auth, adminEvent);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java
index 79bb6c8..7ad9d22 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java
@@ -364,9 +364,9 @@ public class RoleContainerResource extends RoleResource {
throw new NotFoundException("Could not find role");
}
+ AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
+ permissions.roles().setPermissionsEnabled(role, ref.isEnabled());
if (ref.isEnabled()) {
- AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
- permissions.roles().setPermissionsEnabled(role, ref.isEnabled());
return RoleByIdResource.toMgmtRef(role, permissions);
} else {
return new ManagementPermissionReference();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
index bf3b236..fbd318a 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
@@ -704,6 +704,7 @@ public class UserResource {
String link = builder.build(realm.getName()).toString();
this.session.getProvider(EmailTemplateProvider.class)
+ .setAttribute(Constants.TEMPLATE_ATTR_REQUIRED_ACTIONS, token.getRequiredActions())
.setRealm(realm)
.setUser(user)
.sendExecuteActions(link, TimeUnit.SECONDS.toMinutes(lifespan));
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 6ed677f..c7b9945 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -52,6 +52,7 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.provider.ProviderFactory;
@@ -162,8 +163,9 @@ public class UsersResource {
try {
UserModel user = session.users().addUser(realm, rep.getUsername());
Set<String> emptySet = Collections.emptySet();
- UserResource.updateUserFromRep(user, rep, emptySet, realm, session, false);
+ UserResource.updateUserFromRep(user, rep, emptySet, realm, session, false);
+ RepresentationToModel.createCredentials(rep, session, realm, user);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, user.getId()).representation(rep).success();
if (session.getTransactionManager().isActive()) {
diff --git a/services/src/main/java/org/keycloak/services/resources/Cors.java b/services/src/main/java/org/keycloak/services/resources/Cors.java
index f938a5f..c9bfa03 100755
--- a/services/src/main/java/org/keycloak/services/resources/Cors.java
+++ b/services/src/main/java/org/keycloak/services/resources/Cors.java
@@ -133,7 +133,11 @@ public class Cors {
return builder.build();
}
- builder.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
+ if (allowedOrigins != null && allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)) {
+ builder.header(ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD);
+ } else {
+ builder.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
+ }
if (preflight) {
if (allowedMethods != null) {
@@ -178,7 +182,11 @@ public class Cors {
logger.debug("build CORS headers and return");
- response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
+ if (allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)) {
+ response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD);
+ } else {
+ response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
+ }
if (preflight) {
if (allowedMethods != null) {
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index eed2858..7961163 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -221,18 +221,6 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
- // only allow origins from client. Not sure we need this as I don't believe cookies can be
- // sent if CORS preflight requests can't execute.
- String origin = headers.getRequestHeaders().getFirst("Origin");
- if (origin != null) {
- String redirectOrigin = UriUtils.getOrigin(redirectUri);
- if (!redirectOrigin.equals(origin)) {
- event.error(Errors.ILLEGAL_ORIGIN);
- throw new ErrorPageException(session, Messages.INVALID_REQUEST);
-
- }
- }
-
AuthenticationManager.AuthResult cookieResult = AuthenticationManager.authenticateIdentityCookie(session, realmModel, true);
String errorParam = "link_error";
if (cookieResult == null) {
@@ -979,7 +967,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return ParsedCodeContext.response(staleCodeError);
}
- SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, clientConnection, session, event, code, null, clientId, LoginActionsService.AUTHENTICATE_PATH);
+ SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, request, clientConnection, session, event, code, null, clientId, LoginActionsService.AUTHENTICATE_PATH);
checks.initialVerify();
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
@@ -993,7 +981,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
Response errorResponse = checks.getResponse();
// Remove "code" from browser history
- errorResponse = BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, errorResponse, true);
+ errorResponse = BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, errorResponse, true, request);
return ParsedCodeContext.response(errorResponse);
}
} else {
diff --git a/services/src/main/java/org/keycloak/services/resources/JsResource.java b/services/src/main/java/org/keycloak/services/resources/JsResource.java
index 3e41dad..404d3e4 100755
--- a/services/src/main/java/org/keycloak/services/resources/JsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/JsResource.java
@@ -17,14 +17,15 @@
package org.keycloak.services.resources;
-import org.keycloak.Config;
import org.keycloak.common.Version;
+import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.utils.MediaType;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Response;
import java.io.InputStream;
@@ -45,37 +46,29 @@ public class JsResource {
@GET
@Path("/keycloak.js")
@Produces(MediaType.TEXT_PLAIN_JAVASCRIPT)
- public Response getKeycloakJs() {
- return getJs("keycloak.js");
+ public Response getKeycloakJs(@QueryParam("version") String version) {
+ return getJs("keycloak.js", version);
}
@GET
@Path("/{version}/keycloak.js")
@Produces(MediaType.TEXT_PLAIN_JAVASCRIPT)
public Response getKeycloakJsWithVersion(@PathParam("version") String version) {
- if (!version.equals(Version.RESOURCES_VERSION)) {
- return Response.status(Response.Status.NOT_FOUND).build();
- }
-
- return getKeycloakJs();
+ return getJs("keycloak.js", version);
}
@GET
@Path("/keycloak.min.js")
@Produces(MediaType.TEXT_PLAIN_JAVASCRIPT)
- public Response getKeycloakMinJs() {
- return getJs("keycloak.min.js");
+ public Response getKeycloakMinJs(@QueryParam("version") String version) {
+ return getJs("keycloak.min.js", version);
}
@GET
@Path("/{version}/keycloak.min.js")
@Produces(MediaType.TEXT_PLAIN_JAVASCRIPT)
public Response getKeycloakMinJsWithVersion(@PathParam("version") String version) {
- if (!version.equals(Version.RESOURCES_VERSION)) {
- return Response.status(Response.Status.NOT_FOUND).build();
- }
-
- return getKeycloakMinJs();
+ return getJs("keycloak.min.js", version);
}
/**
@@ -86,46 +79,44 @@ public class JsResource {
@GET
@Path("/keycloak-authz.js")
@Produces(MediaType.TEXT_PLAIN_JAVASCRIPT)
- public Response getKeycloakAuthzJs() {
- return getJs("keycloak-authz.js");
+ public Response getKeycloakAuthzJs(@QueryParam("version") String version) {
+ return getJs("keycloak-authz.js", version);
}
@GET
@Path("/{version}/keycloak-authz.js")
@Produces(MediaType.TEXT_PLAIN_JAVASCRIPT)
public Response getKeycloakAuthzJsWithVersion(@PathParam("version") String version) {
- if (!version.equals(Version.RESOURCES_VERSION)) {
- return Response.status(Response.Status.NOT_FOUND).build();
- }
-
- return getKeycloakAuthzJs();
+ return getJs("keycloak-authz.js", version);
}
@GET
@Path("/keycloak-authz.min.js")
@Produces(MediaType.TEXT_PLAIN_JAVASCRIPT)
- public Response getKeycloakAuthzMinJs() {
- return getJs("keycloak-authz.min.js");
+ public Response getKeycloakAuthzMinJs(@QueryParam("version") String version) {
+ return getJs("keycloak-authz.min.js", version);
}
@GET
@Path("/{version}/keycloak-authz.min.js")
@Produces(MediaType.TEXT_PLAIN_JAVASCRIPT)
public Response getKeycloakAuthzMinJsWithVersion(@PathParam("version") String version) {
- if (!version.equals(Version.RESOURCES_VERSION)) {
- return Response.status(Response.Status.NOT_FOUND).build();
- }
-
- return getKeycloakAuthzMinJs();
+ return getJs("keycloak-authz.min.js", version);
}
- private Response getJs(String name) {
+ private Response getJs(String name, String version) {
+ CacheControl cacheControl;
+ if (version != null) {
+ if (!version.equals(Version.RESOURCES_VERSION)) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+ cacheControl = CacheControlUtil.getDefaultCacheControl();
+ } else {
+ cacheControl = CacheControlUtil.noCache();
+ }
+
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(name);
if (inputStream != null) {
- CacheControl cacheControl = new CacheControl();
- cacheControl.setNoTransform(false);
- cacheControl.setMaxAge(Config.scope("theme").getInt("staticMaxAge", -1));
-
return Response.ok(inputStream).type("text/javascript").cacheControl(cacheControl).build();
} else {
return Response.status(Response.Status.NOT_FOUND).build();
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index 42a2fd8..cf8910c 100644
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -78,6 +78,7 @@ import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -153,20 +154,20 @@ public class KeycloakApplication extends Application {
exportImportManager[0].runExport();
}
- boolean bootstrapAdminUser = false;
- KeycloakSession session = sessionFactory.create();
- try {
- session.getTransactionManager().begin();
- bootstrapAdminUser = new ApplianceBootstrap(session).isNoMasterUser();
+ AtomicBoolean bootstrapAdminUser = new AtomicBoolean(false);
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
- session.getTransactionManager().commit();
- } finally {
- session.close();
- }
+ @Override
+ public void run(KeycloakSession session) {
+ boolean shouldBootstrapAdmin = new ApplianceBootstrap(session).isNoMasterUser();
+ bootstrapAdminUser.set(shouldBootstrapAdmin);
+
+ sessionFactory.publish(new PostMigrationEvent(session));
+ }
- sessionFactory.publish(new PostMigrationEvent());
+ });
- singletons.add(new WelcomeResource(bootstrapAdminUser));
+ singletons.add(new WelcomeResource(bootstrapAdminUser.get()));
setupScheduledTasks(sessionFactory);
} catch (Throwable t) {
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 b1bd354..f6dd8a4 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -16,7 +16,6 @@
*/
package org.keycloak.services.resources;
-import org.keycloak.authentication.actiontoken.DefaultActionToken;
import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
@@ -27,6 +26,7 @@ import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.TokenVerifier;
+import org.keycloak.authentication.ExplainedVerificationException;
import org.keycloak.authentication.actiontoken.*;
import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
@@ -42,6 +42,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.exceptions.TokenNotActiveException;
+import org.keycloak.models.ActionTokenKeyModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
@@ -59,6 +60,7 @@ import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
@@ -179,7 +181,7 @@ public class LoginActionsService {
}
private SessionCodeChecks checksForCode(String code, String execution, String clientId, String flowPath) {
- SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, code, execution, clientId, flowPath);
+ SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, code, execution, clientId, flowPath);
res.initialVerify();
return res;
}
@@ -200,7 +202,7 @@ public class LoginActionsService {
@GET
public Response restartSession(@QueryParam("client_id") String clientId) {
event.event(EventType.RESTART_AUTHENTICATION);
- SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, null, null, clientId, null);
+ SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, null, null, clientId, null);
AuthenticationSessionModel authSession = checks.initialVerifyAuthSession();
if (authSession == null) {
@@ -286,7 +288,7 @@ public class LoginActionsService {
authSession = processor.getAuthenticationSession(); // Could be changed (eg. Forked flow)
}
- return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, action);
+ return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, action, request);
}
/**
@@ -342,7 +344,7 @@ public class LoginActionsService {
}
authSession = createAuthenticationSessionForClient();
- return processResetCredentials(false, null, authSession);
+ return processResetCredentials(false, null, authSession, null);
}
event.event(EventType.RESET_PASSWORD);
@@ -386,7 +388,7 @@ public class LoginActionsService {
}
- return processResetCredentials(checks.isActionRequest(), execution, authSession);
+ return processResetCredentials(checks.isActionRequest(), execution, authSession, null);
}
/**
@@ -405,7 +407,7 @@ public class LoginActionsService {
return handleActionToken(key, execution, clientId);
}
- protected <T extends DefaultActionToken> Response handleActionToken(String tokenString, String execution, String clientId) {
+ protected <T extends JsonWebToken & ActionTokenKeyModel> Response handleActionToken(String tokenString, String execution, String clientId) {
T token;
ActionTokenHandler<T> handler;
ActionTokenContext<T> tokenContext;
@@ -430,8 +432,8 @@ public class LoginActionsService {
throw new ExplainedTokenVerificationException(null, Errors.NOT_ALLOWED, Messages.INVALID_REQUEST);
}
- TokenVerifier<DefaultActionToken> tokenVerifier = TokenVerifier.create(tokenString, DefaultActionToken.class);
- DefaultActionToken aToken = tokenVerifier.getToken();
+ TokenVerifier<DefaultActionTokenKey> tokenVerifier = TokenVerifier.create(tokenString, DefaultActionTokenKey.class);
+ DefaultActionTokenKey aToken = tokenVerifier.getToken();
event
.detail(Details.TOKEN_ID, aToken.getId())
@@ -469,12 +471,16 @@ public class LoginActionsService {
flowPath = AUTHENTICATE_PATH;
}
AuthenticationProcessor.resetFlow(authSession, flowPath);
- return processAuthentication(false, null, authSession, Messages.LOGIN_TIMEOUT);
+
+ // Process correct flow
+ return processFlowFromPath(flowPath, authSession, Messages.EXPIRED_ACTION_TOKEN_SESSION_EXISTS);
}
- return handleActionTokenVerificationException(null, ex, Errors.EXPIRED_CODE, defaultErrorMessage);
+ return handleActionTokenVerificationException(null, ex, Errors.EXPIRED_CODE, Messages.EXPIRED_ACTION_TOKEN_NO_SESSION);
} catch (ExplainedTokenVerificationException ex) {
return handleActionTokenVerificationException(null, ex, ex.getErrorEvent(), ex.getMessage());
+ } catch (ExplainedVerificationException ex) {
+ return handleActionTokenVerificationException(null, ex, ex.getErrorEvent(), ex.getMessage());
} catch (VerificationException ex) {
return handleActionTokenVerificationException(null, ex, eventError, defaultErrorMessage);
}
@@ -483,7 +489,7 @@ public class LoginActionsService {
tokenContext = new ActionTokenContext(session, realm, uriInfo, clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow);
try {
- String tokenAuthSessionId = handler.getAuthenticationSessionIdFromToken(token);
+ String tokenAuthSessionId = handler.getAuthenticationSessionIdFromToken(token, tokenContext);
if (tokenAuthSessionId != null) {
// This can happen if the token contains ID but user opens the link in a new browser
@@ -539,7 +545,19 @@ public class LoginActionsService {
}
}
- private <T extends DefaultActionToken> ActionTokenHandler<T> resolveActionTokenHandler(String actionId) throws VerificationException {
+ private Response processFlowFromPath(String flowPath, AuthenticationSessionModel authSession, String errorMessage) {
+ if (AUTHENTICATE_PATH.equals(flowPath)) {
+ return processAuthentication(false, null, authSession, errorMessage);
+ } else if (REGISTRATION_PATH.equals(flowPath)) {
+ return processRegistration(false, null, authSession, errorMessage);
+ } else if (RESET_CREDENTIALS_PATH.equals(flowPath)) {
+ return processResetCredentials(false, null, authSession, errorMessage);
+ } else {
+ return ErrorPage.error(session, errorMessage == null ? Messages.INVALID_REQUEST : errorMessage);
+ }
+ }
+
+ private <T extends JsonWebToken> ActionTokenHandler<T> resolveActionTokenHandler(String actionId) throws VerificationException {
if (actionId == null) {
throw new VerificationException("Action token operation not set");
}
@@ -562,10 +580,10 @@ public class LoginActionsService {
return ErrorPage.error(session, errorMessage == null ? Messages.INVALID_CODE : errorMessage);
}
- protected Response processResetCredentials(boolean actionRequest, String execution, AuthenticationSessionModel authSession) {
+ protected Response processResetCredentials(boolean actionRequest, String execution, AuthenticationSessionModel authSession, String errorMessage) {
AuthenticationProcessor authProcessor = new ResetCredentialsActionTokenHandler.ResetCredsAuthenticationProcessor();
- return processFlow(actionRequest, execution, authSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), null, authProcessor);
+ return processFlow(actionRequest, execution, authSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage, authProcessor);
}
@@ -911,7 +929,7 @@ public class LoginActionsService {
throw new RuntimeException("Unreachable");
}
- return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, true);
+ return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, true, request);
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
index 9edc513..e330d29 100644
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
@@ -18,7 +18,6 @@ package org.keycloak.services.resources;
import org.keycloak.TokenVerifier.Predicate;
import org.keycloak.authentication.AuthenticationProcessor;
-import org.keycloak.authentication.actiontoken.DefaultActionToken;
import org.keycloak.authentication.ExplainedVerificationException;
import org.keycloak.authentication.actiontoken.ActionTokenContext;
import org.keycloak.authentication.actiontoken.ExplainedTokenVerificationException;
@@ -152,7 +151,7 @@ public class LoginActionsServiceChecks {
* Verifies whether the user given by ID both exists in the current realm. If yes,
* it optionally also injects the user using the given function (e.g. into session context).
*/
- public static <T extends DefaultActionToken> void checkIsUserValid(T token, ActionTokenContext<T> context) throws VerificationException {
+ public static <T extends JsonWebToken & ActionTokenKeyModel> void checkIsUserValid(T token, ActionTokenContext<T> context) throws VerificationException {
try {
checkIsUserValid(context.getSession(), context.getRealm(), token.getUserId(), context.getAuthenticationSession()::setAuthenticatedUser);
} catch (ExplainedVerificationException ex) {
@@ -178,7 +177,7 @@ public class LoginActionsServiceChecks {
* Verifies whether the client denoted by client ID in token's {@code iss} ({@code issuedFor})
* field both exists and is enabled.
*/
- public static <T extends DefaultActionToken> void checkIsClientValid(T token, ActionTokenContext<T> context) throws VerificationException {
+ public static <T extends JsonWebToken> void checkIsClientValid(T token, ActionTokenContext<T> context) throws VerificationException {
String clientId = token.getIssuedFor();
AuthenticationSessionModel authSession = context.getAuthenticationSession();
ClientModel client = authSession == null ? null : authSession.getClient();
@@ -297,8 +296,9 @@ public class LoginActionsServiceChecks {
return true;
}
- public static <T extends DefaultActionToken> void checkTokenWasNotUsedYet(T token, ActionTokenContext<T> context) throws VerificationException {
+ public static <T extends JsonWebToken & ActionTokenKeyModel> void checkTokenWasNotUsedYet(T token, ActionTokenContext<T> context) throws VerificationException {
ActionTokenStoreProvider actionTokenStore = context.getSession().getProvider(ActionTokenStoreProvider.class);
+
if (actionTokenStore.get(token) != null) {
throw new ExplainedTokenVerificationException(token, Errors.EXPIRED_CODE, Messages.EXPIRED_ACTION);
}
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index bb8de2d..bc3f8dc 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -260,8 +260,7 @@ public class RealmsResource {
WellKnownProvider wellKnown = session.getProvider(WellKnownProvider.class, providerName);
if (wellKnown != null) {
- ResponseBuilder responseBuilder = Response.ok(wellKnown.getConfig())
- .cacheControl(CacheControlUtil.getDefaultCacheControl());
+ ResponseBuilder responseBuilder = Response.ok(wellKnown.getConfig()).cacheControl(CacheControlUtil.noCache());
return Cors.add(request, responseBuilder).allowedOrigins("*").auth().build();
}
diff --git a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
index 0f3ebbe..b5011fb 100644
--- a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
+++ b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
@@ -24,6 +24,7 @@ import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
+import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.ObjectUtil;
@@ -59,6 +60,7 @@ public class SessionCodeChecks {
private final RealmModel realm;
private final UriInfo uriInfo;
+ private final HttpRequest request;
private final ClientConnection clientConnection;
private final KeycloakSession session;
private final EventBuilder event;
@@ -69,9 +71,10 @@ public class SessionCodeChecks {
private final String flowPath;
- public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, String code, String execution, String clientId, String flowPath) {
+ public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, String code, String execution, String clientId, String flowPath) {
this.realm = realm;
this.uriInfo = uriInfo;
+ this.request = request;
this.clientConnection = clientConnection;
this.session = session;
this.event = event;
@@ -220,10 +223,17 @@ public class SessionCodeChecks {
}
}
- if (ObjectUtil.isEqualOrBothNull(execution, lastExecFromSession)) {
+ if (execution == null || execution.equals(lastExecFromSession)) {
// Allow refresh of previous page
clientCode = new ClientSessionCode<>(session, realm, authSession);
actionRequest = false;
+
+ // Allow refresh, but rewrite browser history
+ if (execution == null && lastExecFromSession != null) {
+ logger.debugf("Parameter 'execution' is not in the request, but flow wasn't changed. Will update browser history");
+ request.setAttribute(BrowserHistoryHelper.SHOULD_UPDATE_BROWSER_HISTORY, true);
+ }
+
return true;
} else {
response = showPageExpired(authSession);
diff --git a/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java b/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java
index ef34b16..6832f10 100644
--- a/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java
+++ b/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java
@@ -24,6 +24,7 @@ import java.util.regex.Pattern;
import javax.ws.rs.core.Response;
import org.jboss.logging.Logger;
+import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.models.KeycloakSession;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.theme.BrowserSecurityHeaderSetup;
@@ -44,13 +45,27 @@ import org.keycloak.utils.MediaType;
*/
public abstract class BrowserHistoryHelper {
+ // Request attribute, which specifies if flow was changed in this request (eg. click "register" from the login screen)
+ public static final String SHOULD_UPDATE_BROWSER_HISTORY = "SHOULD_UPDATE_BROWSER_HISTORY";
+
protected static final Logger logger = Logger.getLogger(BrowserHistoryHelper.class);
- public abstract Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest);
+ public abstract Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest, HttpRequest httpRequest);
public abstract Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession);
+ protected boolean shouldReplaceBrowserHistory(boolean actionRequest, HttpRequest httpRequest) {
+ if (actionRequest) {
+ return true;
+ }
+
+ Boolean flowChanged = (Boolean) httpRequest.getAttribute(SHOULD_UPDATE_BROWSER_HISTORY);
+ return (flowChanged != null && flowChanged);
+ }
+
+
+
// Always rely on javascript for now
public static BrowserHistoryHelper getInstance() {
return new JavascriptHistoryReplace();
@@ -66,8 +81,8 @@ public abstract class BrowserHistoryHelper {
private static final Pattern HEAD_END_PATTERN = Pattern.compile("</[hH][eE][aA][dD]>");
@Override
- public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) {
- if (!actionRequest) {
+ public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest, HttpRequest httpRequest) {
+ if (!shouldReplaceBrowserHistory(actionRequest, httpRequest)) {
return response;
}
@@ -129,8 +144,8 @@ public abstract class BrowserHistoryHelper {
private static final String CACHED_RESPONSE = "cached.response";
@Override
- public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) {
- if (!actionRequest) {
+ public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest, HttpRequest httpRequest) {
+ if (!shouldReplaceBrowserHistory(actionRequest, httpRequest)) {
return response;
}
@@ -179,7 +194,7 @@ public abstract class BrowserHistoryHelper {
private static class NoOpHelper extends BrowserHistoryHelper {
@Override
- public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) {
+ public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest, HttpRequest httpRequest) {
return response;
}
diff --git a/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java
new file mode 100755
index 0000000..c7583b7
--- /dev/null
+++ b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.social.bitbucket;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
+import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
+import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
+import org.keycloak.broker.oidc.util.JsonSimpleHttp;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.provider.IdentityBrokerException;
+import org.keycloak.broker.provider.util.SimpleHttp;
+import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class BitbucketIdentityProvider extends AbstractOAuth2IdentityProvider implements SocialIdentityProvider {
+
+ public static final String AUTH_URL = "https://bitbucket.org/site/oauth2/authorize";
+ public static final String TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token";
+ public static final String USER_URL = "https://api.bitbucket.org/2.0/user";
+ public static final String EMAIL_SCOPE = "email";
+ public static final String ACCOUNT_SCOPE = "account";
+ public static final String DEFAULT_SCOPE = ACCOUNT_SCOPE;
+
+ public BitbucketIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
+ super(session, config);
+ config.setAuthorizationUrl(AUTH_URL);
+ config.setTokenUrl(TOKEN_URL);
+ String defaultScope = config.getDefaultScope();
+
+ if (defaultScope == null || defaultScope.trim().equals("")) {
+ config.setDefaultScope(ACCOUNT_SCOPE);
+ }
+ }
+
+ @Override
+ protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
+ try {
+ JsonNode profile = JsonSimpleHttp.asJson(SimpleHttp.doGet(USER_URL, session).header("Authorization", "Bearer " + accessToken));
+
+ String type = getJsonProperty(profile, "type");
+ if (type == null) {
+ throw new IdentityBrokerException("Could not obtain account information from bitbucket.");
+
+ }
+ if (type.equals("error")) {
+ JsonNode errorNode = profile.get("error");
+ if (errorNode != null) {
+ String errorMsg = getJsonProperty(errorNode, "message");
+ throw new IdentityBrokerException("Could not obtain account information from bitbucket. Error: " + errorMsg);
+ } else {
+ throw new IdentityBrokerException("Could not obtain account information from bitbucket.");
+ }
+ }
+ if (!type.equals("user")) {
+ logger.debug("Unknown object type: " + type);
+ throw new IdentityBrokerException("Could not obtain account information from bitbucket.");
+
+ }
+ BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "account_id"));
+
+ String username = getJsonProperty(profile, "username");
+ user.setUsername(username);
+ user.setIdpConfig(getConfig());
+ user.setIdp(this);
+
+ AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+
+ return user;
+ } catch (Exception e) {
+ if (e instanceof IdentityBrokerException) throw (IdentityBrokerException)e;
+ throw new IdentityBrokerException("Could not obtain user profile from github.", e);
+ }
+ }
+
+ @Override
+ protected String getDefaultScopes() {
+ return DEFAULT_SCOPE;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProviderFactory.java
new file mode 100755
index 0000000..b736d74
--- /dev/null
+++ b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProviderFactory.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.social.bitbucket;
+
+import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
+import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
+import org.keycloak.broker.social.SocialIdentityProviderFactory;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author Pedro Igor
+ */
+public class BitbucketIdentityProviderFactory extends AbstractIdentityProviderFactory<BitbucketIdentityProvider> implements SocialIdentityProviderFactory<BitbucketIdentityProvider> {
+
+ public static final String PROVIDER_ID = "bitbucket";
+
+ @Override
+ public String getName() {
+ return "BitBucket";
+ }
+
+ @Override
+ public BitbucketIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+ return new BitbucketIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java
new file mode 100755
index 0000000..a57704f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.social.gitlab;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
+import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
+import org.keycloak.broker.oidc.OIDCIdentityProvider;
+import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
+import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
+import org.keycloak.broker.oidc.util.JsonSimpleHttp;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.provider.IdentityBrokerException;
+import org.keycloak.broker.provider.util.SimpleHttp;
+import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.JsonWebToken;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class GitLabIdentityProvider extends OIDCIdentityProvider implements SocialIdentityProvider<OIDCIdentityProviderConfig> {
+
+ public static final String AUTH_URL = "https://gitlab.com/oauth/authorize";
+ public static final String TOKEN_URL = "https://gitlab.com/oauth/token";
+ public static final String USER_INFO = "https://gitlab.com/api/v4/user";
+ public static final String API_SCOPE = "api";
+
+ public GitLabIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
+ super(session, config);
+ config.setAuthorizationUrl(AUTH_URL);
+ config.setTokenUrl(TOKEN_URL);
+ config.setUserInfoUrl(USER_INFO);
+
+ String defaultScope = config.getDefaultScope();
+
+ if (defaultScope.equals(SCOPE_OPENID)) {
+ config.setDefaultScope((API_SCOPE + " " + defaultScope).trim());
+ }
+ }
+
+ protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
+ String id = idToken.getSubject();
+ BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
+ String name = (String)idToken.getOtherClaims().get(IDToken.NAME);
+ String preferredUsername = (String)idToken.getOtherClaims().get(IDToken.NICKNAME);
+ String email = (String)idToken.getOtherClaims().get(IDToken.EMAIL);
+
+ if (getConfig().getDefaultScope().contains(API_SCOPE)) {
+ String userInfoUrl = getUserInfoUrl();
+ if (userInfoUrl != null && !userInfoUrl.isEmpty() && (id == null || name == null || preferredUsername == null || email == null)) {
+ SimpleHttp request = JsonSimpleHttp.doGet(userInfoUrl, session)
+ .header("Authorization", "Bearer " + accessToken);
+ JsonNode userInfo = JsonSimpleHttp.asJson(request);
+
+ name = getJsonProperty(userInfo, "name");
+ preferredUsername = getJsonProperty(userInfo, "username");
+ email = getJsonProperty(userInfo, "email");
+ AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
+ }
+ }
+ identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
+ identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
+ processAccessTokenResponse(identity, tokenResponse);
+
+ identity.setId(id);
+ identity.setName(name);
+ identity.setEmail(email);
+
+ identity.setBrokerUserId(getConfig().getAlias() + "." + id);
+ if (tokenResponse.getSessionState() != null) {
+ identity.setBrokerSessionId(getConfig().getAlias() + "." + tokenResponse.getSessionState());
+ }
+
+ if (preferredUsername == null) {
+ preferredUsername = email;
+ }
+
+ if (preferredUsername == null) {
+ preferredUsername = id;
+ }
+
+ identity.setUsername(preferredUsername);
+ return identity;
+ }
+
+
+
+
+}
diff --git a/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProviderFactory.java
new file mode 100755
index 0000000..35e7a5e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProviderFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.social.gitlab;
+
+import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
+import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
+import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
+import org.keycloak.broker.social.SocialIdentityProviderFactory;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author Pedro Igor
+ */
+public class GitLabIdentityProviderFactory extends AbstractIdentityProviderFactory<GitLabIdentityProvider> implements SocialIdentityProviderFactory<GitLabIdentityProvider> {
+
+ public static final String PROVIDER_ID = "gitlab";
+
+ @Override
+ public String getName() {
+ return "GitLab";
+ }
+
+ @Override
+ public GitLabIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+ return new GitLabIdentityProvider(session, new OIDCIdentityProviderConfig(model));
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+}
diff --git a/services/src/main/resources/DockerComposeYamlReadme.md b/services/src/main/resources/DockerComposeYamlReadme.md
new file mode 100644
index 0000000..84dff48
--- /dev/null
+++ b/services/src/main/resources/DockerComposeYamlReadme.md
@@ -0,0 +1,23 @@
+# Docker Compose YAML Installation
+-----------------------------------
+
+*NOTE:* This installation method is intended for development use only. Please don't ever let this anywhere near prod!
+
+## Keycloak Realm Assumptions:
+ - Client configuration has not changed since the installtion files were generated. If you change your client configuration, be sure to grab a re-generated installtion .zip from the 'Installation' tab.
+ - Keycloak server is started with the 'docker' feature enabled. I.E. -Dkeycloak.profile.feature.docker=enabled
+
+## Running the Installation:
+ - Spin up a fully functional docker registry with:
+
+ docker-compose up
+
+ - Now you can login against the registry and perform normal operations:
+
+ docker login -u $username -p $password localhost:5000
+
+ docker pull centos:7
+ docker tag centos:7 localhost:5000/centos:7
+ docker push localhost:5000/centos:7
+
+ ** Remember that users for the `docker login` command must be configured and available in the keycloak realm that hosts the docker client.
\ No newline at end of file
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
index 208f16d..2b11382 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
@@ -34,6 +34,7 @@ org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFac
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory
org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory
-org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator
+org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFactory
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
+org.keycloak.protocol.docker.DockerAuthenticatorFactory
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory
index 00a5e51..f2861ab 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory
@@ -23,3 +23,5 @@ org.keycloak.social.stackoverflow.StackoverflowIdentityProviderFactory
org.keycloak.social.twitter.TwitterIdentityProviderFactory
org.keycloak.social.microsoft.MicrosoftIdentityProviderFactory
org.keycloak.social.openshift.OpenshiftV3IdentityProviderFactory
+org.keycloak.social.gitlab.GitLabIdentityProviderFactory
+org.keycloak.social.bitbucket.BitbucketIdentityProviderFactory
\ No newline at end of file
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider
index a0d8052..f38a5c2 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider
+++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider
@@ -22,4 +22,6 @@ org.keycloak.protocol.saml.installation.SamlSPDescriptorClientInstallation
org.keycloak.protocol.saml.installation.SamlIDPDescriptorClientInstallation
org.keycloak.protocol.saml.installation.ModAuthMellonClientInstallation
org.keycloak.protocol.saml.installation.KeycloakSamlSubsystemInstallation
-
+org.keycloak.protocol.docker.installation.DockerVariableOverrideInstallationProvider
+org.keycloak.protocol.docker.installation.DockerRegistryConfigFileInstallationProvider
+org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory b/services/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory
index 38e1b5a..e954f2e 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory
@@ -16,4 +16,5 @@
#
org.keycloak.protocol.oidc.OIDCLoginProtocolFactory
-org.keycloak.protocol.saml.SamlProtocolFactory
\ No newline at end of file
+org.keycloak.protocol.saml.SamlProtocolFactory
+org.keycloak.protocol.docker.DockerAuthV2ProtocolFactory
\ No newline at end of file
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
index 04f090e..95b79cf 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
+++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
@@ -35,4 +35,5 @@ org.keycloak.protocol.saml.mappers.GroupMembershipMapper
org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper
org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper
org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper
+org.keycloak.protocol.docker.mapper.AllowAllDockerProtocolMapper
diff --git a/services/src/main/resources/scripts/authenticator-template.js b/services/src/main/resources/scripts/authenticator-template.js
index 514337d..53f1c13 100644
--- a/services/src/main/resources/scripts/authenticator-template.js
+++ b/services/src/main/resources/scripts/authenticator-template.js
@@ -15,7 +15,7 @@ AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationF
* session - current KeycloakSession {@see org.keycloak.models.KeycloakSession}
* httpRequest - current HttpRequest {@see org.jboss.resteasy.spi.HttpRequest}
* script - current script {@see org.keycloak.models.ScriptModel}
- * authenticationSession - current client session {@see org.keycloak.sessions.AuthenticationSessionModel}
+ * authenticationSession - current authentication session {@see org.keycloak.sessions.AuthenticationSessionModel}
* LOG - current logger {@see org.jboss.logging.Logger}
*
* You one can extract current http request headers via:
diff --git a/services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java
new file mode 100644
index 0000000..a5f494c
--- /dev/null
+++ b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java
@@ -0,0 +1,193 @@
+package org.keycloak.procotol.docker.installation;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.keycloak.common.util.CertificateUtils;
+import org.keycloak.common.util.PemUtils;
+import org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider;
+
+import javax.ws.rs.core.Response;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Optional;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.junit.Assert.fail;
+import static org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider.ROOT_DIR;
+
+public class DockerComposeYamlInstallationProviderTest {
+
+ DockerComposeYamlInstallationProvider installationProvider;
+ static Certificate certificate;
+
+ @BeforeClass
+ public static void setUp_beforeClass() throws NoSuchAlgorithmException {
+ final KeyPairGenerator keyGen;
+ keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048, new SecureRandom());
+
+ final KeyPair keypair = keyGen.generateKeyPair();
+ certificate = CertificateUtils.generateV1SelfSignedCertificate(keypair, "test-realm");
+ }
+
+ @Before
+ public void setUp() {
+ installationProvider = new DockerComposeYamlInstallationProvider();
+ }
+
+ private Response fireInstallationProvider() throws IOException {
+ ByteArrayOutputStream byteStream = null;
+ ZipOutputStream zipOutput = null;
+ byteStream = new ByteArrayOutputStream();
+ zipOutput = new ZipOutputStream(byteStream);
+
+ return installationProvider.generateInstallation(zipOutput, byteStream, certificate, new URL("http://localhost:8080/auth"), "docker-test", "docker-registry");
+ }
+
+ @Test
+ @Ignore // Used only for smoke testing
+ public void writeToRealZip() throws IOException {
+ final Response response = fireInstallationProvider();
+ final byte[] responseBytes = (byte[]) response.getEntity();
+ FileUtils.writeByteArrayToFile(new File("target/keycloak-docker-compose-yaml.zip"), responseBytes);
+ }
+
+ @Test
+ public void testAllTheZipThings() throws Exception {
+ final Response response = fireInstallationProvider();
+ assertThat("compose YAML returned non-ok response", response.getStatus(), equalTo(Response.Status.OK.getStatusCode()));
+
+ shouldIncludeDockerComposeYamlInZip(getZipResponseFromInstallProvider(response));
+ shouldIncludeReadmeInZip(getZipResponseFromInstallProvider(response));
+ shouldWriteBlankDataDirectoryInZip(getZipResponseFromInstallProvider(response));
+ shouldWriteCertDirectoryInZip(getZipResponseFromInstallProvider(response));
+ shouldWriteSslCertificateInZip(getZipResponseFromInstallProvider(response));
+ shouldWritePrivateKeyInZip(getZipResponseFromInstallProvider(response));
+ }
+
+ public void shouldIncludeDockerComposeYamlInZip(ZipInputStream zipInput) throws Exception {
+ final Optional<String> dockerComposeFileContents = getFileContents(zipInput, ROOT_DIR + "docker-compose.yaml");
+
+ assertThat("Could not find docker-compose.yaml file in zip archive response", dockerComposeFileContents.isPresent(), equalTo(true));
+ final boolean zipFileContentEqualsTestFile = IOUtils.contentEquals(new ByteArrayInputStream(dockerComposeFileContents.get().getBytes()), new FileInputStream("src/test/resources/docker-compose-expected.yaml"));
+ assertThat("Invalid docker-compose file contents: \n" + dockerComposeFileContents.get(), zipFileContentEqualsTestFile, equalTo(true));
+ }
+
+ public void shouldIncludeReadmeInZip(ZipInputStream zipInput) throws Exception {
+ final Optional<String> dockerComposeFileContents = getFileContents(zipInput, ROOT_DIR + "README.md");
+
+ assertThat("Could not find README.md file in zip archive response", dockerComposeFileContents.isPresent(), equalTo(true));
+ }
+
+ public void shouldWriteBlankDataDirectoryInZip(ZipInputStream zipInput) throws Exception {
+ ZipEntry zipEntry;
+ boolean dataDirFound = false;
+ while ((zipEntry = zipInput.getNextEntry()) != null) {
+ try {
+ if (zipEntry.getName().equals(ROOT_DIR + "data/")) {
+ dataDirFound = true;
+ assertThat("Zip entry for data directory is not the correct type", zipEntry.isDirectory(), equalTo(true));
+ }
+ } finally {
+ zipInput.closeEntry();
+ }
+ }
+
+ assertThat("Could not find data directory", dataDirFound, equalTo(true));
+ }
+
+ public void shouldWriteCertDirectoryInZip(ZipInputStream zipInput) throws Exception {
+ ZipEntry zipEntry;
+ boolean certsDirFound = false;
+ while ((zipEntry = zipInput.getNextEntry()) != null) {
+ try {
+ if (zipEntry.getName().equals(ROOT_DIR + "certs/")) {
+ certsDirFound = true;
+ assertThat("Zip entry for cert directory is not the correct type", zipEntry.isDirectory(), equalTo(true));
+ }
+ } finally {
+ zipInput.closeEntry();
+ }
+ }
+
+ assertThat("Could not find cert directory", certsDirFound, equalTo(true));
+ }
+
+ public void shouldWriteSslCertificateInZip(ZipInputStream zipInput) throws Exception {
+ final Optional<String> localhostCertificateFileContents = getFileContents(zipInput, ROOT_DIR + "certs/localhost.crt");
+
+ assertThat("Could not find localhost certificate", localhostCertificateFileContents.isPresent(), equalTo(true));
+ final X509Certificate x509Certificate = PemUtils.decodeCertificate(localhostCertificateFileContents.get());
+ assertThat("Invalid x509 given by docker-compose YAML", x509Certificate, notNullValue());
+ }
+
+ public void shouldWritePrivateKeyInZip(ZipInputStream zipInput) throws Exception {
+ final Optional<String> localhostPrivateKeyFileContents = getFileContents(zipInput, ROOT_DIR + "certs/localhost.key");
+
+ assertThat("Could not find localhost private key", localhostPrivateKeyFileContents.isPresent(), equalTo(true));
+ final PrivateKey privateKey = PemUtils.decodePrivateKey(localhostPrivateKeyFileContents.get());
+ assertThat("Invalid private Key given by docker-compose YAML", privateKey, notNullValue());
+ }
+
+ private ZipInputStream getZipResponseFromInstallProvider(Response response) throws IOException {
+ final Object responseEntity = response.getEntity();
+ if (!(responseEntity instanceof byte[])) {
+ fail("Recieved non-byte[] entity for docker-compose YAML installation response");
+ }
+
+ return new ZipInputStream(new ByteArrayInputStream((byte[]) responseEntity));
+ }
+
+ private static Optional<String> getFileContents(final ZipInputStream zipInputStream, final String fileName) throws IOException {
+ ZipEntry zipEntry;
+ while ((zipEntry = zipInputStream.getNextEntry()) != null) {
+ try {
+ if (zipEntry.getName().equals(fileName)) {
+ return Optional.of(readBytesToString(zipInputStream));
+ }
+ } finally {
+ zipInputStream.closeEntry();
+ }
+ }
+
+ // fall-through case if file name not found:
+ return Optional.empty();
+ }
+
+ private static String readBytesToString(final InputStream inputStream) throws IOException {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ final byte[] buffer = new byte[4096];
+ int bytesRead;
+
+ try {
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ output.write(buffer, 0, bytesRead);
+ }
+ } finally {
+ output.close();
+ }
+
+ return new String(output.toByteArray());
+ }
+}
diff --git a/services/src/test/java/org/keycloak/procotol/docker/installation/DockerKeyIdentifierTest.java b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerKeyIdentifierTest.java
new file mode 100644
index 0000000..0fa8cb9
--- /dev/null
+++ b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerKeyIdentifierTest.java
@@ -0,0 +1,41 @@
+package org.keycloak.procotol.docker.installation;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.models.utils.Base32;
+import org.keycloak.protocol.docker.DockerKeyIdentifier;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Docker gets really unhappy if the key identifier is not in the format documented here:
+ * @see https://github.com/docker/libtrust/blob/master/key.go#L24
+ */
+public class DockerKeyIdentifierTest {
+
+ String keyIdentifierString;
+ PublicKey publicKey;
+
+ @Before
+ public void shouldBlah() throws Exception {
+ final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048, new SecureRandom());
+
+ final KeyPair keypair = keyGen.generateKeyPair();
+ publicKey = keypair.getPublic();
+ final DockerKeyIdentifier identifier = new DockerKeyIdentifier(publicKey);
+ keyIdentifierString = identifier.toString();
+ }
+
+ @Test
+ public void shoulProduceExpectedKeyFormat() {
+ assertThat("Every 4 chars are not delimted by colon", keyIdentifierString.matches("([\\w]{4}:){11}[\\w]{4}"), equalTo(true));
+ }
+}
diff --git a/services/src/test/java/org/keycloak/test/broker/saml/SAMLDataMarshallerTest.java b/services/src/test/java/org/keycloak/test/broker/saml/SAMLDataMarshallerTest.java
index 9a68621..c8647f3 100755
--- a/services/src/test/java/org/keycloak/test/broker/saml/SAMLDataMarshallerTest.java
+++ b/services/src/test/java/org/keycloak/test/broker/saml/SAMLDataMarshallerTest.java
@@ -25,17 +25,22 @@ import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
+import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
+import java.io.InputStream;
+import org.hamcrest.CoreMatchers;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertThat;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SAMLDataMarshallerTest {
- private static final String TEST_RESPONSE = "<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_4804cf50-cd96-4b92-823e-89adaa0c78ba\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.920Z\" Destination=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\" InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"></samlp:StatusCode></samlp:Status><saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.911Z\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><saml:Subject><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">test-user</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\" NotOnOrAfter=\"2015-11-06T11:05:31.911Z\" Recipient=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\"></saml:SubjectConfirmationData></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"2015-11-06T11:00:31.911Z\" NotOnOrAfter=\"2015-11-06T11:01:31.911Z\"><saml:AudienceRestriction><saml:Audience>http://localhost:8081/auth/realms/realm-with-broker</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name=\"mobile\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">617-666-7777</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"urn:oid:1.2.840.113549.1.9.1\" FriendlyName=\"email\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">test-user@localhost</saml:AttributeValue></saml:Attribute></saml:AttributeStatement><saml:AttributeStatement><saml:Attribute Name=\"Role\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">manager</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>";
+ private static final String TEST_RESPONSE = "<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_4804cf50-cd96-4b92-823e-89adaa0c78ba\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.920Z\" Destination=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\" InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status><saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.911Z\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><saml:Subject><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">test-user</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\" NotOnOrAfter=\"2015-11-06T11:05:31.911Z\" Recipient=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"2015-11-06T11:00:31.911Z\" NotOnOrAfter=\"2015-11-06T11:01:31.911Z\"><saml:AudienceRestriction><saml:Audience>http://localhost:8081/auth/realms/realm-with-broker</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name=\"mobile\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">617-666-7777</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"urn:oid:1.2.840.113549.1.9.1\" FriendlyName=\"email\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">test-user@localhost</saml:AttributeValue></saml:Attribute></saml:AttributeStatement><saml:AttributeStatement><saml:Attribute Name=\"Role\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">manager</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>";
- private static final String TEST_ASSERTION = "<saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.911Z\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><saml:Subject><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">test-user</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\" NotOnOrAfter=\"2015-11-06T11:05:31.911Z\" Recipient=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\"></saml:SubjectConfirmationData></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"2015-11-06T11:00:31.911Z\" NotOnOrAfter=\"2015-11-06T11:01:31.911Z\"><saml:AudienceRestriction><saml:Audience>http://localhost:8081/auth/realms/realm-with-broker</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name=\"mobile\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">617-666-7777</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"urn:oid:1.2.840.113549.1.9.1\" FriendlyName=\"email\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">test-user@localhost</saml:AttributeValue></saml:Attribute></saml:AttributeStatement><saml:AttributeStatement><saml:Attribute Name=\"Role\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">manager</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion>";
+ private static final String TEST_ASSERTION = "<saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.911Z\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><saml:Subject><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">test-user</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\" NotOnOrAfter=\"2015-11-06T11:05:31.911Z\" Recipient=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"2015-11-06T11:00:31.911Z\" NotOnOrAfter=\"2015-11-06T11:01:31.911Z\"><saml:AudienceRestriction><saml:Audience>http://localhost:8081/auth/realms/realm-with-broker</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name=\"mobile\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">617-666-7777</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"urn:oid:1.2.840.113549.1.9.1\" FriendlyName=\"email\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">test-user@localhost</saml:AttributeValue></saml:Attribute></saml:AttributeStatement><saml:AttributeStatement><saml:Attribute Name=\"Role\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">manager</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion>";
- private static final String TEST_ASSERTION_WITH_NAME_ID = "<saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.911Z\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><saml:Subject><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">test-user</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\" NotOnOrAfter=\"2015-11-06T11:05:31.911Z\" Recipient=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\"></saml:SubjectConfirmationData></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"2015-11-06T11:00:31.911Z\" NotOnOrAfter=\"2015-11-06T11:01:31.911Z\"><saml:AudienceRestriction><saml:Audience>http://localhost:8081/auth/realms/realm-with-broker</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name=\"mobile\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">617-666-7777</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"urn:oid:1.2.840.113549.1.9.1\" FriendlyName=\"email\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">test-user@localhost</saml:AttributeValue></saml:Attribute></saml:AttributeStatement><saml:AttributeStatement><saml:Attribute Name=\"Role\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\">b2c6275838784dba219c92f53ea5493c8ef4da09</saml:NameID></saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion>";
+ private static final String TEST_ASSERTION_WITH_NAME_ID = "<saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.911Z\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><saml:Subject><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">test-user</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\" NotOnOrAfter=\"2015-11-06T11:05:31.911Z\" Recipient=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"2015-11-06T11:00:31.911Z\" NotOnOrAfter=\"2015-11-06T11:01:31.911Z\"><saml:AudienceRestriction><saml:Audience>http://localhost:8081/auth/realms/realm-with-broker</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name=\"mobile\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">617-666-7777</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"urn:oid:1.2.840.113549.1.9.1\" FriendlyName=\"email\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">test-user@localhost</saml:AttributeValue></saml:Attribute></saml:AttributeStatement><saml:AttributeStatement><saml:Attribute Name=\"Role\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\">b2c6275838784dba219c92f53ea5493c8ef4da09</saml:NameID></saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion>";
private static final String TEST_AUTHN_TYPE = "<saml:AuthnStatement xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement>";
@@ -95,4 +100,40 @@ public class SAMLDataMarshallerTest {
String serialized = serializer.serialize(authnStatement);
Assert.assertEquals(TEST_AUTHN_TYPE, serialized);
}
+
+ @Test
+ public void testSerializeWithNamespaceInSignatureElement() throws Exception {
+ SAMLParser parser = new SAMLParser();
+ try (InputStream st = SAMLDataMarshallerTest.class.getResourceAsStream("saml-response-ds-ns-in-signature.xml")) {
+ Object parsedObject = parser.parse(st);
+ assertThat(parsedObject, instanceOf(ResponseType.class));
+
+ ResponseType response = (ResponseType) parsedObject;
+
+ SAMLDataMarshaller serializer = new SAMLDataMarshaller();
+ String serialized = serializer.serialize(response.getAssertions().get(0).getAssertion());
+
+ AssertionType deserialized = serializer.deserialize(serialized, AssertionType.class);
+ assertThat(deserialized, CoreMatchers.notNullValue());
+ assertThat(deserialized.getID(), CoreMatchers.is("id-4r-Xj702KQsM0gJyu3Fqpuwfe-LvDrEcQZpxKrhC"));
+ }
+ }
+
+ @Test
+ public void testSerializeWithNamespaceNotInSignatureElement() throws Exception {
+ SAMLParser parser = new SAMLParser();
+ try (InputStream st = SAMLDataMarshallerTest.class.getResourceAsStream("saml-response-ds-ns-above-signature.xml")) {
+ Object parsedObject = parser.parse(st);
+ assertThat(parsedObject, instanceOf(ResponseType.class));
+
+ ResponseType response = (ResponseType) parsedObject;
+
+ SAMLDataMarshaller serializer = new SAMLDataMarshaller();
+ String serialized = serializer.serialize(response.getAssertions().get(0).getAssertion());
+
+ AssertionType deserialized = serializer.deserialize(serialized, AssertionType.class);
+ assertThat(deserialized, CoreMatchers.notNullValue());
+ assertThat(deserialized.getID(), CoreMatchers.is("id-4r-Xj702KQsM0gJyu3Fqpuwfe-LvDrEcQZpxKrhC"));
+ }
+ }
}
diff --git a/services/src/test/resources/docker-compose-expected.yaml b/services/src/test/resources/docker-compose-expected.yaml
new file mode 100644
index 0000000..3c912de
--- /dev/null
+++ b/services/src/test/resources/docker-compose-expected.yaml
@@ -0,0 +1,15 @@
+registry:
+ image: registry:2
+ ports:
+ - 127.0.0.1:5000:5000
+ environment:
+ REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
+ REGISTRY_HTTP_TLS_CERTIFICATE: /opt/certs/localhost.crt
+ REGISTRY_HTTP_TLS_KEY: /opt/certs/localhost.key
+ REGISTRY_AUTH_TOKEN_REALM: http://localhost:8080/auth/realms/docker-test/protocol/docker-v2/auth
+ REGISTRY_AUTH_TOKEN_SERVICE: docker-registry
+ REGISTRY_AUTH_TOKEN_ISSUER: http://localhost:8080/auth/realms/docker-test
+ REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /opt/certs/localhost_trust_chain.pem
+ volumes:
+ - ./data:/data:z
+ - ./certs:/opt/certs:z
\ No newline at end of file
diff --git a/services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-above-signature.xml b/services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-above-signature.xml
new file mode 100644
index 0000000..dfa74aa
--- /dev/null
+++ b/services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-above-signature.xml
@@ -0,0 +1,89 @@
+<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
+ xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"
+ xmlns:enc="http://www.w3.org/2001/04/xmlenc#"
+ xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+ xmlns:x500="urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ Destination="http://SERVER/auth/realms/MY_REALM/broker/saml/endpoint"
+ ID="id-EYgqtumZ-P-Ph7t37f-brUKMwB5MKix0sNjr-0YV"
+ IssueInstant="2017-06-28T03:34:26Z"
+ Version="2.0">
+ <saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">SSO</saml:Issuer>
+ <samlp:Status>
+ <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
+ </samlp:Status>
+ <saml:Assertion ID="id-4r-Xj702KQsM0gJyu3Fqpuwfe-LvDrEcQZpxKrhC"
+ IssueInstant="2017-06-28T03:34:26Z"
+ Version="2.0"
+ >
+ <saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">SSO</saml:Issuer>
+ <dsig:Signature>
+ <dsig:SignedInfo>
+ <dsig:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ <dsig:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
+ <dsig:Reference URI="#id-4r-Xj702KQsM0gJyu3Fqpuwfe-LvDrEcQZpxKrhC">
+ <dsig:Transforms>
+ <dsig:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
+ <dsig:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ </dsig:Transforms>
+ <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
+ <dsig:DigestValue>DIGEST</dsig:DigestValue>
+ </dsig:Reference>
+ </dsig:SignedInfo>
+ <dsig:SignatureValue>SIG_VAL</dsig:SignatureValue>
+ </dsig:Signature>
+ <saml:Subject>
+ <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">my_email@my_provider.com</saml:NameID>
+ <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+ <saml:SubjectConfirmationData NotOnOrAfter="2017-06-28T03:39:26Z"
+ Recipient="http://SERVER/auth/realms/MY_REALM/broker/saml/endpoint"
+ />
+ </saml:SubjectConfirmation>
+ </saml:Subject>
+ <saml:Conditions NotBefore="2017-06-28T03:34:26Z"
+ NotOnOrAfter="2017-06-28T03:39:26Z"
+ >
+ <saml:AudienceRestriction>
+ <saml:Audience>http://SERVER/auth/realms/MY_REALM</saml:Audience>
+ </saml:AudienceRestriction>
+ </saml:Conditions>
+ <saml:AuthnStatement AuthnInstant="2017-06-28T03:34:26Z"
+ SessionIndex="id-4efQg54WPFyzSEPsepFVZSb8KmeVYtonQNZO1iAE"
+ SessionNotOnOrAfter="2017-06-28T04:34:26Z"
+ >
+ <saml:AuthnContext>
+ <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
+ </saml:AuthnContext>
+ </saml:AuthnStatement>
+ <saml:AttributeStatement>
+ <saml:Attribute Name="lastName"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
+ >
+ <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xsi:type="xs:string"
+ >Yadav</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="username"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
+ >
+ <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xsi:type="xs:string"
+ >H183561</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="email"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
+ >
+ <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xsi:type="xs:string"
+ >my_email@my_provider.com</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="firstName"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
+ >
+ <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xsi:type="xs:string"
+ >MY_NAME</saml:AttributeValue>
+ </saml:Attribute>
+ </saml:AttributeStatement>
+ </saml:Assertion>
+</samlp:Response>
diff --git a/services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-in-signature.xml b/services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-in-signature.xml
new file mode 100644
index 0000000..8460b8e
--- /dev/null
+++ b/services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-in-signature.xml
@@ -0,0 +1,88 @@
+<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
+ xmlns:enc="http://www.w3.org/2001/04/xmlenc#"
+ xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+ xmlns:x500="urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ Destination="http://SERVER/auth/realms/MY_REALM/broker/saml/endpoint"
+ ID="id-EYgqtumZ-P-Ph7t37f-brUKMwB5MKix0sNjr-0YV"
+ IssueInstant="2017-06-28T03:34:26Z"
+ Version="2.0">
+ <saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">SSO</saml:Issuer>
+ <samlp:Status>
+ <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
+ </samlp:Status>
+ <saml:Assertion ID="id-4r-Xj702KQsM0gJyu3Fqpuwfe-LvDrEcQZpxKrhC"
+ IssueInstant="2017-06-28T03:34:26Z"
+ Version="2.0"
+ >
+ <saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">SSO</saml:Issuer>
+ <dsig:Signature xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
+ <dsig:SignedInfo>
+ <dsig:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ <dsig:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
+ <dsig:Reference URI="#id-4r-Xj702KQsM0gJyu3Fqpuwfe-LvDrEcQZpxKrhC">
+ <dsig:Transforms>
+ <dsig:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
+ <dsig:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ </dsig:Transforms>
+ <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
+ <dsig:DigestValue>DIGEST</dsig:DigestValue>
+ </dsig:Reference>
+ </dsig:SignedInfo>
+ <dsig:SignatureValue>SIG_VAL</dsig:SignatureValue>
+ </dsig:Signature>
+ <saml:Subject>
+ <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">my_email@my_provider.com</saml:NameID>
+ <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+ <saml:SubjectConfirmationData NotOnOrAfter="2017-06-28T03:39:26Z"
+ Recipient="http://SERVER/auth/realms/MY_REALM/broker/saml/endpoint"
+ />
+ </saml:SubjectConfirmation>
+ </saml:Subject>
+ <saml:Conditions NotBefore="2017-06-28T03:34:26Z"
+ NotOnOrAfter="2017-06-28T03:39:26Z"
+ >
+ <saml:AudienceRestriction>
+ <saml:Audience>http://SERVER/auth/realms/MY_REALM</saml:Audience>
+ </saml:AudienceRestriction>
+ </saml:Conditions>
+ <saml:AuthnStatement AuthnInstant="2017-06-28T03:34:26Z"
+ SessionIndex="id-4efQg54WPFyzSEPsepFVZSb8KmeVYtonQNZO1iAE"
+ SessionNotOnOrAfter="2017-06-28T04:34:26Z"
+ >
+ <saml:AuthnContext>
+ <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
+ </saml:AuthnContext>
+ </saml:AuthnStatement>
+ <saml:AttributeStatement>
+ <saml:Attribute Name="lastName"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
+ >
+ <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xsi:type="xs:string"
+ >Yadav</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="username"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
+ >
+ <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xsi:type="xs:string"
+ >H183561</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="email"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
+ >
+ <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xsi:type="xs:string"
+ >my_email@my_provider.com</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="firstName"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
+ >
+ <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xsi:type="xs:string"
+ >MY_NAME</saml:AttributeValue>
+ </saml:Attribute>
+ </saml:AttributeStatement>
+ </saml:Assertion>
+</samlp:Response>
testsuite/integration/pom.xml 2(+1 -1)
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index cda3bbb..25b8036 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-testsuite-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java
index acf775c..b64cf03 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java
@@ -37,6 +37,7 @@ import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.pages.LoginExpiredPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage;
+import org.keycloak.testsuite.pages.ProceedPage;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
@@ -52,6 +53,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.hamcrest.Matchers;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
@@ -345,6 +347,9 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
// Go to the same link again
driver.navigate().to(linkFromMail.trim());
+ proceedPage.assertCurrent();
+ Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Confirm linking the account"));
+ proceedPage.clickProceedLink();
infoPage.assertCurrent();
Assert.assertThat(infoPage.getInfo(), startsWith("You successfully verified your email. Please go back to your original browser and continue there with the login."));
}
@@ -379,10 +384,14 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
WebDriver driver2 = webRule2.getDriver();
InfoPage infoPage2 = webRule2.getPage(InfoPage.class);
+ ProceedPage proceedPage2 = webRule2.getPage(ProceedPage.class);
driver2.navigate().to(linkFromMail.trim());
// authenticated, but not redirected to app. Just seeing info page.
+ proceedPage2.assertCurrent();
+ Assert.assertThat(proceedPage2.getInfo(), Matchers.containsString("Confirm linking the account"));
+ proceedPage2.clickProceedLink();
infoPage2.assertCurrent();
Assert.assertThat(infoPage2.getInfo(), startsWith("You successfully verified your email. Please go back to your original browser and continue there with the login."));
} finally {
@@ -540,16 +549,12 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
driver.navigate().back();
Assert.assertTrue(driver.getPageSource().contains("You are already logged in."));
driver.navigate().forward();
- this.loginExpiredPage.assertCurrent();
- this.loginExpiredPage.clickLoginContinueLink();
this.idpConfirmLinkPage.assertCurrent();
// Click browser 'back' on review profile page
this.idpConfirmLinkPage.clickReviewProfile();
this.updateProfilePage.assertCurrent();
driver.navigate().back();
- this.loginExpiredPage.assertCurrent();
- this.loginExpiredPage.clickLoginContinueLink();
this.updateProfilePage.assertCurrent();
this.updateProfilePage.update("Pedro", "Igor", "psilva@redhat.com");
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 297d00a..c854e1e 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
@@ -110,6 +110,9 @@ public abstract class AbstractIdentityProviderTest {
@WebResource
protected InfoPage infoPage;
+ @WebResource
+ protected ProceedPage proceedPage;
+
protected KeycloakSession session;
protected int logoutTimeOffset = 0;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java
index c3e0135..9fb585e 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java
@@ -91,9 +91,9 @@ public class OIDCBrokerUserPropertyTest extends AbstractKeycloakIdentityProvider
@Override
protected void doAssertTokenRetrieval(String pageSource) {
try {
- SAML2Request saml2Request = new SAML2Request();
- ResponseType responseType = (ResponseType) saml2Request
- .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource));
+ ResponseType responseType = (ResponseType) SAML2Request
+ .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource))
+ .getSamlObject();
//.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8")));
assertNotNull(responseType);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java
index bbbbc47..8fca98d 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java
@@ -90,9 +90,9 @@ public class SAMLBrokerUserPropertyTest extends AbstractKeycloakIdentityProvider
@Override
protected void doAssertTokenRetrieval(String pageSource) {
try {
- SAML2Request saml2Request = new SAML2Request();
- ResponseType responseType = (ResponseType) saml2Request
- .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource));
+ ResponseType responseType = (ResponseType) SAML2Request
+ .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource))
+ .getSamlObject();
//.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8")));
assertNotNull(responseType);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java
index 8afc49b..5e17779 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java
@@ -93,9 +93,9 @@ public class SAMLKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP
@Override
protected void doAssertTokenRetrieval(String pageSource) {
try {
- SAML2Request saml2Request = new SAML2Request();
- ResponseType responseType = (ResponseType) saml2Request
- .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource));
+ ResponseType responseType = (ResponseType) SAML2Request
+ .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource))
+ .getSamlObject();
//.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8")));
assertNotNull(responseType);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java
index 8a453a7..a0ee823 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java
@@ -98,9 +98,9 @@ public class SAMLKeyCloakServerBrokerWithSignatureTest extends AbstractKeycloakI
@Override
protected void doAssertTokenRetrieval(String pageSource) {
try {
- SAML2Request saml2Request = new SAML2Request();
- ResponseType responseType = (ResponseType) saml2Request
- .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource));
+ ResponseType responseType = (ResponseType) SAML2Request
+ .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource))
+ .getSamlObject();
assertNotNull(responseType);
assertFalse(responseType.getAssertions().isEmpty());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java
index 8c38363..f981377 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java
@@ -27,6 +27,7 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher;
import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer;
import org.jboss.resteasy.spi.ResteasyDeployment;
+import org.keycloak.common.Version;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -38,16 +39,18 @@ import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.testsuite.util.cli.TestsuiteCLI;
import org.keycloak.util.JsonSerialization;
-import org.mvel2.util.Make;
import javax.servlet.DispatcherType;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
+import javax.net.ssl.SSLContext;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -61,6 +64,7 @@ public class KeycloakServer {
public static class KeycloakServerConfig {
private String host = "localhost";
private int port = 8081;
+ private int portHttps = -1;
private int workerThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2) * 8;
private String resourcesHome;
@@ -72,6 +76,10 @@ public class KeycloakServer {
return port;
}
+ public int getPortHttps() {
+ return portHttps;
+ }
+
public String getResourcesHome() {
return resourcesHome;
}
@@ -84,6 +92,10 @@ public class KeycloakServer {
this.port = port;
}
+ public void setPortHttps(int portHttps) {
+ this.portHttps = portHttps;
+ }
+
public void setResourcesHome(String resourcesHome) {
this.resourcesHome = resourcesHome;
}
@@ -106,6 +118,10 @@ public class KeycloakServer {
}
public static void main(String[] args) throws Throwable {
+ if (!System.getenv().containsKey("MAVEN_CMD_LINE_ARGS")) {
+ Version.BUILD_TIME = new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date());
+ }
+
bootstrapKeycloakServer(args);
}
@@ -133,6 +149,10 @@ public class KeycloakServer {
config.setPort(Integer.valueOf(System.getProperty("keycloak.port")));
}
+ if (System.getProperty("keycloak.port.https") != null) {
+ config.setPortHttps(Integer.valueOf(System.getProperty("keycloak.port.https")));
+ }
+
if (System.getProperty("keycloak.bind.address") != null) {
config.setHost(System.getProperty("keycloak.bind.address"));
}
@@ -305,6 +325,10 @@ public class KeycloakServer {
.setWorkerThreads(config.getWorkerThreads())
.setIoThreads(config.getWorkerThreads() / 8);
+ if (config.getPortHttps() != -1) {
+ builder = builder.addHttpsListener(config.getPortHttps(), config.getHost(), SSLContext.getDefault());
+ }
+
server = new UndertowJaxrsServer();
try {
server.start(builder);
@@ -343,7 +367,9 @@ public class KeycloakServer {
info("Loading resources from " + config.getResourcesHome());
}
- info("Started Keycloak (http://" + config.getHost() + ":" + config.getPort() + "/auth) in "
+ info("Started Keycloak (http://" + config.getHost() + ":" + config.getPort() + "/auth"
+ + (config.getPortHttps() > 0 ? ", https://" + config.getHost() + ":" + config.getPortHttps()+ "/auth" : "")
+ + ") in "
+ (System.currentTimeMillis() - start) + " ms\n");
} catch (RuntimeException e) {
server.stop();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java
index f71d0da..f11499e 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java
@@ -67,7 +67,7 @@ import org.keycloak.testsuite.util.cli.TestCacheUtils;
*
* <local-cache name="work" start="EAGER" batching="false" />
*
- * Finally, add this system property when running the test: -Dkeycloak.connectionsInfinispan.remoteStoreEnabled=true
+ * Finally, add this system properties when running the test: -Dkeycloak.connectionsInfinispan.remoteStoreEnabled=true -Dkeycloak.connectionsInfinispan.siteName=dc-0
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
index 7d6745e..0312cbb 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
@@ -106,6 +106,13 @@ public class UserSessionProviderTest {
}
@Test
+ public void testUpdateSessionInSameTransaction() {
+ UserSessionModel[] sessions = createSessions();
+ session.sessions().getUserSession(realm, sessions[0].getId()).setLastSessionRefresh(1000);
+ assertEquals(1000, session.sessions().getUserSession(realm, sessions[0].getId()).getLastSessionRefresh());
+ }
+
+ @Test
public void testRestartSession() {
int started = Time.currentTime();
UserSessionModel[] sessions = createSessions();
@@ -156,7 +163,8 @@ public class UserSessionProviderTest {
String userSessionId = sessions[0].getId();
String clientUUID = realm.getClientByClientId("test-app").getId();
- AuthenticatedClientSessionModel clientSession = sessions[0].getAuthenticatedClientSessions().get(clientUUID);
+ UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId);
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientUUID);
int time = clientSession.getTimestamp();
assertEquals(null, clientSession.getAction());
@@ -173,6 +181,24 @@ public class UserSessionProviderTest {
}
@Test
+ public void testUpdateClientSessionInSameTransaction() {
+ UserSessionModel[] sessions = createSessions();
+
+ String userSessionId = sessions[0].getId();
+ String clientUUID = realm.getClientByClientId("test-app").getId();
+
+ UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId);
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientUUID);
+
+ clientSession.setAction(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name());
+ clientSession.setNote("foo", "bar");
+
+ AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessions().get(clientUUID);
+ assertEquals(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name(), updated.getAction());
+ assertEquals("bar", updated.getNote("foo"));
+ }
+
+ @Test
public void testGetUserSessions() {
UserSessionModel[] sessions = createSessions();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ProceedPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ProceedPage.java
new file mode 100644
index 0000000..97d7c28
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ProceedPage.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.pages;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class ProceedPage extends AbstractPage {
+
+ @FindBy(className = "instruction")
+ private WebElement infoMessage;
+
+ @FindBy(linkText = "» Click here to proceed")
+ private WebElement proceedLink;
+
+ public String getInfo() {
+ return infoMessage.getText();
+ }
+
+ public boolean isCurrent() {
+ return driver.getPageSource().contains("kc-info-message") && proceedLink.isDisplayed();
+ }
+
+ @Override
+ public void open() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void clickProceedLink() {
+ proceedLink.click();
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/ValidationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/ValidationTest.java
index 6833b34..de905bf 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/ValidationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/ValidationTest.java
@@ -78,7 +78,7 @@ public class ValidationTest {
public void testBrokerExportDescriptor() throws Exception {
URL schemaFile = getClass().getResource("/schema/saml/v2/saml-schema-metadata-2.0.xsd");
Source xmlFile = new StreamSource(new ByteArrayInputStream(SPMetadataDescriptor.getSPDescriptor(
- "POST", "http://realm/assertion", "http://realm/logout", true, false, "test", SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT, KeycloakModelUtils.generateKeyPairCertificate("test").getCertificate()
+ "POST", "http://realm/assertion", "http://realm/logout", true, false, false, "test", SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT, KeycloakModelUtils.generateKeyPairCertificate("test").getCertificate(), ""
).getBytes()), "SP Descriptor");
SchemaFactory schemaFactory = SchemaFactory
.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/BatchTaskRunner.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/BatchTaskRunner.java
new file mode 100644
index 0000000..e6f96cc
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/BatchTaskRunner.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.util.cli;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+class BatchTaskRunner {
+
+ static void runInBatches(int first, int count, int batchCount, KeycloakSessionFactory sessionFactory, BatchTask batchTask) {
+
+ final StateHolder state = new StateHolder();
+ state.firstInThisBatch = first;
+ state.remaining = count;
+ state.countInThisBatch = Math.min(batchCount, state.remaining);
+ while (state.remaining > 0) {
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ batchTask.run(session, state.firstInThisBatch, state.countInThisBatch);
+ }
+ });
+
+ // update state
+ state.firstInThisBatch = state.firstInThisBatch + state.countInThisBatch;
+ state.remaining = state.remaining - state.countInThisBatch;
+ state.countInThisBatch = Math.min(batchCount, state.remaining);
+ }
+ }
+
+
+ private static class StateHolder {
+ int firstInThisBatch;
+ int countInThisBatch;
+ int remaining;
+ };
+
+
+ @FunctionalInterface
+ public interface BatchTask {
+
+ void run(KeycloakSession session, int firstInThisIteration, int countInThisIteration);
+
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java
index 0c7eff0..1f2e10d 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java
@@ -72,7 +72,7 @@ public class CacheCommands {
log.infof("Cache %s, size: %d", cache.getName(), size);
if (size > 50) {
- log.info("Skip printing cache recors due to big size");
+ log.info("Skip printing cache records due to big size");
} else {
for (Map.Entry<Object, Object> entry : cache.entrySet()) {
log.infof("%s=%s", entry.getKey(), entry.getValue());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/ClusterProviderTaskCommand.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/ClusterProviderTaskCommand.java
new file mode 100644
index 0000000..f1e0967
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/ClusterProviderTaskCommand.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.util.cli;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.testsuite.federation.sync.SyncDummyUserFederationProviderFactory;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClusterProviderTaskCommand extends AbstractCommand {
+
+ private static final ExecutorService executors = Executors.newCachedThreadPool();
+
+ @Override
+ protected void doRunCommand(KeycloakSession session) {
+ String taskName = getArg(0);
+ int taskTimeout = getIntArg(1);
+ int sleepTime = getIntArg(2);
+
+ ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+ Future future = cluster.executeIfNotExecutedAsync(taskName, taskTimeout, () -> {
+ log.infof("Started sleeping for " + sleepTime + " seconds");
+ Thread.sleep(sleepTime * 1000);
+ log.infof("Stopped sleeping");
+ return null;
+ });
+
+ log.info("I've retrieved future successfully");
+
+ executors.execute(() -> {
+ try {
+ future.get();
+ log.info("Successfully finished future!");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ private void updateConfig(MultivaluedHashMap<String, String> cfg, int waitTime) {
+ cfg.putSingle(SyncDummyUserFederationProviderFactory.WAIT_TIME, String.valueOf(waitTime));
+ }
+
+
+ @Override
+ public String getName() {
+ return "clusterProviderTask";
+ }
+
+ @Override
+ public String printUsage() {
+ return super.printUsage() + " <task-name> <task-wait-time-in-seconds> <sleep-time-in-seconds>";
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java
index 2c71c72..2ac3054 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java
@@ -43,12 +43,6 @@ public class RoleCommands {
return "createRoles";
}
- private class StateHolder {
- int firstInThisBatch;
- int countInThisBatch;
- int remaining;
- };
-
@Override
protected void doRunCommand(KeycloakSession session) {
rolePrefix = getArg(0);
@@ -57,24 +51,9 @@ public class RoleCommands {
int count = getIntArg(3);
int batchCount = getIntArg(4);
- final StateHolder state = new StateHolder();
- state.firstInThisBatch = first;
- state.remaining = count;
- state.countInThisBatch = Math.min(batchCount, state.remaining);
- while (state.remaining > 0) {
- KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() {
-
- @Override
- public void run(KeycloakSession session) {
- createRolesInBatch(session, roleContainer, rolePrefix, state.firstInThisBatch, state.countInThisBatch);
- }
- });
-
- // update state
- state.firstInThisBatch = state.firstInThisBatch + state.countInThisBatch;
- state.remaining = state.remaining - state.countInThisBatch;
- state.countInThisBatch = Math.min(batchCount, state.remaining);
- }
+ BatchTaskRunner.runInBatches(first, count, batchCount, session.getKeycloakSessionFactory(), (KeycloakSession bathcSession, int firstInThisIteration, int countInThisIteration) -> {
+ createRolesInBatch(session, roleContainer, rolePrefix, firstInThisIteration, countInThisIteration);
+ });
log.infof("Command finished. All roles from %s to %s created", rolePrefix + first, rolePrefix + (first + count - 1));
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
index b1ff087..baedc8a 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
@@ -43,15 +43,17 @@ public class TestsuiteCLI {
private static final Class<?>[] BUILTIN_COMMANDS = {
ExitCommand.class,
HelpCommand.class,
- AbstractOfflineCacheCommand.PutCommand.class,
- AbstractOfflineCacheCommand.GetCommand.class,
- AbstractOfflineCacheCommand.GetMultipleCommand.class,
- AbstractOfflineCacheCommand.GetLocalCommand.class,
- AbstractOfflineCacheCommand.SizeLocalCommand.class,
- AbstractOfflineCacheCommand.RemoveCommand.class,
- AbstractOfflineCacheCommand.SizeCommand.class,
- AbstractOfflineCacheCommand.ListCommand.class,
- AbstractOfflineCacheCommand.ClearCommand.class,
+ AbstractSessionCacheCommand.PutCommand.class,
+ AbstractSessionCacheCommand.GetCommand.class,
+ AbstractSessionCacheCommand.GetMultipleCommand.class,
+ AbstractSessionCacheCommand.GetLocalCommand.class,
+ AbstractSessionCacheCommand.SizeLocalCommand.class,
+ AbstractSessionCacheCommand.RemoveCommand.class,
+ AbstractSessionCacheCommand.SizeCommand.class,
+ AbstractSessionCacheCommand.ListCommand.class,
+ AbstractSessionCacheCommand.ClearCommand.class,
+ AbstractSessionCacheCommand.CreateManySessionsCommand.class,
+ AbstractSessionCacheCommand.CreateManySessionsProviderCommand.class,
PersistSessionsCommand.class,
LoadPersistentSessionsCommand.class,
UserCommands.Create.class,
@@ -62,7 +64,8 @@ public class TestsuiteCLI {
RoleCommands.CreateRoles.class,
CacheCommands.ListCachesCommand.class,
CacheCommands.GetCacheCommand.class,
- CacheCommands.CacheRealmObjectsCommand.class
+ CacheCommands.CacheRealmObjectsCommand.class,
+ ClusterProviderTaskCommand.class
};
private final KeycloakSessionFactory sessionFactory;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java
index 5fc0e86..7360d7b 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java
@@ -48,12 +48,6 @@ public class UserCommands {
return "createUsers";
}
- private class StateHolder {
- int firstInThisBatch;
- int countInThisBatch;
- int remaining;
- };
-
@Override
protected void doRunCommand(KeycloakSession session) {
usernamePrefix = getArg(0);
@@ -64,24 +58,7 @@ public class UserCommands {
int batchCount = getIntArg(5);
roleNames = getArg(6);
- final StateHolder state = new StateHolder();
- state.firstInThisBatch = first;
- state.remaining = count;
- state.countInThisBatch = Math.min(batchCount, state.remaining);
- while (state.remaining > 0) {
- KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() {
-
- @Override
- public void run(KeycloakSession session) {
- createUsersInBatch(session, state.firstInThisBatch, state.countInThisBatch);
- }
- });
-
- // update state
- state.firstInThisBatch = state.firstInThisBatch + state.countInThisBatch;
- state.remaining = state.remaining - state.countInThisBatch;
- state.countInThisBatch = Math.min(batchCount, state.remaining);
- }
+ BatchTaskRunner.runInBatches(first, count, batchCount, session.getKeycloakSessionFactory(), this::createUsersInBatch);
log.infof("Command finished. All users from %s to %s created", usernamePrefix + first, usernamePrefix + (first + count - 1));
}
diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties
index 5f0d60b..20f1df6 100755
--- a/testsuite/integration/src/test/resources/log4j.properties
+++ b/testsuite/integration/src/test/resources/log4j.properties
@@ -58,6 +58,13 @@ log4j.logger.org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory=${
# Enable to view hibernate statistics
log4j.logger.org.keycloak.connections.jpa.HibernateStatsReporter=debug
+keycloak.infinispan.logging.level=info
+log4j.logger.org.keycloak.cluster.infinispan=${keycloak.infinispan.logging.level}
+log4j.logger.org.keycloak.connections.infinispan=${keycloak.infinispan.logging.level}
+log4j.logger.org.keycloak.keys.infinispan=${keycloak.infinispan.logging.level}
+log4j.logger.org.keycloak.models.cache.infinispan=${keycloak.infinispan.logging.level}
+log4j.logger.org.keycloak.models.sessions.infinispan=${keycloak.infinispan.logging.level}
+
# Enable to view ldap logging
# log4j.logger.org.keycloak.storage.ldap=trace
@@ -86,6 +93,8 @@ log4j.logger.org.apache.directory.server.ldap.LdapProtocolHandler=error
#log4j.logger.org.keycloak.services.resources.IdentityBrokerService=trace
#log4j.logger.org.keycloak.broker=trace
+#log4j.logger.org.keycloak.cluster.infinispan.InfinispanNotificationsManager=trace
+
#log4j.logger.io.undertow=trace
#log4j.logger.org.keycloak.protocol=debug
diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
index fc695d4..a8aa46d 100755
--- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
@@ -85,6 +85,9 @@
"connectionsInfinispan": {
"default": {
+ "jgroupsUdpMcastAddr": "${keycloak.connectionsInfinispan.jgroupsUdpMcastAddr:234.56.78.90}",
+ "nodeName": "${keycloak.connectionsInfinispan.nodeName,jboss.node.name:}",
+ "siteName": "${keycloak.connectionsInfinispan.siteName,jboss.site.name:}",
"clustered": "${keycloak.connectionsInfinispan.clustered:false}",
"async": "${keycloak.connectionsInfinispan.async:false}",
"sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}",
testsuite/integration-arquillian/HOW-TO-RUN.md 162(+159 -3)
diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md
index cd818c2..e80479d 100644
--- a/testsuite/integration-arquillian/HOW-TO-RUN.md
+++ b/testsuite/integration-arquillian/HOW-TO-RUN.md
@@ -61,7 +61,7 @@ More info: http://javahowto.blogspot.cz/2010/09/java-agentlibjdwp-for-attaching.
Analogically, there is the same behaviour for JBoss based app server as for auth server. The default port is set to 5006. There are app server properties.
-Dapp.server.debug.port=$PORT
- -Dapp.server.debug.suspend=y
+ -Dapp.server.debug.suspend=y
## Testsuite logging
@@ -262,6 +262,8 @@ The UI tests are focused on the Admin Console as well as on some login scenarios
The tests also use some constants placed in [test-constants.properties](tests/base/src/test/resources/test-constants.properties). A different file can be specified by `-Dtestsuite.constants=path/to/different-test-constants.properties`
+In case a custom `settings.xml` is used for Maven, you need to specify it also in `-Dkie.maven.settings.custom=path/to/settings.xml`.
+
#### Execution example
```
mvn -f testsuite/integration-arquillian/tests/other/console/pom.xml \
@@ -452,7 +454,7 @@ First compile the Infinispan/JDG test server via the following command:
`mvn -Pcache-server-infinispan -f testsuite/integration-arquillian -DskipTests clean install`
or
-
+
`mvn -Pcache-server-jdg -f testsuite/integration-arquillian -DskipTests clean install`
Then you can run the tests using the following command (adjust the test specification according to your needs):
@@ -462,5 +464,159 @@ Then you can run the tests using the following command (adjust the test specific
or
`mvn -Pcache-server-jdg -Dtest=*.crossdc.* -pl testsuite/integration-arquillian/tests/base test`
+
+It can be useful to add additional system property to enable logging:
+
+ -Dkeycloak.infinispan.logging.level=debug
+
+
+
+#### Run Cross-DC Tests from Intellij IDEA
+
+First we will manually download, configure and run infinispan server. Then we can run the tests from IDE against 1 server. It's more effective during
+development as there is no need to restart infinispan server(s) among test runs.
+
+1) Download infinispan server 8.2.X from http://infinispan.org/download/
+
+2) Edit `ISPN_SERVER_HOME/standalone/configuration/standalone.xml` and add these local-caches to the section under cache-container `local` :
+
+ <cache-container name="local" ...
+
+ ...
+
+ <local-cache-configuration name="sessions-cfg" start="EAGER" batching="false">
+ <transaction mode="NON_XA" locking="PESSIMISTIC"/>
+ </local-cache-configuration>
+
+ <local-cache name="sessions" configuration="sessions-cfg" />
+ <local-cache name="offlineSessions" configuration="sessions-cfg" />
+ <local-cache name="loginFailures" configuration="sessions-cfg" />
+ <local-cache name="actionTokens" configuration="sessions-cfg" />
+ <local-cache name="work" configuration="sessions-cfg" />
+
+ </cache>
+
+3) Run the server through `./standalone.sh`
+
+4) Setup MySQL database or some other shared database.
+
+5) Ensure that org.wildfly.arquillian:wildfly-arquillian-container-managed is on the classpath when running test. On Intellij, it can be
+done by going to: View -> Tool Windows -> Maven projects. Then check profile "cache-server-infinispan". The tests will use this profile when executed.
+
+6) Run the LoginCrossDCTest (or any other test) with those properties. In shortcut, it's using MySQL database, disabled L1 lifespan and
+connects to the remoteStore provided by infinispan server configured in previous steps:
+
+ -Dauth.server.crossdc=true -Dauth.server.undertow.crossdc=true -Dcache.server.lifecycle.skip=true -Dkeycloak.connectionsJpa.url.crossdc=jdbc:mysql://localhost/keycloak
+ -Dkeycloak.connectionsJpa.driver.crossdc=com.mysql.jdbc.Driver -Dkeycloak.connectionsJpa.user=keycloak
+ -Dkeycloak.connectionsJpa.password=keycloak -Dkeycloak.connectionsInfinispan.clustered=true -Dkeycloak.connectionsInfinispan.l1Lifespan=0
+ -Dkeycloak.connectionsInfinispan.remoteStorePort=11222 -Dkeycloak.connectionsInfinispan.remoteStorePort.2=11222 -Dkeycloak.connectionsInfinispan.sessionsOwners=1
+ -Dsession.cache.owners=1 -Dkeycloak.infinispan.logging.level=debug -Dresources
+
+7) If you want to debug and test manually, the servers are running on these ports (Note that not all backend servers are running by default and some might be also unused by loadbalancer):
+
+ Loadbalancer -> "http://localhost:8180/auth"
+ auth-server-undertow-cross-dc-0_1 -> "http://localhost:8101/auth"
+ auth-server-undertow-cross-dc-0_2-manual -> "http://localhost:8102/auth"
+ auth-server-undertow-cross-dc-1_1 -> "http://localhost:8111/auth"
+ auth-server-undertow-cross-dc-1_2-manual -> "http://localhost:8112/auth"
+
+
+## Run Docker Authentication test
+
+First, validate that your machine has a valid docker installation and that it is available to the JVM running the test.
+The exact steps to configure Docker depend on the operating system.
+
+By default, the test will run against Undertow based embedded Keycloak Server, thus no distribution build is required beforehand.
+The exact command line arguments depend on the operating system.
+
+### General guidelines
+
+If docker daemon doesn't run locally, or if you're not running on Linux, you may need
+ to determine the IP of the bridge interface or local interface that Docker daemon can use to connect to Keycloak Server.
+ Then specify that IP as additional system property called *host.ip*, for example:
+
+ -Dhost.ip=192.168.64.1
+
+If using Docker for Mac, you can create an alias for your local network interface:
+
+ sudo ifconfig lo0 alias 10.200.10.1/24
+
+Then pass the IP as *host.ip*:
+
+ -Dhost.ip=10.200.10.1
+
+
+If you're running a Docker fork that always lists a host component of an image on `docker images` (e.g. Fedora / RHEL Docker)
+use `-Ddocker.io-prefix-explicit=true` argument when running the test.
+
+
+### Fedora
+
+On Fedora one way to set up Docker server is the following:
+
+ # install docker
+ sudo dnf install docker
+
+ # configure docker
+ # remove --selinux-enabled from OPTIONS
+ sudo vi /etc/sysconfig/docker
+
+ # create docker group and add your user (so docker wouldn't need root permissions)
+ sudo groupadd docker && sudo gpasswd -a ${USER} docker && sudo systemctl restart docker
+ newgrp docker
+
+ # you need to login again after this
+
+
+ # make sure Docker is available
+ docker pull registry:2
+
+You may also need to add an iptables rule to allow container to host traffic
+
+ sudo iptables -I INPUT -i docker0 -j ACCEPT
+
+Then, run the test passing `-Ddocker.io-prefix-explicit=true`:
+
+ mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
+ clean test \
+ -Dtest=DockerClientTest \
+ -Dkeycloak.profile.feature.docker=enabled \
+ -Ddocker.io-prefix-explicit=true
+
+
+### macOS
+
+On macOS all you need to do is install Docker for Mac, start it up, and check that it works:
+
+ # make sure Docker is available
+ docker pull registry:2
+
+Be especially careful to restart Docker server after every sleep / suspend to ensure system clock of Docker VM is synchronized with
+that of the host operating system - Docker for Mac runs inside a VM.
+
+
+Then, run the test passing `-Dhost.ip=IP` where IP corresponds to en0 interface or an alias for localhost:
+
+ mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
+ clean test \
+ -Dtest=DockerClientTest \
+ -Dkeycloak.profile.feature.docker=enabled \
+ -Dhost.ip=10.200.10.1
+
+
+
+### Running Docker test against Keycloak Server distribution
+
+Make sure to build the distribution:
+
+ mvn clean install -f distribution
+
+Then, before running the test, setup Keycloak Server distribution for the tests:
+
+ mvn -f testsuite/integration-arquillian/servers/pom.xml \
+ clean install \
+ -Pauth-server-wildfly
+
+When running the test, add the following arguments to the command line:
-_Someone using IntelliJ IDEA, please describe steps for that IDE_
+ -Pauth-server-wildfly -Pauth-server-enable-disable-feature -Dfeature.name=docker -Dfeature.value=enabled
testsuite/integration-arquillian/pom.xml 10(+8 -2)
diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml
index 7e36d1a..0bcb2b8 100644
--- a/testsuite/integration-arquillian/pom.xml
+++ b/testsuite/integration-arquillian/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-testsuite-pom</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -46,6 +46,7 @@
<arquillian-drone.version>2.0.1.Final</arquillian-drone.version>
<arquillian-graphene.version>2.1.0.Alpha3</arquillian-graphene.version>
<arquillian-wildfly-container.version>2.1.0.Alpha2</arquillian-wildfly-container.version>
+ <arquillian-wls-container.version>1.0.1.Final</arquillian-wls-container.version>
<arquillian-infinispan-container.version>1.2.0.Beta2</arquillian-infinispan-container.version>
<version.shrinkwrap.resolvers>2.2.2</version.shrinkwrap.resolvers>
<undertow-embedded.version>1.0.0.Alpha2</undertow-embedded.version>
@@ -108,6 +109,12 @@
<artifactId>wildfly-arquillian-container-domain-managed</artifactId>
<version>${arquillian-wildfly-container.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.jboss.arquillian.container</groupId>
+ <artifactId>arquillian-wls-remote-12.1.x</artifactId>
+ <version>${arquillian-wls-container.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</dependencyManagement>
@@ -136,7 +143,6 @@
<modules>
<module>test-apps</module>
- <module>test-utils</module>
<module>servers</module>
<module>tests</module>
</modules>
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/as7/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/as7/pom.xml
index 51e243c..99e1935 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/as7/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/as7/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/eap/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/eap/pom.xml
index 1764447..7e7b10b 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/eap/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/eap/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/eap6/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/eap6/pom.xml
index e3f057a..e78f3e8 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/eap6/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/eap6/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/eap6-fuse/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/eap6-fuse/pom.xml
index 22663bd..0a1eb4c 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/eap6-fuse/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/eap6-fuse/pom.xml
@@ -5,7 +5,7 @@
<parent>
<artifactId>integration-arquillian-servers-app-server-jboss</artifactId>
<groupId>org.keycloak.testsuite</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml
index 3775738..382612c 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -309,7 +309,7 @@
</goals>
<configuration>
<executable>${common.resources}/install-patch.${script.suffix}</executable>
- <workingDirectory>${app.server.home}/bin</workingDirectory>
+ <workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<environmentVariables>
<JAVA_HOME>${app.server.java.home}</JAVA_HOME>
<JBOSS_HOME>${app.server.jboss.home}</JBOSS_HOME>
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/relative/eap/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/relative/eap/pom.xml
index 9bb5d99..4e47112 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/relative/eap/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/relative/eap/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-jboss-relative</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/relative/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/relative/pom.xml
index 2a5df75..3014456 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/relative/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/relative/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/relative/wildfly/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/relative/wildfly/pom.xml
index fcf1ff3..18e864e 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/relative/wildfly/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/relative/wildfly/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-jboss-relative</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly/pom.xml
index 68f00c9..a4c073e 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly10/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly10/pom.xml
index 4d37972..50def5c 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly10/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly10/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly8/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly8/pom.xml
index 2f5f4b1..60845f8 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly8/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly8/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly9/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly9/pom.xml
index 218453a..afdb346 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly9/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly9/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse61/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/fuse61/pom.xml
index efd21cf..0b565ec 100644
--- a/testsuite/integration-arquillian/servers/app-server/karaf/fuse61/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse61/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-karaf</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse62/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/fuse62/pom.xml
index 6760aae..2d555c4 100644
--- a/testsuite/integration-arquillian/servers/app-server/karaf/fuse62/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse62/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-karaf</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml
index a5966c5..eccac11 100644
--- a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-karaf</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config-auth.cli b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config-auth.cli
index 2c3bc66..f343e28 100644
--- a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config-auth.cli
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config-auth.cli
@@ -4,5 +4,5 @@ config:update
system-property -p hawtio.roles admin,user
system-property -p hawtio.keycloakEnabled true
system-property -p hawtio.realm keycloak
-system-property -p hawtio.keycloakClientConfig \$\{karaf.base\}/etc/keycloak-hawtio-client.json
+system-property -p hawtio.keycloakClientConfig file://\$\{karaf.base\}/etc/keycloak-hawtio-client.json
system-property -p hawtio.rolePrincipalClasses org.keycloak.adapters.jaas.RolePrincipal,org.apache.karaf.jaas.boot.principal.RolePrincipal
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/karaf3/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/karaf3/pom.xml
index 93cf564..9b307e5 100644
--- a/testsuite/integration-arquillian/servers/app-server/karaf/karaf3/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/karaf3/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-karaf</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml
index 15bf097..e611434 100644
--- a/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/pom.xml b/testsuite/integration-arquillian/servers/app-server/pom.xml
index 3120cc3..f0c8fbf 100644
--- a/testsuite/integration-arquillian/servers/app-server/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/tomcat/pom.xml b/testsuite/integration-arquillian/servers/app-server/tomcat/pom.xml
index cc74129..3d0d309 100644
--- a/testsuite/integration-arquillian/servers/app-server/tomcat/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/tomcat/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat7/pom.xml b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat7/pom.xml
index fd4af48..bbc1631 100644
--- a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat7/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat7/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-tomcat</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat8/pom.xml b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat8/pom.xml
index 99b4353..82f7d30 100644
--- a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat8/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat8/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-tomcat</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat9/pom.xml b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat9/pom.xml
index 48b0234..463dff3 100644
--- a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat9/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat9/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-tomcat</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl
index d104e37..276f389 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl
@@ -36,6 +36,15 @@
</provider>
</spi>
</xsl:variable>
+ <xsl:variable name="samlPortsDefinition">
+ <spi name="login-protocol">
+ <provider name="saml" enabled="true">
+ <properties>
+ <property name="knownProtocols" value="["http=${{auth.server.http.port}}","https=${{auth.server.https.port}}"]"/>
+ </properties>
+ </provider>
+ </spi>
+ </xsl:variable>
<xsl:variable name="themeModuleDefinition">
<modules>
<module>org.keycloak.testsuite.integration-arquillian-testsuite-providers</module>
@@ -60,11 +69,12 @@
</xsl:copy>
</xsl:template>
- <!--inject truststore-->
+ <!--inject truststore and SAML port-protocol mappings-->
<xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $nsKS)]">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
<xsl:copy-of select="$truststoreDefinition"/>
+ <xsl:copy-of select="$samlPortsDefinition"/>
</xsl:copy>
</xsl:template>
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/eap/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/eap/pom.xml
index 0acda47..426906d 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/eap/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/eap/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-auth-server-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
index 775aa5a..c7059ad 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-auth-server</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -100,7 +100,6 @@
<outputDirectory>${project.build.directory}/unpacked</outputDirectory>
</artifactItem>
</artifactItems>
- <excludes>**/product.conf</excludes>
</configuration>
</execution>
<execution>
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/wildfly/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/wildfly/pom.xml
index e1c64cf..675104f 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/wildfly/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/wildfly/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-auth-server-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -30,6 +30,14 @@
<artifactId>integration-arquillian-servers-auth-server-wildfly</artifactId>
<name>Auth Server - JBoss - Wildfly</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-server-dist</artifactId>
+ <type>zip</type>
+ </dependency>
+ </dependencies>
<properties>
<auth.server.jboss>wildfly</auth.server.jboss>
diff --git a/testsuite/integration-arquillian/servers/auth-server/pom.xml b/testsuite/integration-arquillian/servers/auth-server/pom.xml
index 5fdb02c..31f2aa5 100644
--- a/testsuite/integration-arquillian/servers/auth-server/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/pom.xml b/testsuite/integration-arquillian/servers/auth-server/services/pom.xml
index f154e41..6387079 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/services/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-auth-server</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml
index d29bb1a..8ad166f 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-auth-server-services</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-testsuite-providers</artifactId>
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/JGroupsStats.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/JGroupsStats.java
new file mode 100644
index 0000000..e14609a
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/JGroupsStats.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.rest.representation;
+
+import java.text.NumberFormat;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class JGroupsStats {
+
+ private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance();
+
+ static {
+ NUMBER_FORMAT.setGroupingUsed(true);
+ }
+
+ private long sentBytes;
+ private long sentMessages;
+ private long receivedBytes;
+ private long receivedMessages;
+
+ public JGroupsStats() {
+ }
+
+ public JGroupsStats(long sentBytes, long sentMessages, long receivedBytes, long receivedMessages) {
+ this.sentBytes = sentBytes;
+ this.sentMessages = sentMessages;
+ this.receivedBytes = receivedBytes;
+ this.receivedMessages = receivedMessages;
+ }
+
+ public long getSentBytes() {
+ return sentBytes;
+ }
+
+ public void setSentBytes(long sentBytes) {
+ this.sentBytes = sentBytes;
+ }
+
+ public long getSentMessages() {
+ return sentMessages;
+ }
+
+ public void setSentMessages(long sentMessages) {
+ this.sentMessages = sentMessages;
+ }
+
+ public long getReceivedBytes() {
+ return receivedBytes;
+ }
+
+ public void setReceivedBytes(long receivedBytes) {
+ this.receivedBytes = receivedBytes;
+ }
+
+ public long getReceivedMessages() {
+ return receivedMessages;
+ }
+
+ public void setReceivedMessages(long receivedMessages) {
+ this.receivedMessages = receivedMessages;
+ }
+
+ public String statsAsString() {
+ return String.format("sentBytes: %s, sentMessages: %d, receivedBytes: %s, receivedMessages: %d",
+ NUMBER_FORMAT.format(sentBytes), sentMessages, NUMBER_FORMAT.format(receivedBytes), receivedMessages);
+ }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/RemoteCacheStats.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/RemoteCacheStats.java
new file mode 100644
index 0000000..272cbc5
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/RemoteCacheStats.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.rest.representation;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.infinispan.client.hotrod.ServerStatistics;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RemoteCacheStats {
+
+ @JsonProperty(ServerStatistics.STORES)
+ private Integer stores;
+
+ @JsonProperty("globalStores")
+ private Integer globalStores;
+
+ private Map<String, String> otherStats = new HashMap<>();
+
+
+ public Integer getStores() {
+ return stores;
+ }
+
+ public void setStores(Integer stores) {
+ this.stores = stores;
+ }
+
+ public Integer getGlobalStores() {
+ return globalStores;
+ }
+
+ public void setGlobalStores(Integer globalStores) {
+ this.globalStores = globalStores;
+ }
+
+ @JsonAnyGetter
+ public Map<String, String> getOtherStats() {
+ return otherStats;
+ }
+
+ @JsonAnySetter
+ public void setOtherStats(String name, String value) {
+ otherStats.put(name, value);
+ }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java
index b6f0b81..9847b27 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java
@@ -17,6 +17,8 @@
package org.keycloak.testsuite.rest.resource;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -28,8 +30,15 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.remoting.transport.Transport;
+import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
+import org.jgroups.JChannel;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+import org.keycloak.testsuite.rest.representation.JGroupsStats;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -77,4 +86,55 @@ public class TestCacheResource {
public void clear() {
cache.clear();
}
+
+ @GET
+ @Path("/jgroups-stats")
+ @Produces(MediaType.APPLICATION_JSON)
+ public JGroupsStats getJgroupsStats() {
+ Transport transport = cache.getCacheManager().getTransport();
+ if (transport == null) {
+ return new JGroupsStats(0, 0, 0, 0);
+ } else {
+ try {
+ // Need to use reflection due some incompatibilities between ispn 8.2.6 and 9.0.1
+ JChannel channel = (JChannel) transport.getClass().getMethod("getChannel").invoke(transport);
+
+ return new JGroupsStats(channel.getSentBytes(), channel.getSentMessages(), channel.getReceivedBytes(), channel.getReceivedMessages());
+ } catch (Exception nsme) {
+ throw new RuntimeException(nsme);
+ }
+ }
+ }
+
+
+ @GET
+ @Path("/remote-cache-stats")
+ @Produces(MediaType.APPLICATION_JSON)
+ public Map<String, String> getRemoteCacheStats() {
+ RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache);
+ if (remoteCache == null) {
+ return new HashMap<>();
+ } else {
+ return remoteCache.stats().getStatsMap();
+ }
+ }
+
+
+ @GET
+ @Path("/remote-cache-last-session-refresh/{user-session-id}")
+ @Produces(MediaType.APPLICATION_JSON)
+ public int getRemoteCacheLastSessionRefresh(@PathParam("user-session-id") String userSessionId) {
+ RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache);
+ if (remoteCache == null) {
+ return -1;
+ } else {
+ UserSessionEntity userSession = (UserSessionEntity) remoteCache.get(userSessionId);
+ if (userSession == null) {
+ return -1;
+ } else {
+ return userSession.getLastSessionRefresh();
+ }
+ }
+ }
+
}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
index f18bbbd..d5395f2 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
@@ -64,6 +64,7 @@ import org.keycloak.testsuite.runonserver.FetchOnServer;
import org.keycloak.testsuite.runonserver.RunOnServer;
import org.keycloak.testsuite.runonserver.SerializationUtil;
import org.keycloak.util.JsonSerialization;
+import org.keycloak.utils.MediaType;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
@@ -74,7 +75,6 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.text.ParseException;
import java.text.SimpleDateFormat;
@@ -170,6 +170,25 @@ public class TestingResourceProvider implements RealmResourceProvider {
}
@GET
+ @Path("/get-client-sessions-count")
+ @Produces(MediaType.APPLICATION_JSON)
+ public Integer getClientSessionsCountInUserSession(@QueryParam("realm") final String name, @QueryParam("session") final String sessionId) {
+
+ RealmManager realmManager = new RealmManager(session);
+ RealmModel realm = realmManager.getRealmByName(name);
+ if (realm == null) {
+ throw new NotFoundException("Realm not found");
+ }
+
+ UserSessionModel sessionModel = session.sessions().getUserSession(realm, sessionId);
+ if (sessionModel == null) {
+ throw new NotFoundException("Session not found");
+ }
+
+ return sessionModel.getAuthenticatedClientSessions().size();
+ }
+
+ @GET
@Path("/time-offset")
@Produces(MediaType.APPLICATION_JSON)
public Map<String, String> getTimeOffset() {
@@ -655,8 +674,8 @@ public class TestingResourceProvider implements RealmResourceProvider {
@POST
@Path("/run-on-server")
- @Consumes(MediaType.TEXT_PLAIN)
- @Produces(MediaType.TEXT_PLAIN)
+ @Consumes(MediaType.TEXT_PLAIN_UTF_8)
+ @Produces(MediaType.TEXT_PLAIN_UTF_8)
public String runOnServer(String runOnServer) throws Exception {
try {
ClassLoader cl = ModuleUtil.isModules() ? ModuleUtil.getClassLoader() : getClass().getClassLoader();
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/runonserver/SerializationUtil.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/runonserver/SerializationUtil.java
index cd1b216..1f56a80 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/runonserver/SerializationUtil.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/runonserver/SerializationUtil.java
@@ -50,7 +50,7 @@ public class SerializationUtil {
oos.writeObject(t);
oos.close();
- return Base64.encodeBytes(os.toByteArray());
+ return "EXCEPTION:" + Base64.encodeBytes(os.toByteArray());
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -58,6 +58,7 @@ public class SerializationUtil {
public static Throwable decodeException(String result) {
try {
+ result = result.substring("EXCEPTION:".length());
byte[] bytes = Base64.decode(result);
ByteArrayInputStream is = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(is);
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml b/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml
index fdcb092..ba49570 100644
--- a/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-auth-server</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java
index 3b533f1..1a4b118 100644
--- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java
@@ -208,10 +208,10 @@ public class SimpleUndertowLoadBalancer {
if (stickyHost != null) {
if (!stickyHost.isAvailable()) {
- log.infof("Sticky host %s not available. Trying different hosts", stickyHost.getUri());
+ log.debugf("Sticky host %s not available. Trying different hosts", stickyHost.getUri());
return null;
} else {
- log.infof("Sticky host %s found and looks available", stickyHost.getUri());
+ log.debugf("Sticky host %s found and looks available", stickyHost.getUri());
}
}
@@ -259,7 +259,7 @@ public class SimpleUndertowLoadBalancer {
} else {
// Host was restored
if (!host.isAvailable()) {
- log.infof("Host %s available again", host.getUri());
+ log.infof("Host %s available again after failover", host.getUri());
host.clearError();
}
}
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
index 540b4b5..b6fbd2e 100644
--- a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
@@ -29,8 +29,16 @@
/*[local-name()='cache-container' and starts-with(namespace-uri(), $nsCacheServer) and @name='local']">
<xsl:copy>
<xsl:apply-templates select="@* | node()" />
- <local-cache name="work" start="EAGER" batching="false" />
- <local-cache name="actionTokens" start="EAGER" batching="false" />
+
+ <local-cache-configuration name="sessions-cfg" start="EAGER" batching="false">
+ <transaction mode="NON_XA" locking="PESSIMISTIC"/>
+ </local-cache-configuration>
+
+ <local-cache name="sessions" configuration="sessions-cfg" />
+ <local-cache name="offlineSessions" configuration="sessions-cfg" />
+ <local-cache name="loginFailures" configuration="sessions-cfg" />
+ <local-cache name="actionTokens" configuration="sessions-cfg" />
+ <local-cache name="work" configuration="sessions-cfg" />
</xsl:copy>
</xsl:template>
@@ -38,8 +46,17 @@
/*[local-name()='cache-container' and starts-with(namespace-uri(), $nsCacheServer) and @name='clustered']">
<xsl:copy>
<xsl:apply-templates select="@* | node()" />
- <replicated-cache name="work" start="EAGER" batching="false" />
- <replicated-cache name="actionTokens" start="EAGER" batching="false" />
+
+ <replicated-cache-configuration name="sessions-cfg" mode="ASYNC" start="EAGER" batching="false">
+ <transaction mode="NON_XA" locking="PESSIMISTIC"/>
+ </replicated-cache-configuration>
+
+
+ <replicated-cache name="sessions" configuration="sessions-cfg" />
+ <replicated-cache name="offlineSessions" configuration="sessions-cfg" />
+ <replicated-cache name="loginFailures" configuration="sessions-cfg" />
+ <replicated-cache name="actionTokens" configuration="sessions-cfg" />
+ <replicated-cache name="work" configuration="sessions-cfg" />
</xsl:copy>
</xsl:template>
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml
index 3ac23ac..73735e2 100644
--- a/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-cache-server-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/pom.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/pom.xml
index 8fa5902..74b792f 100644
--- a/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/pom.xml
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-cache-server-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml
index 9c2d1f9..5b04016 100644
--- a/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-cache-server</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/cache-server/pom.xml b/testsuite/integration-arquillian/servers/cache-server/pom.xml
index 3f5a5a0..3a739e7 100644
--- a/testsuite/integration-arquillian/servers/cache-server/pom.xml
+++ b/testsuite/integration-arquillian/servers/cache-server/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/migration/pom.xml b/testsuite/integration-arquillian/servers/migration/pom.xml
index d2bda81..a8fd5ab 100644
--- a/testsuite/integration-arquillian/servers/migration/pom.xml
+++ b/testsuite/integration-arquillian/servers/migration/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/servers/pom.xml b/testsuite/integration-arquillian/servers/pom.xml
index 0b7e899..e733638 100644
--- a/testsuite/integration-arquillian/servers/pom.xml
+++ b/testsuite/integration-arquillian/servers/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -47,7 +47,7 @@
<fuse62.version>6.2.1.redhat-084</fuse62.version>
<!-- cache server versions -->
- <infinispan.version>9.0.1.Final</infinispan.version>
+ <!--<infinispan.version>9.0.1.Final</infinispan.version>--> <!-- Use same version like our infinispan version for now -->
<jdg.version>8.4.0.Final-redhat-2</jdg.version><!-- JDG 7.1.0 -->
<jboss.default.worker.io-threads>16</jboss.default.worker.io-threads>
diff --git a/testsuite/integration-arquillian/servers/wildfly-balancer/pom.xml b/testsuite/integration-arquillian/servers/wildfly-balancer/pom.xml
index dd5f7d5..3a78379 100644
--- a/testsuite/integration-arquillian/servers/wildfly-balancer/pom.xml
+++ b/testsuite/integration-arquillian/servers/wildfly-balancer/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/test-apps/app-profile-jee/pom.xml b/testsuite/integration-arquillian/test-apps/app-profile-jee/pom.xml
index d72b490..78e40d0 100644
--- a/testsuite/integration-arquillian/test-apps/app-profile-jee/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/app-profile-jee/pom.xml
@@ -5,7 +5,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-test-apps</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-test-app-profile-jee</artifactId>
diff --git a/testsuite/integration-arquillian/test-apps/cors/angular-product/pom.xml b/testsuite/integration-arquillian/test-apps/cors/angular-product/pom.xml
index f812a9f..adceab1 100755
--- a/testsuite/integration-arquillian/test-apps/cors/angular-product/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/cors/angular-product/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-test-apps-cors-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/index.html b/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/index.html
index ed3da59..516ddc1 100755
--- a/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/index.html
+++ b/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/index.html
@@ -97,6 +97,7 @@
</div>
</div>
+ <div id="headers">{{headers}}</div>
</div>
</body>
</html>
diff --git a/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/js/app.js b/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/js/app.js
index e09b058..eb760a9 100755
--- a/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/js/app.js
+++ b/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/js/app.js
@@ -73,9 +73,9 @@ module.controller('GlobalCtrl', function($scope, $http) {
$scope.realm = [];
$scope.version = [];
$scope.reloadData = function() {
- $http.get(getAppServerUrl("localhost-db") + "/cors-database/products").success(function(data) {
+ $http.get(getAppServerUrl("localhost-db") + "/cors-database/products").success(function(data, status, headers, config) {
$scope.products = angular.fromJson(data);
-
+ $scope.headers = headers();
});
};
diff --git a/testsuite/integration-arquillian/test-apps/cors/database-service/pom.xml b/testsuite/integration-arquillian/test-apps/cors/database-service/pom.xml
index 2f7f167..d4b2e4e 100755
--- a/testsuite/integration-arquillian/test-apps/cors/database-service/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/cors/database-service/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-test-apps-cors-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/java/org/keycloak/example/oauth/ProductService.java b/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/java/org/keycloak/example/oauth/ProductService.java
index 69cb58f..321f1ef 100755
--- a/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/java/org/keycloak/example/oauth/ProductService.java
+++ b/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/java/org/keycloak/example/oauth/ProductService.java
@@ -19,9 +19,11 @@ package org.keycloak.example.oauth;
import org.jboss.resteasy.annotations.cache.NoCache;
+import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
import java.util.ArrayList;
import java.util.List;
@@ -31,6 +33,10 @@ import java.util.List;
*/
@Path("products")
public class ProductService {
+
+ @Context
+ private HttpServletResponse response;
+
@GET
@Produces("application/json")
@NoCache
@@ -39,6 +45,8 @@ public class ProductService {
rtn.add("iphone");
rtn.add("ipad");
rtn.add("ipod");
+
+ response.addHeader("X-Custom1", "some-value");
return rtn;
}
}
diff --git a/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/webapp/WEB-INF/keycloak.json b/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/webapp/WEB-INF/keycloak.json
index 493176d..993d69c 100755
--- a/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/webapp/WEB-INF/keycloak.json
+++ b/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/webapp/WEB-INF/keycloak.json
@@ -5,5 +5,6 @@
"auth-server-url": "http://localhost-auth:8180/auth",
"bearer-only" : true,
"ssl-required": "external",
- "enable-cors": true
+ "enable-cors": true,
+ "cors-exposed-headers": "X-Custom1"
}
diff --git a/testsuite/integration-arquillian/test-apps/cors/pom.xml b/testsuite/integration-arquillian/test-apps/cors/pom.xml
index 2265c43..052505c 100644
--- a/testsuite/integration-arquillian/test-apps/cors/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/cors/pom.xml
@@ -5,7 +5,7 @@
<parent>
<artifactId>integration-arquillian-test-apps</artifactId>
<groupId>org.keycloak.testsuite</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml
index 5c17a5e..ccddbb2 100755
--- a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-test-apps</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>hello-world-authz-service</artifactId>
diff --git a/testsuite/integration-arquillian/test-apps/js-console/pom.xml b/testsuite/integration-arquillian/test-apps/js-console/pom.xml
index 4579922..43e0bc3 100755
--- a/testsuite/integration-arquillian/test-apps/js-console/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/js-console/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-test-apps</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/test-apps/js-database/pom.xml b/testsuite/integration-arquillian/test-apps/js-database/pom.xml
index 4c85860..729524a 100644
--- a/testsuite/integration-arquillian/test-apps/js-database/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/js-database/pom.xml
@@ -5,7 +5,7 @@
<parent>
<artifactId>integration-arquillian-test-apps</artifactId>
<groupId>org.keycloak.testsuite</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/pom.xml
index 9ea84a4..bb401d2 100755
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-test-apps-photoz-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/pom.xml
index ea54b52..c1f9d27 100755
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/pom.xml
@@ -5,7 +5,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-test-apps-photoz-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml
index 010eb28..0bd38d9 100755
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-test-apps-photoz-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/pom.xml
index 40ca552..5d5431a 100755
--- a/testsuite/integration-arquillian/test-apps/photoz/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/photoz/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-test-apps</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-test-apps-photoz-parent</artifactId>
diff --git a/testsuite/integration-arquillian/test-apps/pom.xml b/testsuite/integration-arquillian/test-apps/pom.xml
index 913b641..1afac47 100644
--- a/testsuite/integration-arquillian/test-apps/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/pom.xml
@@ -5,7 +5,7 @@
<parent>
<artifactId>integration-arquillian</artifactId>
<groupId>org.keycloak.testsuite</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml b/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml
index 41e09b3..adeb787 100755
--- a/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-test-apps</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>servlet-authz-app</artifactId>
diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml
index 6cd4610..2dfa42d 100755
--- a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-test-apps</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>servlet-policy-enforcer</artifactId>
diff --git a/testsuite/integration-arquillian/test-apps/servlets/pom.xml b/testsuite/integration-arquillian/test-apps/servlets/pom.xml
index 8ec3f96..ed876db 100644
--- a/testsuite/integration-arquillian/test-apps/servlets/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/servlets/pom.xml
@@ -5,7 +5,7 @@
<parent>
<artifactId>integration-arquillian-test-apps</artifactId>
<groupId>org.keycloak.testsuite</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java
index f5690a5..2c0b17d 100755
--- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java
+++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java
@@ -25,6 +25,7 @@ import org.keycloak.adapters.spi.AuthenticationError;
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
@@ -47,7 +48,7 @@ import java.util.List;
* @version $Revision: 1 $
*/
@Path("/")
-public class SendUsernameServlet {
+public class SendUsernameServlet extends HttpServlet {
private static boolean checkRoles = false;
private static SamlAuthenticationError authError;
diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/mvnw b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/mvnw
new file mode 100755
index 0000000..5bf251c
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/mvnw
@@ -0,0 +1,225 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven2 Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Migwn, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+ # TODO classpath?
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+echo $MAVEN_PROJECTBASEDIR
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/mvnw.cmd b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/mvnw.cmd
new file mode 100644
index 0000000..019bd74
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/mvnw.cmd
@@ -0,0 +1,143 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven2 Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/pom.xml b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/pom.xml
new file mode 100644
index 0000000..b53481b
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/pom.xml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.keycloak</groupId>
+ <artifactId>spring-boot-adapter</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ <packaging>jar</packaging>
+
+ <name>spring-boot-adapter</name>
+ <description>Spring boot adapter test application</description>
+
+ <parent>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-parent</artifactId>
+ <version>1.5.3.RELEASE</version>
+ <relativePath/> <!-- lookup parent from repository -->
+ </parent>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+ <java.version>1.8</java.version>
+
+ <keycloak.version>3.3.0.CR1-SNAPSHOT</keycloak.version>
+ </properties>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-thymeleaf</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-spring-boot-adapter</artifactId>
+ <version>${keycloak.version}</version>
+ </dependency>
+
+ </dependencies>
+
+ <profiles>
+ <profile>
+ <id>spring-boot-adapter-tomcat</id>
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-tomcat8-adapter</artifactId>
+ <version>${keycloak.version}</version>
+ </dependency>
+ </dependencies>
+ </profile>
+
+ <profile>
+ <id>spring-boot-adapter-jetty</id>
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-tomcat</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-jetty</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-jetty94-adapter</artifactId>
+ <version>${keycloak.version}</version>
+ </dependency>
+ </dependencies>
+ </profile>
+
+ <profile>
+ <id>spring-boot-adapter-undertow</id>
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-tomcat</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-undertow</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-undertow-adapter</artifactId>
+ <version>${keycloak.version}</version>
+ </dependency>
+ </dependencies>
+ </profile>
+ </profiles>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+
+
+</project>
diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/AdminController.java b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/AdminController.java
new file mode 100644
index 0000000..3b9ccc4
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/AdminController.java
@@ -0,0 +1,59 @@
+package org.keycloak;
+
+import java.io.IOException;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
+import org.keycloak.common.util.Time;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.util.JsonSerialization;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.util.NumberUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.context.request.WebRequest;
+
+@Controller
+@RequestMapping(path = "/admin")
+public class AdminController {
+
+ @RequestMapping(path = "/TokenServlet", method = RequestMethod.GET)
+ public String showTokens(WebRequest req, Model model, @RequestParam Map<String, String> attributes) throws IOException {
+ String timeOffset = attributes.get("timeOffset");
+ if (!StringUtils.isEmpty(timeOffset)) {
+ int offset;
+ try {
+ offset = Integer.parseInt(timeOffset, 10);
+ }
+ catch (NumberFormatException e) {
+ offset = 0;
+ }
+
+ Time.setOffset(offset);
+ }
+
+ RefreshableKeycloakSecurityContext ctx =
+ (RefreshableKeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName(), WebRequest.SCOPE_REQUEST);
+ String accessTokenPretty = JsonSerialization.writeValueAsPrettyString(ctx.getToken());
+ RefreshToken refreshToken;
+ try {
+ refreshToken = new JWSInput(ctx.getRefreshToken()).readJsonContent(RefreshToken.class);
+ } catch (JWSInputException e) {
+ throw new IOException(e);
+ }
+ String refreshTokenPretty = JsonSerialization.writeValueAsPrettyString(refreshToken);
+
+ model.addAttribute("accessToken", accessTokenPretty);
+ model.addAttribute("refreshToken", refreshTokenPretty);
+ model.addAttribute("accessTokenString", ctx.getTokenString());
+
+ return "tokens";
+ }
+}
diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/SpringBootAdapterApplication.java b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/SpringBootAdapterApplication.java
new file mode 100644
index 0000000..3833299
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/SpringBootAdapterApplication.java
@@ -0,0 +1,12 @@
+package org.keycloak;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class SpringBootAdapterApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootAdapterApplication.class, args);
+ }
+}
diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/application.properties b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/application.properties
new file mode 100644
index 0000000..84de1bb
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/application.properties
@@ -0,0 +1,12 @@
+server.port=8280
+
+keycloak.realm=test
+keycloak.auth-server-url=http://localhost:8180/auth
+keycloak.ssl-required=external
+keycloak.resource=spring-boot-app
+keycloak.credentials.secret=e3789ac5-bde6-4957-a7b0-612823dac101
+keycloak.realm-key=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB
+
+keycloak.security-constraints[0].authRoles[0]=admin
+keycloak.security-constraints[0].securityCollections[0].name=Admin zone
+keycloak.security-constraints[0].securityCollections[0].patterns[0]=/admin/*
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/admin/index.html b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/admin/index.html
new file mode 100644
index 0000000..acb47af
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/admin/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>springboot admin page</title>
+</head>
+<body>
+
+ <div class="test">You are now admin</div>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/index.html b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/index.html
new file mode 100644
index 0000000..5ca7303
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>springboot test page</title>
+</head>
+<body>
+
+ <div class="test">Click <a href="admin/index.html" class="adminlink">here</a> to go admin</div>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/tokens.html b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/tokens.html
new file mode 100644
index 0000000..09dee72
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/tokens.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html xmlns:th="http://www.thymeleaf.org/">
+ <head>
+ <title>Tokens from spring boot</title>
+ </head>
+ <body>
+ <span id="accessToken" th:text="${accessToken}"></span>
+ <span id="refreshToken" th:text="${refreshToken}"></span>
+ <span id="accessTokenString" th:text="${accessTokenString}"></span>
+ </body>
+</html>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/test/java/org/keycloak/SpringBootAdapterApplicationTests.java b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/test/java/org/keycloak/SpringBootAdapterApplicationTests.java
new file mode 100644
index 0000000..8df20da
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/test/java/org/keycloak/SpringBootAdapterApplicationTests.java
@@ -0,0 +1,16 @@
+package org.keycloak;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootAdapterApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/testsuite/integration-arquillian/test-apps/test-apps-dist/pom.xml b/testsuite/integration-arquillian/test-apps/test-apps-dist/pom.xml
index c025ebc..4268d88 100644
--- a/testsuite/integration-arquillian/test-apps/test-apps-dist/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/test-apps-dist/pom.xml
@@ -5,7 +5,7 @@
<parent>
<artifactId>integration-arquillian-test-apps</artifactId>
<groupId>org.keycloak.testsuite</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index caa76aa..b2c06ad 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -79,6 +79,11 @@
<scope>compile</scope>
</dependency>
<dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-all</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
<groupId>org.subethamail</groupId>
<artifactId>subethasmtp</artifactId>
<scope>compile</scope>
@@ -88,6 +93,28 @@
<artifactId>greenmail</artifactId>
<scope>compile</scope>
</dependency>
+ <!--<dependency>-->
+ <!--<groupId>com.spotify</groupId>-->
+ <!--<artifactId>docker-client</artifactId>-->
+ <!--<version>8.3.2</version>-->
+ <!--<scope>test</scope>-->
+ <!--<exclusions>-->
+ <!--<exclusion>-->
+ <!--<groupId>javax.ws.rs</groupId>-->
+ <!--<artifactId>javax.ws.rs-api</artifactId>-->
+ <!--</exclusion>-->
+ <!--<exclusion>-->
+ <!--<groupId>com.github.jnr</groupId>-->
+ <!--<artifactId>jnr-unixsocket</artifactId>-->
+ <!--</exclusion>-->
+ <!--</exclusions>-->
+ <!--</dependency>-->
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers</artifactId>
+ <version>1.2.1</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AngularCorsProductTestApp.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AngularCorsProductTestApp.java
index f49023e..cb84089 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AngularCorsProductTestApp.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AngularCorsProductTestApp.java
@@ -66,6 +66,8 @@ public class AngularCorsProductTestApp extends AbstractPageWithInjectedUrl {
@FindBy(id = "output")
private WebElement outputArea;
+ @FindBy(id = "headers")
+ private WebElement headers;
public void reloadData() {
reloadDataButton.click();
@@ -99,5 +101,9 @@ public class AngularCorsProductTestApp extends AbstractPageWithInjectedUrl {
return outputArea;
}
+ public WebElement getHeaders() {
+ return headers;
+ }
+
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostServlet.java
index cd9ea11..01b8b08 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostServlet.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostServlet.java
@@ -27,6 +27,7 @@ import java.net.URL;
*/
public class SalesPostServlet extends SAMLServlet {
public static final String DEPLOYMENT_NAME = "sales-post";
+ public static final String CLIENT_NAME = "http://localhost:8081/sales-post/";
@ArquillianResource
@OperateOnDeployment(DEPLOYMENT_NAME)
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java
index 2dd7bbc..ef15acf 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java
@@ -19,6 +19,7 @@ package org.keycloak.testsuite.arquillian.annotation;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.arquillian.InfinispanStatistics;
import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants;
+import org.keycloak.testsuite.crossdc.DC;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -48,7 +49,7 @@ public @interface JmxInfinispanCacheStatistics {
// Host address - either given by arrangement of DC ...
/** Index of the data center, starting from 0 */
- int dcIndex() default -1;
+ DC dc() default DC.UNDEFINED;
/** Index of the node within data center, starting from 0. Nodes are ordered by arquillian qualifier as per {@link AuthServerTestEnricher} */
int dcNodeIndex() default -1;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java
index 41e9f20..cddb815 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java
@@ -17,6 +17,7 @@
package org.keycloak.testsuite.arquillian.annotation;
import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants;
+import org.keycloak.testsuite.crossdc.DC;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -40,7 +41,7 @@ public @interface JmxInfinispanChannelStatistics {
// Host address - either given by arrangement of DC ...
/** Index of the data center, starting from 0 */
- int dcIndex() default -1;
+ DC dc() default DC.UNDEFINED;
/** Index of the node within data center, starting from 0. Nodes are ordered by arquillian qualifier as per {@link AuthServerTestEnricher} */
int dcNodeIndex() default -1;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AppServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AppServerTestEnricher.java
index 92646f4..bc83338 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AppServerTestEnricher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AppServerTestEnricher.java
@@ -133,6 +133,14 @@ public class AppServerTestEnricher {
return getAppServerQualifier(testClass).contains("tomcat");
}
+ public static boolean isWASAppServer(Class testClass) {
+ return getAppServerQualifier(testClass).contains("was");
+ }
+
+ public static boolean isWLSAppServer(Class testClass) {
+ return getAppServerQualifier(testClass).contains("wls");
+ }
+
public static boolean isOSGiAppServer(Class testClass) {
String q = getAppServerQualifier(testClass);
return q.contains("karaf") || q.contains("fuse");
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
index 97347d9..085c2de 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
@@ -81,8 +81,6 @@ public class AuthServerTestEnricher {
private static final String AUTH_SERVER_CROSS_DC_PROPERTY = "auth.server.crossdc";
public static final boolean AUTH_SERVER_CROSS_DC = Boolean.parseBoolean(System.getProperty(AUTH_SERVER_CROSS_DC_PROPERTY, "false"));
- private static final boolean AUTH_SERVER_UNDERTOW_CLUSTER = Boolean.parseBoolean(System.getProperty("auth.server.undertow.cluster", "false"));
-
private static final Boolean START_MIGRATION_CONTAINER = "auto".equals(System.getProperty("migration.mode")) ||
"manual".equals(System.getProperty("migration.mode"));
@@ -195,11 +193,6 @@ public class AuthServerTestEnricher {
suiteContext.setAuthServerInfo(container);
}
- // Setup with 2 undertow backend nodes and no loadbalancer.
-// if (AUTH_SERVER_UNDERTOW_CLUSTER && suiteContext.getAuthServerInfo() == null && !suiteContext.getAuthServerBackendsInfo().isEmpty()) {
-// suiteContext.setAuthServerInfo(suiteContext.getAuthServerBackendsInfo().get(0));
-// }
-
if (START_MIGRATION_CONTAINER) {
// init migratedAuthServerInfo
for (ContainerInfo container : suiteContext.getContainers()) {
@@ -268,6 +261,8 @@ public class AuthServerTestEnricher {
}
public void initializeOAuthClient(@Observes(precedence = 3) BeforeClass event) {
+ // TODO workaround. Check if can be removed
+ OAuthClient.updateURLs(suiteContext.getAuthServerInfo().getContextRoot().toString());
OAuthClient oAuthClient = new OAuthClient();
oAuthClientProducer.set(oAuthClient);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java
index 4091ca4..33af2f2 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java
@@ -33,6 +33,7 @@ import java.util.Set;
import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanChannelStatistics;
import org.keycloak.testsuite.arquillian.jmx.JmxConnectorRegistry;
import org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow;
+import org.keycloak.testsuite.crossdc.DC;
import java.io.NotSerializableException;
import java.lang.management.ManagementFactory;
import java.util.Objects;
@@ -84,7 +85,7 @@ public class CacheStatisticsControllerEnricher implements TestEnricher {
ObjectName mbeanName = new ObjectName(String.format(
"%s:type=%s,name=\"%s(%s)\",manager=\"%s\",component=%s",
- annotation.domain().isEmpty() ? getDefaultDomain(annotation.dcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN,
+ annotation.domain().isEmpty() ? getDefaultDomain(annotation.dc().getDcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN,
annotation.type(),
annotation.cacheName(),
annotation.cacheMode(),
@@ -98,8 +99,8 @@ public class CacheStatisticsControllerEnricher implements TestEnricher {
try {
Retry.execute(() -> value.reset(), 2, 150);
} catch (RuntimeException ex) {
- if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1
- && suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex()).isStarted()) {
+ if (annotation.dc() != DC.UNDEFINED && annotation.dcNodeIndex() != -1
+ && suiteContext.get().getAuthServerBackendsInfo(annotation.dc().getDcIndex()).get(annotation.dcNodeIndex()).isStarted()) {
LOG.warn("Could not reset statistics for " + mbeanName);
}
}
@@ -113,7 +114,7 @@ public class CacheStatisticsControllerEnricher implements TestEnricher {
ObjectName mbeanName = new ObjectName(String.format(
"%s:type=%s,cluster=\"%s\"",
- annotation.domain().isEmpty() ? getDefaultDomain(annotation.dcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN,
+ annotation.domain().isEmpty() ? getDefaultDomain(annotation.dc().getDcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN,
annotation.type(),
annotation.cluster()
));
@@ -124,8 +125,8 @@ public class CacheStatisticsControllerEnricher implements TestEnricher {
try {
Retry.execute(() -> value.reset(), 2, 150);
} catch (RuntimeException ex) {
- if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1
- && suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex()).isStarted()) {
+ if (annotation.dc() != DC.UNDEFINED && annotation.dcNodeIndex() != -1
+ && suiteContext.get().getAuthServerBackendsInfo(annotation.dc().getDcIndex()).get(annotation.dcNodeIndex()).isStarted()) {
LOG.warn("Could not reset statistics for " + mbeanName);
}
}
@@ -170,8 +171,8 @@ public class CacheStatisticsControllerEnricher implements TestEnricher {
final String host;
final int port;
- if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1) {
- ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex());
+ if (annotation.dc() != DC.UNDEFINED && annotation.dcNodeIndex() != -1) {
+ ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dc().getDcIndex()).get(annotation.dcNodeIndex());
Container container = node.getArquillianContainer();
if (container.getDeployableContainer() instanceof KeycloakOnUndertow) {
return ManagementFactory.getPlatformMBeanServer();
@@ -204,8 +205,8 @@ public class CacheStatisticsControllerEnricher implements TestEnricher {
final String host;
final int port;
- if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1) {
- ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex());
+ if (annotation.dc() != DC.UNDEFINED && annotation.dcNodeIndex() != -1) {
+ ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dc().getDcIndex()).get(annotation.dcNodeIndex());
Container container = node.getArquillianContainer();
if (container.getDeployableContainer() instanceof KeycloakOnUndertow) {
return ManagementFactory.getPlatformMBeanServer();
@@ -228,7 +229,10 @@ public class CacheStatisticsControllerEnricher implements TestEnricher {
: annotation.managementPort();
}
- JMXServiceURL url = new JMXServiceURL("service:jmx:remote+http://" + host + ":" + port);
+ String jmxUrl = "service:jmx:remote+http://" + host + ":" + port;
+ LOG.infof("JMX Service URL: %s", jmxUrl);
+
+ JMXServiceURL url = new JMXServiceURL(jmxUrl);
JMXConnector jmxc = jmxConnectorRegistry.get().getConnection(url);
return jmxc.getMBeanServerConnection();
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java
index 2f1f841..79b0365 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java
@@ -26,8 +26,15 @@ import org.jboss.arquillian.test.spi.annotation.ClassScoped;
import org.jboss.logging.Logger;
import org.jboss.logging.Logger.Level;
import org.jboss.shrinkwrap.api.Archive;
+import org.jboss.shrinkwrap.api.ArchivePath;
+import org.jboss.shrinkwrap.api.Filters;
+import org.jboss.shrinkwrap.api.Node;
+import org.jboss.shrinkwrap.api.asset.ClassAsset;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.jboss.shrinkwrap.resolver.api.maven.Maven;
+import org.jboss.shrinkwrap.resolver.api.maven.MavenFormatStage;
+import org.jboss.shrinkwrap.resolver.api.maven.MavenResolverSystem;
import org.keycloak.adapters.servlet.KeycloakOIDCFilter;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.testsuite.arquillian.annotation.UseServletFilter;
@@ -35,18 +42,29 @@ import org.keycloak.testsuite.util.IOUtil;
import org.keycloak.util.JsonSerialization;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
-import javax.xml.transform.TransformerException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.hasAppServerContainerAnnotation;
import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isRelative;
import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isTomcatAppServer;
+import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isWLSAppServer;
+import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isWASAppServer;
import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.getAuthServerContextRoot;
-import static org.keycloak.testsuite.util.IOUtil.*;
+import static org.keycloak.testsuite.util.IOUtil.appendChildInDocument;
+import static org.keycloak.testsuite.util.IOUtil.documentToString;
+import static org.keycloak.testsuite.util.IOUtil.getElementTextContent;
+import static org.keycloak.testsuite.util.IOUtil.loadJson;
+import static org.keycloak.testsuite.util.IOUtil.loadXML;
+import static org.keycloak.testsuite.util.IOUtil.modifyDocElementAttribute;
+import static org.keycloak.testsuite.util.IOUtil.modifyDocElementValue;
+import static org.keycloak.testsuite.util.IOUtil.removeElementsFromDoc;
+import static org.keycloak.testsuite.util.IOUtil.removeNodeByAttributeValue;
/**
@@ -86,6 +104,21 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
// } else {
// log.info(testClass.getJavaClass().getSimpleName() + " is not an AdapterTest");
// }
+ if (isWLSAppServer(testClass.getJavaClass())) {
+// {
+ MavenResolverSystem resolver = Maven.resolver();
+ MavenFormatStage dependencies = resolver
+ .loadPomFromFile("pom.xml")
+ .importTestDependencies()
+ .resolve("org.apache.httpcomponents:httpclient")
+ .withTransitivity();
+
+ ((WebArchive) archive)
+ .addAsLibraries(dependencies.asFile())
+ .addClass(org.keycloak.testsuite.arquillian.annotation.AppServerContainer.class)
+ .addClass(org.keycloak.testsuite.arquillian.annotation.UseServletFilter.class);
+ }
+
}
public static boolean isAdapterTest(TestClass testClass) {
@@ -260,11 +293,43 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
removeElementsFromDoc(webXmlDoc, "web-app", "login-config");
removeElementsFromDoc(webXmlDoc, "web-app", "security-role");
+ if (isWASAppServer(testClass.getJavaClass())) {
+ removeElementsFromDoc(webXmlDoc, "web-app", "servlet-mapping");
+ removeElementsFromDoc(webXmlDoc, "web-app", "servlet");
+ }
+
+ if (isWLSAppServer(testClass.getJavaClass())) {
- }
+ // add <servlet> tag in case it is missing
+ NodeList nodes = webXmlDoc.getElementsByTagName("servlet");
+ if (nodes.getLength() < 1) {
+ Element servlet = webXmlDoc.createElement("servlet");
+ Element servletName = webXmlDoc.createElement("servlet-name");
+ Element servletClass = webXmlDoc.createElement("servlet-class");
+ servletName.setTextContent("javax.ws.rs.core.Application");
+ servletClass.setTextContent(getServletClassName(archive));
+
+ servlet.appendChild(servletName);
+ servlet.appendChild(servletClass);
+
+ appendChildInDocument(webXmlDoc, "web-app", servlet);
+ }
+ }
+ }
archive.add(new StringAsset((documentToString(webXmlDoc))), WEBXML_PATH);
}
-
+
+ private String getServletClassName(Archive<?> archive) {
+
+ Map<ArchivePath, Node> content = archive.getContent(Filters.include(".*Servlet.class"));
+ for (ArchivePath path : content.keySet()) {
+ ClassAsset asset = (ClassAsset) content.get(path).getAsset();
+ return asset.getSource().getName();
+ }
+
+ return null;
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java
index e8852bc..f9d557b 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java
@@ -39,6 +39,7 @@ public class URLProvider extends URLResourceProvider {
protected final Logger log = Logger.getLogger(this.getClass());
+ public static final String BOUND_TO_ALL = "0.0.0.0";
public static final String LOCALHOST_ADDRESS = "127.0.0.1";
public static final String LOCALHOST_HOSTNAME = "localhost";
@@ -59,6 +60,7 @@ public class URLProvider extends URLResourceProvider {
if (url != null) {
try {
url = fixLocalhost(url);
+ url = fixBoundToAll(url);
url = removeTrailingSlash(url);
if (appServerSslRequired) {
url = fixSsl(url);
@@ -111,6 +113,14 @@ public class URLProvider extends URLResourceProvider {
return url;
}
+ public URL fixBoundToAll(URL url) throws MalformedURLException {
+ URL fixedUrl = url;
+ if (url.getHost().contains(BOUND_TO_ALL)) {
+ fixedUrl = new URL(fixedUrl.toExternalForm().replace(BOUND_TO_ALL, LOCALHOST_HOSTNAME));
+ }
+ return fixedUrl;
+ }
+
public URL fixLocalhost(URL url) throws MalformedURLException {
URL fixedUrl = url;
if (url.getHost().contains(LOCALHOST_ADDRESS)) {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java
index 684c383..533dfbb 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java
@@ -101,7 +101,7 @@ public class KeycloakTestingClient {
String encoded = SerializationUtil.encode(function);
String result = testing(realm != null ? realm : "master").runOnServer(encoded);
- if (result != null && !result.isEmpty() && !result.trim().startsWith("{")) {
+ if (result != null && !result.isEmpty() && result.trim().startsWith("EXCEPTION:")) {
Throwable t = SerializationUtil.decodeException(result);
if (t instanceof AssertionError) {
throw (AssertionError) t;
@@ -117,7 +117,7 @@ public class KeycloakTestingClient {
String encoded = SerializationUtil.encode(function);
String result = testing(realm != null ? realm : "master").runOnServer(encoded);
- if (result != null && !result.isEmpty() && !result.trim().startsWith("{")) {
+ if (result != null && !result.isEmpty() && result.trim().startsWith("EXCEPTION:")) {
Throwable t = SerializationUtil.decodeException(result);
if (t instanceof AssertionError) {
throw (AssertionError) t;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java
index 4561c99..e1aee2a 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java
@@ -17,6 +17,7 @@
package org.keycloak.testsuite.client.resources;
+import java.util.Map;
import java.util.Set;
import javax.ws.rs.Consumes;
@@ -26,6 +27,9 @@ import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
+import org.keycloak.testsuite.rest.representation.JGroupsStats;
+import org.keycloak.testsuite.rest.representation.RemoteCacheStats;
+
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@@ -53,4 +57,20 @@ public interface TestingCacheResource {
@Path("/clear")
@Consumes(MediaType.TEXT_PLAIN)
void clear();
+
+ @GET
+ @Path("/jgroups-stats")
+ @Produces(MediaType.APPLICATION_JSON)
+ JGroupsStats getJgroupsStats();
+
+ @GET
+ @Path("/remote-cache-stats")
+ @Produces(MediaType.APPLICATION_JSON)
+ RemoteCacheStats getRemoteCacheStats();
+
+ @GET
+ @Path("/remote-cache-last-session-refresh/{user-session-id}")
+ @Produces(MediaType.APPLICATION_JSON)
+ int getRemoteCacheLastSessionRefresh(@PathParam("user-session-id") String userSessionId);
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
index 32b05e7..2787c0e 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
@@ -192,6 +192,11 @@ public interface TestingResource {
@Produces(MediaType.APPLICATION_JSON)
void removeExpired(@QueryParam("realm") final String realm);
+ @GET
+ @Path("/get-client-sessions-count")
+ @Produces(MediaType.APPLICATION_JSON)
+ Integer getClientSessionsCountInUserSession(@QueryParam("realm") final String realmName, @QueryParam("session") final String sessionId);
+
@Path("/cache/{cache}")
TestingCacheResource cache(@PathParam("cache") String cacheName);
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java
index 9e582d8..bc089a9 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java
@@ -16,7 +16,9 @@
*/
package org.keycloak.testsuite.console.page.fragment;
+import org.jboss.arquillian.graphene.fragment.Root;
import org.keycloak.testsuite.page.AbstractAlert;
+import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -44,6 +46,9 @@ public class AdminConsoleAlert extends AbstractAlert {
public void close() {
closeButton.click();
+ WaitUtils.pause(500); // Sometimes, when a test is too fast,
+ // one of the consecutive alerts is not displayed;
+ // to prevent this we need to slow down a bit
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/crossdc/DC.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/crossdc/DC.java
new file mode 100644
index 0000000..1ed8cad
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/crossdc/DC.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.crossdc;
+
+/**
+ * Identifier of datacentre in the testsuite
+ * @author hmlnarik
+ */
+public enum DC {
+ FIRST,
+ SECOND,
+ UNDEFINED;
+
+ public int getDcIndex() {
+ return ordinal();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java
index 11aac0b..8c647cc 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java
@@ -61,6 +61,14 @@ public class AccountApplicationsPage extends AbstractAccountPage {
case 1:
currentEntry = new AppEntry();
String client = col.getText();
+ WebElement link = null;
+ try {
+ link = col.findElement(By.tagName("a"));
+ String href = link.getAttribute("href");
+ currentEntry.setHref(href);
+ } catch (Exception e) {
+ //ignore
+ }
table.put(client, currentEntry);
break;
case 2:
@@ -111,6 +119,7 @@ public class AccountApplicationsPage extends AbstractAccountPage {
private final List<String> rolesGranted = new ArrayList<String>();
private final List<String> protocolMappersGranted = new ArrayList<String>();
private final List<String> additionalGrants = new ArrayList<>();
+ private String href = null;
private void addAvailableRole(String role) {
rolesAvailable.add(role);
@@ -127,6 +136,14 @@ public class AccountApplicationsPage extends AbstractAccountPage {
private void addAdditionalGrant(String grant) {
additionalGrants.add(grant);
}
+
+ public void setHref(String href) {
+ this.href = href;
+ }
+
+ public String getHref() {
+ return this.href;
+ }
public List<String> getRolesGranted() {
return rolesGranted;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ProceedPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ProceedPage.java
new file mode 100644
index 0000000..97d7c28
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ProceedPage.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.pages;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class ProceedPage extends AbstractPage {
+
+ @FindBy(className = "instruction")
+ private WebElement infoMessage;
+
+ @FindBy(linkText = "» Click here to proceed")
+ private WebElement proceedLink;
+
+ public String getInfo() {
+ return infoMessage.getText();
+ }
+
+ public boolean isCurrent() {
+ return driver.getPageSource().contains("kc-info-message") && proceedLink.isDisplayed();
+ }
+
+ @Override
+ public void open() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void clickProceedLink() {
+ proceedLink.click();
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java
index ad71d38..84b8282 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java
@@ -25,6 +25,10 @@ import org.keycloak.common.Profile;
*/
public class ProfileAssume {
+ public static void assumeFeatureEnabled(Profile.Feature feature) {
+ Assume.assumeTrue("Ignoring test as " + feature.name() + " is not enabled", Profile.isFeatureEnabled(feature));
+ }
+
public static void assumePreview() {
Assume.assumeTrue("Ignoring test as community/preview profile is not enabled", !Profile.getName().equals("product"));
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java
index 3208f02..5b15a3f 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java
@@ -17,19 +17,61 @@
package org.keycloak.testsuite;
+import java.util.function.Supplier;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Retry {
- public static void execute(Runnable runnable, int retryCount, long intervalMillis) {
+ /**
+ * Runs the given {@code runnable} at most {@code retryCount} times until it passes,
+ * leaving {@code intervalMillis} milliseconds between the invocations.
+ * The runnable is reexecuted if it throws a {@link RuntimeException} or {@link AssertionError}.
+ * @param runnable
+ * @param attemptsCount Total number of attempts to execute the {@code runnable}
+ * @param intervalMillis
+ * @return Index of the first successful invocation, starting from 0.
+ */
+ public static int execute(Runnable runnable, int attemptsCount, long intervalMillis) {
+ int executionIndex = 0;
while (true) {
try {
runnable.run();
- return;
- } catch (RuntimeException e) {
- retryCount--;
- if (retryCount > 0) {
+ return executionIndex;
+ } catch (RuntimeException | AssertionError e) {
+ attemptsCount--;
+ executionIndex++;
+ if (attemptsCount > 0) {
+ try {
+ Thread.sleep(intervalMillis);
+ } catch (InterruptedException ie) {
+ ie.addSuppressed(e);
+ throw new RuntimeException(ie);
+ }
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+
+ /**
+ * Runs the given {@code runnable} at most {@code retryCount} times until it passes,
+ * leaving {@code intervalMillis} milliseconds between the invocations.
+ * The runnable is reexecuted if it throws a {@link RuntimeException} or {@link AssertionError}.
+ * @param supplier
+ * @param attemptsCount Total number of attempts to execute the {@code runnable}
+ * @param intervalMillis
+ * @return Value generated by the {@code supplier}.
+ */
+ public static <T> T call(Supplier<T> supplier, int attemptsCount, long intervalMillis) {
+ while (true) {
+ try {
+ return supplier.get();
+ } catch (RuntimeException | AssertionError e) {
+ attemptsCount--;
+ if (attemptsCount > 0) {
try {
Thread.sleep(intervalMillis);
} catch (InterruptedException ie) {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/IdentityProviderAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/IdentityProviderAttributeUpdater.java
new file mode 100644
index 0000000..b8eb487
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/IdentityProviderAttributeUpdater.java
@@ -0,0 +1,55 @@
+package org.keycloak.testsuite.updaters;
+
+import org.keycloak.admin.client.resource.IdentityProviderResource;
+import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import java.io.Closeable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class IdentityProviderAttributeUpdater {
+
+ private final Map<String, String> originalAttributes = new HashMap<>();
+
+ private final IdentityProviderResource identityProviderResource;
+
+ private final IdentityProviderRepresentation rep;
+
+ public IdentityProviderAttributeUpdater(IdentityProviderResource identityProviderResource) {
+ this.identityProviderResource = identityProviderResource;
+ this.rep = identityProviderResource.toRepresentation();
+ if (this.rep.getConfig() == null) {
+ this.rep.setConfig(new HashMap<>());
+ }
+ }
+
+ public IdentityProviderAttributeUpdater setAttribute(String name, String value) {
+ if (! originalAttributes.containsKey(name)) {
+ this.originalAttributes.put(name, this.rep.getConfig().put(name, value));
+ } else {
+ this.rep.getConfig().put(name, value);
+ }
+ return this;
+ }
+
+ public IdentityProviderAttributeUpdater removeAttribute(String name) {
+ if (! originalAttributes.containsKey(name)) {
+ this.originalAttributes.put(name, this.rep.getConfig().put(name, null));
+ } else {
+ this.rep.getConfig().put(name, null);
+ }
+ return this;
+ }
+
+ public Closeable update() {
+ identityProviderResource.update(rep);
+
+ return () -> {
+ rep.getConfig().putAll(originalAttributes);
+ identityProviderResource.update(rep);
+ };
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
index bc0b787..7ebaa1d 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
@@ -43,6 +43,10 @@ public class GreenMailRule extends ExternalResource {
greenMail.start();
}
+ public void credentials(String username, String password) {
+ greenMail.setUser(username, password);
+ }
+
@Override
protected void after() {
if (greenMail != null) {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/IOUtil.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/IOUtil.java
index 734a4fc..1707ef7 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/IOUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/IOUtil.java
@@ -29,7 +29,6 @@ import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 207a317..e577758 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -50,9 +50,11 @@ import org.keycloak.representations.IDToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.KeysMetadataRepresentation;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.arquillian.SuiteContext;
import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.JsonSerialization;
import org.keycloak.util.TokenUtil;
+import com.google.common.base.Charsets;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
@@ -73,11 +75,23 @@ import java.util.*;
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public class OAuthClient {
- public static final String SERVER_ROOT = AuthServerTestEnricher.getAuthServerContextRoot();
- public static String AUTH_SERVER_ROOT = SERVER_ROOT + "/auth";
- public static final String APP_ROOT = AUTH_SERVER_ROOT + "/realms/master/app";
+ public static String SERVER_ROOT;
+ public static String AUTH_SERVER_ROOT;
+ public static String APP_ROOT;
private static final boolean sslRequired = Boolean.parseBoolean(System.getProperty("auth.server.ssl.required"));
+ static {
+ updateURLs(AuthServerTestEnricher.getAuthServerContextRoot());
+ }
+
+ // Workaround, but many tests directly use system properties like OAuthClient.AUTH_SERVER_ROOT instead of taking the URL from suite context
+ public static void updateURLs(String serverRoot) {
+ SERVER_ROOT = serverRoot;
+ AUTH_SERVER_ROOT = SERVER_ROOT + "/auth";
+ APP_ROOT = AUTH_SERVER_ROOT + "/realms/master/app";
+ }
+
+
private Keycloak adminClient;
private WebDriver driver;
@@ -190,6 +204,7 @@ public class OAuthClient {
}
public void fillLoginForm(String username, String password) {
+ WaitUtils.waitForPageToLoad(driver);
String src = driver.getPageSource();
try {
driver.findElement(By.id("username")).sendKeys(username);
@@ -237,8 +252,7 @@ public class OAuthClient {
}
public AccessTokenResponse doAccessTokenRequest(String code, String password) {
- CloseableHttpClient client = newCloseableHttpClient();
- try {
+ try (CloseableHttpClient client = newCloseableHttpClient()) {
HttpPost post = new HttpPost(getAccessTokenUrl());
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
@@ -270,12 +284,7 @@ public class OAuthClient {
parameters.add(new BasicNameValuePair(OAuth2Constants.CODE_VERIFIER, codeVerifier));
}
- UrlEncodedFormEntity formEntity = null;
- try {
- formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- throw new RuntimeException(e);
- }
+ UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, Charsets.UTF_8);
post.setEntity(formEntity);
try {
@@ -283,8 +292,8 @@ public class OAuthClient {
} catch (Exception e) {
throw new RuntimeException("Failed to retrieve access token", e);
}
- } finally {
- closeClient(client);
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
}
}
@@ -297,8 +306,7 @@ public class OAuthClient {
}
public String introspectTokenWithClientCredential(String clientId, String clientSecret, String tokenType, String tokenToIntrospect) {
- CloseableHttpClient client = new DefaultHttpClient();
- try {
+ try (CloseableHttpClient client = new DefaultHttpClient()) {
HttpPost post = new HttpPost(getTokenIntrospectionUrl());
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
@@ -319,19 +327,16 @@ public class OAuthClient {
post.setEntity(formEntity);
- try {
+ try (CloseableHttpResponse response = client.execute(post)) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- CloseableHttpResponse response = client.execute(post);
response.getEntity().writeTo(out);
- response.close();
-
return new String(out.toByteArray());
} catch (Exception e) {
throw new RuntimeException("Failed to retrieve access token", e);
}
- } finally {
- closeClient(client);
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
}
}
@@ -389,6 +394,51 @@ public class OAuthClient {
}
}
+ public AccessTokenResponse doTokenExchange(String realm, String token, String targetAudience,
+ String clientId, String clientSecret) throws Exception {
+ CloseableHttpClient client = newCloseableHttpClient();
+ try {
+ HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
+
+ List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.SUBJECT_TOKEN, token));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, targetAudience));
+
+ if (clientSecret != null) {
+ String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
+ post.setHeader("Authorization", authorization);
+ } else {
+ parameters.add(new BasicNameValuePair("client_id", clientId));
+
+ }
+
+ if (clientSessionState != null) {
+ parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, clientSessionState));
+ }
+ if (clientSessionHost != null) {
+ parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, clientSessionHost));
+ }
+ if (scope != null) {
+ parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope));
+ }
+
+ UrlEncodedFormEntity formEntity;
+ try {
+ formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ post.setEntity(formEntity);
+
+ return new AccessTokenResponse(client.execute(post));
+ } finally {
+ closeClient(client);
+ }
+ }
+
+
public JSONWebKeySet doCertsRequest(String realm) throws Exception {
CloseableHttpClient client = new DefaultHttpClient();
try {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateAuthnRequestStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateAuthnRequestStepBuilder.java
new file mode 100644
index 0000000..aa85821
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateAuthnRequestStepBuilder.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.util.saml;
+
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
+import org.keycloak.saml.common.exceptions.ConfigurationException;
+import org.keycloak.saml.common.exceptions.ParsingException;
+import org.keycloak.saml.common.exceptions.ProcessingException;
+import org.keycloak.saml.common.util.DocumentUtil;
+import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
+import org.keycloak.testsuite.util.SamlClient.Binding;
+import java.net.URI;
+import java.util.UUID;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.w3c.dom.Document;
+
+
+public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<AuthnRequestType, CreateAuthnRequestStepBuilder> {
+
+ private final String issuer;
+ private final URI authServerSamlUrl;
+ private final Binding requestBinding;
+ private final String assertionConsumerURL;
+
+ private final Document forceLoginRequestDocument;
+
+ private String relayState;
+
+ public CreateAuthnRequestStepBuilder(URI authServerSamlUrl, String issuer, String assertionConsumerURL, Binding requestBinding, SamlClientBuilder clientBuilder) {
+ super(clientBuilder);
+ this.issuer = issuer;
+ this.authServerSamlUrl = authServerSamlUrl;
+ this.requestBinding = requestBinding;
+ this.assertionConsumerURL = assertionConsumerURL;
+
+ this.forceLoginRequestDocument = null;
+ }
+
+ public CreateAuthnRequestStepBuilder(URI authServerSamlUrl, Document loginRequestDocument, Binding requestBinding, SamlClientBuilder clientBuilder) {
+ super(clientBuilder);
+ this.forceLoginRequestDocument = loginRequestDocument;
+
+ this.authServerSamlUrl = authServerSamlUrl;
+ this.requestBinding = requestBinding;
+
+ this.issuer = null;
+ this.assertionConsumerURL = null;
+ }
+
+ public String assertionConsumerURL() {
+ return assertionConsumerURL;
+ }
+
+ public String relayState() {
+ return relayState;
+ }
+
+ public void relayState(String relayState) {
+ this.relayState = relayState;
+ }
+
+ @Override
+ public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
+ Document doc = createLoginRequestDocument();
+
+ String documentAsString = DocumentUtil.getDocumentAsString(doc);
+ String transformed = getTransformer().transform(documentAsString);
+
+ if (transformed == null) {
+ return null;
+ }
+
+ return requestBinding.createSamlUnsignedRequest(authServerSamlUrl, relayState, DocumentUtil.getDocument(transformed));
+ }
+
+ protected Document createLoginRequestDocument() {
+ if (this.forceLoginRequestDocument != null) {
+ return this.forceLoginRequestDocument;
+ }
+
+ try {
+ SAML2Request samlReq = new SAML2Request();
+ AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, this.authServerSamlUrl.toString(), issuer);
+
+ return SAML2Request.convert(loginReq);
+ } catch (ConfigurationException | ParsingException | ProcessingException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java
new file mode 100644
index 0000000..ee594d0
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.util.saml;
+
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import org.keycloak.dom.saml.v2.assertion.NameIDType;
+import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
+import org.keycloak.saml.SAML2LogoutRequestBuilder;
+import org.keycloak.saml.common.util.DocumentUtil;
+import org.keycloak.testsuite.util.SamlClient.Binding;
+import java.net.URI;
+import java.util.function.Supplier;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder<LogoutRequestType, CreateLogoutRequestStepBuilder> {
+
+ private final URI authServerSamlUrl;
+ private final String issuer;
+ private final Binding requestBinding;
+
+ private Supplier<String> sessionIndex = () -> null;
+ private Supplier<NameIDType> nameId = () -> null;
+ private Supplier<String> relayState = () -> null;
+
+ public CreateLogoutRequestStepBuilder(URI authServerSamlUrl, String issuer, Binding requestBinding, SamlClientBuilder clientBuilder) {
+ super(clientBuilder);
+ this.authServerSamlUrl = authServerSamlUrl;
+ this.issuer = issuer;
+ this.requestBinding = requestBinding;
+ }
+
+ public String sessionIndex() {
+ return sessionIndex.get();
+ }
+
+ public CreateLogoutRequestStepBuilder sessionIndex(String sessionIndex) {
+ this.sessionIndex = () -> sessionIndex;
+ return this;
+ }
+
+ public CreateLogoutRequestStepBuilder sessionIndex(Supplier<String> sessionIndex) {
+ this.sessionIndex = sessionIndex;
+ return this;
+ }
+
+ public String relayState() {
+ return relayState.get();
+ }
+
+ public CreateLogoutRequestStepBuilder relayState(String relayState) {
+ this.relayState = () -> relayState;
+ return this;
+ }
+
+ public CreateLogoutRequestStepBuilder relayState(Supplier<String> relayState) {
+ this.relayState = relayState;
+ return this;
+ }
+
+ public NameIDType nameId() {
+ return nameId.get();
+ }
+
+ public CreateLogoutRequestStepBuilder nameId(NameIDType nameId) {
+ this.nameId = () -> nameId;
+ return this;
+ }
+
+ public CreateLogoutRequestStepBuilder nameId(Supplier<NameIDType> nameId) {
+ this.nameId = nameId;
+ return this;
+ }
+
+ @Override
+ public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
+ SAML2LogoutRequestBuilder builder = new SAML2LogoutRequestBuilder()
+ .destination(authServerSamlUrl.toString())
+ .issuer(issuer)
+ .sessionIndex(sessionIndex());
+
+ if (nameId() != null) {
+ builder = builder.userPrincipal(nameId().getValue(), nameId().getFormat().toString());
+ }
+
+ String documentAsString = DocumentUtil.getDocumentAsString(builder.buildDocument());
+ String transformed = getTransformer().transform(documentAsString);
+
+ if (transformed == null) {
+ return null;
+ }
+
+ return requestBinding.createSamlUnsignedRequest(authServerSamlUrl, relayState(), DocumentUtil.getDocument(transformed));
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/IdPInitiatedLoginBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/IdPInitiatedLoginBuilder.java
new file mode 100644
index 0000000..d119e64
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/IdPInitiatedLoginBuilder.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.util.saml;
+
+import org.keycloak.testsuite.util.SamlClient.Step;
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import java.net.URI;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class IdPInitiatedLoginBuilder implements Step {
+
+ private final SamlClientBuilder clientBuilder;
+ private final URI authServerSamlUrl;
+ private final String clientId;
+
+ public IdPInitiatedLoginBuilder(URI authServerSamlUrl, String clientId, SamlClientBuilder clientBuilder) {
+ this.clientBuilder = clientBuilder;
+ this.authServerSamlUrl = authServerSamlUrl;
+ this.clientId = clientId;
+ }
+
+ @Override
+ public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
+ return new HttpGet(authServerSamlUrl.toString() + "/clients/" + this.clientId);
+ }
+
+ public SamlClientBuilder build() {
+ return this.clientBuilder;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/LoginBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/LoginBuilder.java
new file mode 100644
index 0000000..4e5713b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/LoginBuilder.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.util.saml;
+
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.util.SamlClient.Step;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.admin.Users.getPasswordOf;
+import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class LoginBuilder implements Step {
+
+ private final SamlClientBuilder clientBuilder;
+ private UserRepresentation user;
+ private boolean sso = false;
+
+ public LoginBuilder(SamlClientBuilder clientBuilder) {
+ this.clientBuilder = clientBuilder;
+ }
+
+ @Override
+ public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
+ if (sso) {
+ return null; // skip this step
+ } else {
+ assertThat(currentResponse, statusCodeIsHC(Response.Status.OK));
+ String loginPageText = EntityUtils.toString(currentResponse.getEntity(), "UTF-8");
+ assertThat(loginPageText, containsString("login"));
+
+ return handleLoginPage(loginPageText);
+ }
+ }
+
+ public SamlClientBuilder build() {
+ return this.clientBuilder;
+ }
+
+ public LoginBuilder user(UserRepresentation user) {
+ this.user = user;
+ return this;
+ }
+
+ public LoginBuilder sso(boolean sso) {
+ this.sso = sso;
+ return this;
+ }
+
+ /**
+ * Prepares a GET/POST request for logging the given user into the given login page. The login page is expected
+ * to have at least input fields with id "username" and "password".
+ *
+ * @param user
+ * @param loginPage
+ * @return
+ */
+ private HttpUriRequest handleLoginPage(String loginPage) {
+ return handleLoginPage(user, loginPage);
+ }
+
+ public static HttpUriRequest handleLoginPage(UserRepresentation user, String loginPage) {
+ String username = user.getUsername();
+ String password = getPasswordOf(user);
+ org.jsoup.nodes.Document theLoginPage = Jsoup.parse(loginPage);
+
+ List<NameValuePair> parameters = new LinkedList<>();
+ for (Element form : theLoginPage.getElementsByTag("form")) {
+ String method = form.attr("method");
+ String action = form.attr("action");
+ boolean isPost = method != null && "post".equalsIgnoreCase(method);
+
+ for (Element input : form.getElementsByTag("input")) {
+ if (Objects.equals(input.id(), "username")) {
+ parameters.add(new BasicNameValuePair(input.attr("name"), username));
+ } else if (Objects.equals(input.id(), "password")) {
+ parameters.add(new BasicNameValuePair(input.attr("name"), password));
+ } else {
+ parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
+ }
+ }
+
+ if (isPost) {
+ HttpPost res = new HttpPost(action);
+
+ UrlEncodedFormEntity formEntity;
+ try {
+ formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ res.setEntity(formEntity);
+
+ return res;
+ } else {
+ UriBuilder b = UriBuilder.fromPath(action);
+ for (NameValuePair parameter : parameters) {
+ b.queryParam(parameter.getName(), parameter.getValue());
+ }
+ return new HttpGet(b.build());
+ }
+ }
+
+ throw new IllegalArgumentException("Invalid login form: " + loginPage);
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/ModifySamlResponseStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/ModifySamlResponseStepBuilder.java
new file mode 100644
index 0000000..e29091b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/ModifySamlResponseStepBuilder.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.util.saml;
+
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import org.keycloak.dom.saml.v2.SAML2Object;
+import org.keycloak.saml.common.constants.GeneralConstants;
+import org.keycloak.saml.processing.web.util.PostBindingUtil;
+import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
+import org.keycloak.testsuite.util.SamlClient.Binding;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import javax.ws.rs.core.Response.Status;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
+
+
+public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2Object, ModifySamlResponseStepBuilder> {
+
+ private final Binding binding;
+
+ private URI targetUri;
+ private String targetAttribute;
+ private Binding targetBinding;
+
+ public ModifySamlResponseStepBuilder(Binding binding, SamlClientBuilder clientBuilder) {
+ super(clientBuilder);
+ this.binding = binding;
+ this.targetBinding = binding;
+ }
+
+ // TODO: support for signing
+ @Override
+ public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
+ switch (binding) {
+ case REDIRECT:
+ return handleRedirectBinding(currentResponse);
+
+ case POST:
+ return handlePostBinding(currentResponse);
+ }
+
+ throw new RuntimeException("Unknown binding for " + ModifySamlResponseStepBuilder.class.getName());
+ }
+
+ public Binding targetBinding() {
+ return targetBinding;
+ }
+
+ public ModifySamlResponseStepBuilder targetBinding(Binding targetBinding) {
+ this.targetBinding = targetBinding;
+ return this;
+ }
+
+ public String targetAttribute() {
+ return targetAttribute;
+ }
+
+ public ModifySamlResponseStepBuilder targetAttribute(String attribute) {
+ targetAttribute = attribute;
+ return this;
+ }
+
+ public ModifySamlResponseStepBuilder targetAttributeSamlRequest() {
+ return targetAttribute(GeneralConstants.SAML_REQUEST_KEY);
+ }
+
+ public ModifySamlResponseStepBuilder targetAttributeSamlResponse() {
+ return targetAttribute(GeneralConstants.SAML_RESPONSE_KEY);
+ }
+
+ public URI targetUri() {
+ return targetUri;
+ }
+
+ public ModifySamlResponseStepBuilder targetUri(URI forceUri) {
+ this.targetUri = forceUri;
+ return this;
+ }
+
+ protected HttpUriRequest handleRedirectBinding(CloseableHttpResponse currentResponse) throws Exception, IOException, URISyntaxException {
+ NameValuePair samlParam = null;
+
+ assertThat(currentResponse, statusCodeIsHC(Status.FOUND));
+ String location = currentResponse.getFirstHeader("Location").getValue();
+ URI locationUri = URI.create(location);
+
+ List<NameValuePair> params = URLEncodedUtils.parse(locationUri, "UTF-8");
+ for (Iterator<NameValuePair> it = params.iterator(); it.hasNext();) {
+ NameValuePair param = it.next();
+ if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) {
+ assertThat("Only one SAMLRequest/SAMLResponse check", samlParam, nullValue());
+ samlParam = param;
+ it.remove();
+ }
+ }
+
+ assertThat(samlParam, notNullValue());
+
+ String base64EncodedSamlDoc = samlParam.getValue();
+ InputStream decoded = RedirectBindingUtil.base64DeflateDecode(base64EncodedSamlDoc);
+ String samlDoc = IOUtils.toString(decoded, GeneralConstants.SAML_CHARSET);
+ IOUtils.closeQuietly(decoded);
+
+ String transformed = getTransformer().transform(samlDoc);
+ if (transformed == null) {
+ return null;
+ }
+
+ final String attrName = this.targetAttribute != null ? this.targetAttribute : samlParam.getName();
+
+ return createRequest(locationUri, attrName, transformed, params);
+ }
+
+ private HttpUriRequest handlePostBinding(CloseableHttpResponse currentResponse) throws Exception {
+ assertThat(currentResponse, statusCodeIsHC(Status.OK));
+
+ org.jsoup.nodes.Document theResponsePage = Jsoup.parse(EntityUtils.toString(currentResponse.getEntity()));
+ Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]");
+ Elements samlRequests = theResponsePage.select("input[name=SAMLRequest]");
+ Elements forms = theResponsePage.select("form");
+ Elements relayStates = theResponsePage.select("input[name=RelayState]");
+ int size = samlResponses.size() + samlRequests.size();
+ assertThat("Checking uniqueness of SAMLResponse/SAMLRequest input field in the page", size, is(1));
+ assertThat("Checking uniqueness of forms in the page", forms, hasSize(1));
+
+ Element respElement = samlResponses.isEmpty() ? samlRequests.first() : samlResponses.first();
+ Element form = forms.first();
+
+ String base64EncodedSamlDoc = respElement.val();
+ InputStream decoded = PostBindingUtil.base64DecodeAsStream(base64EncodedSamlDoc);
+ String samlDoc = IOUtils.toString(decoded, GeneralConstants.SAML_CHARSET);
+ IOUtils.closeQuietly(decoded);
+
+ String transformed = getTransformer().transform(samlDoc);
+ if (transformed == null) {
+ return null;
+ }
+
+ final String attributeName = this.targetAttribute != null
+ ? this.targetAttribute
+ : respElement.attr("name");
+ List<NameValuePair> parameters = new LinkedList<>();
+
+ if (! relayStates.isEmpty()) {
+ parameters.add(new BasicNameValuePair(GeneralConstants.RELAY_STATE, relayStates.first().val()));
+ }
+ URI locationUri = this.targetUri != null
+ ? this.targetUri
+ : URI.create(form.attr("action"));
+
+ return createRequest(locationUri, attributeName, transformed, parameters);
+ }
+
+ protected HttpUriRequest createRequest(URI locationUri, String attributeName, String transformed, List<NameValuePair> parameters) throws IOException, URISyntaxException {
+ switch (this.targetBinding) {
+ case POST:
+ return createPostRequest(locationUri, attributeName, transformed, parameters);
+ case REDIRECT:
+ return createRedirectRequest(locationUri, attributeName, transformed, parameters);
+ }
+ throw new RuntimeException("Unknown target binding for " + ModifySamlResponseStepBuilder.class.getName());
+ }
+
+ protected HttpUriRequest createRedirectRequest(URI locationUri, String attributeName, String transformed, List<NameValuePair> parameters) throws IOException, URISyntaxException {
+ final byte[] responseBytes = transformed.getBytes(GeneralConstants.SAML_CHARSET);
+ parameters.add(new BasicNameValuePair(attributeName, RedirectBindingUtil.deflateBase64Encode(responseBytes)));
+
+ if (this.targetUri != null) {
+ locationUri = this.targetUri;
+ }
+
+ URI target = new URIBuilder(locationUri).setParameters(parameters).build();
+
+ return new HttpGet(target);
+ }
+
+ protected HttpUriRequest createPostRequest(URI locationUri, String attributeName, String transformed, List<NameValuePair> parameters) throws IOException {
+ HttpPost post = new HttpPost(locationUri);
+
+ parameters.add(new BasicNameValuePair(attributeName, PostBindingUtil.base64Encode(transformed)));
+
+ UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, GeneralConstants.SAML_CHARSET);
+ post.setEntity(formEntity);
+
+ return post;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/RequiredConsentBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/RequiredConsentBuilder.java
new file mode 100644
index 0000000..ee24b06
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/RequiredConsentBuilder.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.util.saml;
+
+import org.keycloak.testsuite.util.SamlClient.Step;
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class RequiredConsentBuilder implements Step {
+
+ private final SamlClientBuilder clientBuilder;
+ private boolean approveConsent = true;
+
+ public RequiredConsentBuilder(SamlClientBuilder clientBuilder) {
+ this.clientBuilder = clientBuilder;
+ }
+
+ @Override
+ public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
+ assertThat(currentResponse, statusCodeIsHC(Response.Status.OK));
+ String consentPageText = EntityUtils.toString(currentResponse.getEntity(), "UTF-8");
+ assertThat(consentPageText, containsString("consent"));
+
+ return handleConsentPage(consentPageText, currentURI);
+ }
+
+ public SamlClientBuilder build() {
+ return this.clientBuilder;
+ }
+
+ public RequiredConsentBuilder approveConsent(boolean shouldApproveConsent) {
+ this.approveConsent = shouldApproveConsent;
+ return this;
+ }
+
+ /**
+ * Prepares a GET/POST request for consent granting . The consent page is expected
+ * to have at least input fields with id "kc-login" and "kc-cancel".
+ *
+ * @param consentPage
+ * @param consent
+ * @return
+ */
+ public HttpUriRequest handleConsentPage(String consentPage, URI currentURI) {
+ org.jsoup.nodes.Document theLoginPage = Jsoup.parse(consentPage);
+
+ List<NameValuePair> parameters = new LinkedList<>();
+ for (Element form : theLoginPage.getElementsByTag("form")) {
+ String method = form.attr("method");
+ String action = form.attr("action");
+ boolean isPost = method != null && "post".equalsIgnoreCase(method);
+
+ for (Element input : form.getElementsByTag("input")) {
+ if (Objects.equals(input.id(), "kc-login")) {
+ if (approveConsent)
+ parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value")));
+ } else if (Objects.equals(input.id(), "kc-cancel")) {
+ if (!approveConsent)
+ parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value")));
+ } else {
+ parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
+ }
+ }
+
+ if (isPost) {
+ HttpPost res = new HttpPost(currentURI.resolve(action));
+
+ UrlEncodedFormEntity formEntity;
+ try {
+ formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ res.setEntity(formEntity);
+
+ return res;
+ } else {
+ UriBuilder b = UriBuilder.fromPath(action);
+ for (NameValuePair parameter : parameters) {
+ b.queryParam(parameter.getName(), parameter.getValue());
+ }
+ return new HttpGet(b.build());
+ }
+ }
+
+ throw new IllegalArgumentException("Invalid consent page: " + consentPage);
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/SamlDocumentStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/SamlDocumentStepBuilder.java
new file mode 100644
index 0000000..8b8fde0
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/SamlDocumentStepBuilder.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.util.saml;
+
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import org.keycloak.dom.saml.v2.SAML2Object;
+import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType;
+import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
+import org.keycloak.dom.saml.v2.protocol.AttributeQueryType;
+import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
+import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
+import org.keycloak.dom.saml.v2.protocol.ResponseType;
+import org.keycloak.saml.common.constants.GeneralConstants;
+import org.keycloak.saml.common.util.DocumentUtil;
+import org.keycloak.saml.common.util.StaxUtil;
+import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
+import org.keycloak.saml.processing.core.saml.v2.writers.SAMLRequestWriter;
+import org.keycloak.saml.processing.core.saml.v2.writers.SAMLResponseWriter;
+import org.keycloak.testsuite.util.SamlClient.Step;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import javax.xml.stream.XMLStreamWriter;
+import org.junit.Assert;
+import org.w3c.dom.Document;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public abstract class SamlDocumentStepBuilder<T extends SAML2Object, This extends SamlDocumentStepBuilder<T, This>> implements Step {
+
+ @FunctionalInterface
+ public interface Saml2ObjectTransformer<T extends SAML2Object> {
+ public T transform(T original) throws Exception;
+ }
+
+ @FunctionalInterface
+ public interface Saml2DocumentTransformer {
+ public Document transform(Document original) throws Exception;
+ }
+
+ @FunctionalInterface
+ public interface StringTransformer {
+ public String transform(String original) throws Exception;
+ }
+
+ private final SamlClientBuilder clientBuilder;
+
+ private StringTransformer transformer = t -> t;
+
+ public SamlDocumentStepBuilder(SamlClientBuilder clientBuilder) {
+ this.clientBuilder = clientBuilder;
+ }
+
+ @SuppressWarnings("unchecked")
+ public This transformObject(Saml2ObjectTransformer<T> tr) {
+ final StringTransformer original = this.transformer;
+ this.transformer = s -> {
+ final String originalTransformed = original.transform(s);
+
+ if (originalTransformed == null) {
+ return null;
+ }
+
+ final ByteArrayInputStream baos = new ByteArrayInputStream(originalTransformed.getBytes());
+ final T saml2Object = (T) new SAMLParser().parse(baos);
+ final T transformed = tr.transform(saml2Object);
+
+ if (transformed == null) {
+ return null;
+ }
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ XMLStreamWriter xmlStreamWriter = StaxUtil.getXMLStreamWriter(bos);
+
+ if (saml2Object instanceof AuthnRequestType) {
+ new SAMLRequestWriter(xmlStreamWriter).write((AuthnRequestType) saml2Object);
+ } else if (saml2Object instanceof LogoutRequestType) {
+ new SAMLRequestWriter(xmlStreamWriter).write((LogoutRequestType) saml2Object);
+ } else if (saml2Object instanceof ArtifactResolveType) {
+ new SAMLRequestWriter(xmlStreamWriter).write((ArtifactResolveType) saml2Object);
+ } else if (saml2Object instanceof AttributeQueryType) {
+ new SAMLRequestWriter(xmlStreamWriter).write((AttributeQueryType) saml2Object);
+ } else if (saml2Object instanceof ResponseType) {
+ new SAMLResponseWriter(xmlStreamWriter).write((ResponseType) saml2Object);
+ } else if (saml2Object instanceof ArtifactResponseType) {
+ new SAMLResponseWriter(xmlStreamWriter).write((ArtifactResponseType) saml2Object);
+ } else {
+ Assert.assertNotNull("Unknown type: <null>", saml2Object);
+ Assert.fail("Unknown type: " + saml2Object.getClass().getName());
+ }
+ return new String(bos.toByteArray(), GeneralConstants.SAML_CHARSET);
+ };
+ return (This) this;
+ }
+
+ public This transformDocument(Saml2DocumentTransformer tr) {
+ final StringTransformer original = this.transformer;
+ this.transformer = s -> {
+ final String originalTransformed = original.transform(s);
+
+ if (originalTransformed == null) {
+ return null;
+ }
+
+ final Document transformed = tr.transform(DocumentUtil.getDocument(originalTransformed));
+ return transformed == null ? null : DocumentUtil.getDocumentAsString(transformed);
+ };
+ return (This) this;
+ }
+
+ public This transformString(StringTransformer tr) {
+ final StringTransformer original = this.transformer;
+ this.transformer = s -> {
+ final String originalTransformed = original.transform(s);
+
+ if (originalTransformed == null) {
+ return null;
+ }
+
+ return tr.transform(originalTransformed);
+ };
+ return (This) this;
+ }
+
+ public SamlClientBuilder build() {
+ return this.clientBuilder;
+ }
+
+ public StringTransformer getTransformer() {
+ return transformer;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java
new file mode 100644
index 0000000..89d3092
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.util;
+
+import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
+import org.keycloak.testsuite.util.SamlClient.Binding;
+import org.keycloak.testsuite.util.SamlClient.DoNotFollowRedirectStep;
+import org.keycloak.testsuite.util.SamlClient.ResultExtractor;
+import org.keycloak.testsuite.util.SamlClient.Step;
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.Consumer;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.keycloak.testsuite.util.saml.CreateAuthnRequestStepBuilder;
+import org.keycloak.testsuite.util.saml.CreateLogoutRequestStepBuilder;
+import org.keycloak.testsuite.util.saml.IdPInitiatedLoginBuilder;
+import org.keycloak.testsuite.util.saml.LoginBuilder;
+import org.keycloak.testsuite.util.saml.ModifySamlResponseStepBuilder;
+import org.keycloak.testsuite.util.saml.RequiredConsentBuilder;
+import org.w3c.dom.Document;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class SamlClientBuilder {
+
+ private final List<Step> steps = new LinkedList<>();
+
+ public SamlClient execute(Consumer<CloseableHttpResponse> resultConsumer) {
+ final SamlClient samlClient = new SamlClient();
+ samlClient.executeAndTransform(r -> {
+ resultConsumer.accept(r);
+ return null;
+ }, steps);
+ return samlClient;
+ }
+
+ public <T> T executeAndTransform(ResultExtractor<T> resultTransformer) {
+ return new SamlClient().executeAndTransform(resultTransformer, steps);
+ }
+
+ public List<Step> getSteps() {
+ return steps;
+ }
+
+ public <T extends Step> T addStep(T step) {
+ steps.add(step);
+ return step;
+ }
+
+ public SamlClientBuilder doNotFollowRedirects() {
+ this.steps.add(new DoNotFollowRedirectStep());
+ return this;
+ }
+
+ public SamlClientBuilder clearCookies() {
+ this.steps.add((client, currentURI, currentResponse, context) -> {
+ context.getCookieStore().clear();
+ return null;
+ });
+ return this;
+ }
+
+ /** Creates fresh and issues an AuthnRequest to the SAML endpoint */
+ public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, String issuer, String assertionConsumerURL, Binding requestBinding) {
+ return addStep(new CreateAuthnRequestStepBuilder(authServerSamlUrl, issuer, assertionConsumerURL, requestBinding, this));
+ }
+
+ /** Issues the given AuthnRequest to the SAML endpoint */
+ public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, Document authnRequestDocument, Binding requestBinding) {
+ return addStep(new CreateAuthnRequestStepBuilder(authServerSamlUrl, authnRequestDocument, requestBinding, this));
+ }
+
+ /** Issues the given AuthnRequest to the SAML endpoint */
+ public CreateLogoutRequestStepBuilder logoutRequest(URI authServerSamlUrl, String issuer, Binding requestBinding) {
+ return addStep(new CreateLogoutRequestStepBuilder(authServerSamlUrl, issuer, requestBinding, this));
+ }
+
+ /** Handles login page */
+ public LoginBuilder login() {
+ return addStep(new LoginBuilder(this));
+ }
+
+ /** Starts IdP-initiated flow for the given client */
+ public IdPInitiatedLoginBuilder idpInitiatedLogin(URI authServerSamlUrl, String clientId) {
+ return addStep(new IdPInitiatedLoginBuilder(authServerSamlUrl, clientId, this));
+ }
+
+ /** Handles "Requires consent" page */
+ public RequiredConsentBuilder consentRequired() {
+ return addStep(new RequiredConsentBuilder(this));
+ }
+
+ /** Returns SAML request or response as replied from server. Note that the redirects are disabled for this to work. */
+ public SAMLDocumentHolder getSamlResponse(Binding responseBinding) {
+ return
+ doNotFollowRedirects()
+ .executeAndTransform(responseBinding::extractResponse);
+ }
+
+ /** Returns SAML request or response as replied from server. Note that the redirects are disabled for this to work. */
+ public ModifySamlResponseStepBuilder processSamlResponse(Binding responseBinding) {
+ return
+ doNotFollowRedirects()
+ .addStep(new ModifySamlResponseStepBuilder(responseBinding, this));
+ }
+
+ public SamlClientBuilder navigateTo(String httpGetUri) {
+ steps.add((client, currentURI, currentResponse, context) -> {
+ return new HttpGet(httpGetUri);
+ });
+ return this;
+ }
+
+ public SamlClientBuilder navigateTo(URI httpGetUri) {
+ steps.add((client, currentURI, currentResponse, context) -> {
+ return new HttpGet(httpGetUri);
+ });
+ return this;
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
index 262d0b2..d2f7de6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
@@ -144,6 +144,8 @@ public abstract class AbstractKeycloakTest {
updateMasterAdminPassword();
}
+ beforeAbstractKeycloakTestRealmImport();
+
if (testContext.getTestRealmReps() == null) {
importTestRealms();
@@ -155,6 +157,9 @@ public abstract class AbstractKeycloakTest {
oauth.init(adminClient, driver);
}
+ protected void beforeAbstractKeycloakTestRealmImport() throws Exception {
+ }
+
@After
public void afterAbstractKeycloakTest() {
if (resetTimeOffset) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index eba81f4..c69f489 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -28,6 +28,9 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
+import org.keycloak.models.AccountRoles;
+import org.keycloak.models.AdminRoles;
+import org.keycloak.models.Constants;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.ClientRepresentation;
@@ -78,11 +81,18 @@ public class AccountTest extends AbstractTestRealmKeycloakTest {
//UserRepresentation user = findUserInRealmRep(testRealm, "test-user@localhost");
//ClientRepresentation accountApp = findClientInRealmRep(testRealm, ACCOUNT_MANAGEMENT_CLIENT_ID);
UserRepresentation user2 = UserBuilder.create()
- .enabled(true)
- .username("test-user-no-access@localhost")
- .email("test-user-no-access@localhost")
- .password("password")
- .build();
+ .enabled(true)
+ .username("test-user-no-access@localhost")
+ .email("test-user-no-access@localhost")
+ .password("password")
+ .build();
+ UserRepresentation realmAdmin = UserBuilder.create()
+ .enabled(true)
+ .username("realm-admin")
+ .password("password")
+ .role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.REALM_ADMIN)
+ .role(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID, AccountRoles.MANAGE_ACCOUNT)
+ .build();
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
.providerId("github")
@@ -105,7 +115,8 @@ public class AccountTest extends AbstractTestRealmKeycloakTest {
.build());
RealmBuilder.edit(testRealm)
- .user(user2);
+ .user(user2)
+ .user(realmAdmin);
}
private static final UriBuilder BASE = UriBuilder.fromUri("http://localhost:8180/auth");
@@ -870,6 +881,19 @@ public class AccountTest extends AbstractTestRealmKeycloakTest {
}
}
+ // KEYCLOAK-5155
+ @Test
+ public void testConsoleListedInApplications() {
+ applicationsPage.open();
+ loginPage.login("realm-admin", "password");
+ Assert.assertTrue(applicationsPage.isCurrent());
+ Map<String, AccountApplicationsPage.AppEntry> apps = applicationsPage.getApplications();
+ Assert.assertThat(apps.keySet(), hasItems("Admin CLI", "Security Admin Console"));
+ events.clear();
+ }
+
+
+
// More tests (including revoke) are in OAuthGrantTest and OfflineTokenTest
@Test
public void applications() {
@@ -880,7 +904,7 @@ public class AccountTest extends AbstractTestRealmKeycloakTest {
Assert.assertTrue(applicationsPage.isCurrent());
Map<String, AccountApplicationsPage.AppEntry> apps = applicationsPage.getApplications();
- Assert.assertThat(apps.keySet(), containsInAnyOrder("Account", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}"));
+ Assert.assertThat(apps.keySet(), containsInAnyOrder("root-url-client", "Account", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}"));
AccountApplicationsPage.AppEntry accountEntry = apps.get("Account");
Assert.assertEquals(3, accountEntry.getRolesAvailable().size());
@@ -891,12 +915,14 @@ public class AccountTest extends AbstractTestRealmKeycloakTest {
Assert.assertTrue(accountEntry.getRolesGranted().contains("Full Access"));
Assert.assertEquals(1, accountEntry.getProtocolMappersGranted().size());
Assert.assertTrue(accountEntry.getProtocolMappersGranted().contains("Full Access"));
+ Assert.assertEquals("http://localhost:8180/auth/realms/test/account", accountEntry.getHref());
AccountApplicationsPage.AppEntry testAppEntry = apps.get("test-app");
Assert.assertEquals(5, testAppEntry.getRolesAvailable().size());
Assert.assertTrue(testAppEntry.getRolesAvailable().contains("Offline access"));
Assert.assertTrue(testAppEntry.getRolesGranted().contains("Full Access"));
Assert.assertTrue(testAppEntry.getProtocolMappersGranted().contains("Full Access"));
+ Assert.assertEquals("http://localhost:8180/auth/realms/master/app/auth", testAppEntry.getHref());
AccountApplicationsPage.AppEntry thirdPartyEntry = apps.get("third-party");
Assert.assertEquals(2, thirdPartyEntry.getRolesAvailable().size());
@@ -904,6 +930,22 @@ public class AccountTest extends AbstractTestRealmKeycloakTest {
Assert.assertTrue(thirdPartyEntry.getRolesAvailable().contains("Have Customer User privileges in test-app"));
Assert.assertEquals(0, thirdPartyEntry.getRolesGranted().size());
Assert.assertEquals(0, thirdPartyEntry.getProtocolMappersGranted().size());
+ Assert.assertEquals("http://localhost:8180/auth/realms/master/app/auth", thirdPartyEntry.getHref());
+
+ AccountApplicationsPage.AppEntry testAppNamed = apps.get("Test App Named - ${client_account}");
+ Assert.assertEquals("http://localhost:8180/varnamedapp/base", testAppNamed.getHref());
+
+ AccountApplicationsPage.AppEntry rootUrlClient = apps.get("root-url-client");
+ Assert.assertEquals("http://localhost:8180/foo/bar/baz", rootUrlClient.getHref());
+
+ AccountApplicationsPage.AppEntry authzApp = apps.get("test-app-authz");
+ Assert.assertEquals("http://localhost:8180/test-app-authz", authzApp.getHref());
+
+ AccountApplicationsPage.AppEntry namedApp = apps.get("My Named Test App");
+ Assert.assertEquals("http://localhost:8180/namedapp/base", namedApp.getHref());
+
+ AccountApplicationsPage.AppEntry testAppScope = apps.get("test-app-scope");
+ Assert.assertNull(testAppScope.getHref());
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
index 9fd5c7a..f4c7452 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
@@ -37,6 +37,7 @@ import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
+import org.keycloak.testsuite.pages.ProceedPage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.pages.LoginPage;
@@ -82,6 +83,9 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
protected InfoPage infoPage;
@Page
+ protected ProceedPage proceedPage;
+
+ @Page
protected ErrorPage errorPage;
private String testUserId;
@@ -330,6 +334,8 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
driver.navigate().to(verificationUrl2.trim());
+ proceedPage.assertCurrent();
+ proceedPage.clickProceedLink();
infoPage.assertCurrent();
assertEquals("Your email address has been verified.", infoPage.getInfo());
}
@@ -355,6 +361,9 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
driver.manage().deleteAllCookies();
driver.navigate().to(verificationUrl.trim());
+ proceedPage.assertCurrent();
+ proceedPage.clickProceedLink();
+ infoPage.assertCurrent();
events.expectRequiredAction(EventType.VERIFY_EMAIL)
.user(testUserId)
@@ -424,7 +433,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
driver.navigate().to(verificationUrl.trim());
loginPage.assertCurrent();
- assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError());
+ assertEquals("Action expired. Please start again.", loginPage.getError());
events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR)
.error(Errors.EXPIRED_CODE)
@@ -462,7 +471,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
driver.navigate().to(verificationUrl.trim());
errorPage.assertCurrent();
- assertEquals("The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email?", errorPage.getError());
+ assertEquals("Action expired.", errorPage.getError());
events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR)
.error(Errors.EXPIRED_CODE)
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseAdminAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseAdminAdapterTest.java
index 3cc9b38..ef5d835 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseAdminAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseAdminAdapterTest.java
@@ -205,7 +205,7 @@ public abstract class AbstractFuseAdminAdapterTest extends AbstractExampleAdapte
pipe.write("logout\n".getBytes());
pipe.flush();
- channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0);
+ channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), TimeUnit.SECONDS.toMillis(15L));
session.close(true);
client.stop();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/cors/AbstractCorsExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/cors/AbstractCorsExampleAdapterTest.java
index 4398164..83a34c7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/cors/AbstractCorsExampleAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/cors/AbstractCorsExampleAdapterTest.java
@@ -94,6 +94,7 @@ public abstract class AbstractCorsExampleAdapterTest extends AbstractExampleAdap
waitUntilElement(angularCorsProductPage.getOutput()).text().contains("iphone");
waitUntilElement(angularCorsProductPage.getOutput()).text().contains("ipad");
waitUntilElement(angularCorsProductPage.getOutput()).text().contains("ipod");
+ waitUntilElement(angularCorsProductPage.getHeaders()).text().contains("\"x-custom1\":\"some-value\"");
angularCorsProductPage.loadRoles();
waitUntilElement(angularCorsProductPage.getOutput()).text().contains("user");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java
index ea9937e..f95fe7f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java
@@ -168,6 +168,10 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer
user.setUsername("child");
user.setEnabled(true);
childUserId = createUserAndResetPasswordWithAdminClient(realm, user, "password");
+ UserRepresentation user2 = new UserRepresentation();
+ user2.setUsername("child2");
+ user2.setEnabled(true);
+ String user2Id = createUserAndResetPasswordWithAdminClient(realm, user2, "password");
// have to add a role as undertow default auth manager doesn't like "*". todo we can remove this eventually as undertow fixes this in later versions
realm.roles().create(new RoleRepresentation("user", null, false));
@@ -175,11 +179,13 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer
List<RoleRepresentation> roles = new LinkedList<>();
roles.add(role);
realm.users().get(childUserId).roles().realmLevel().add(roles);
+ realm.users().get(user2Id).roles().realmLevel().add(roles);
ClientRepresentation brokerService = realm.clients().findByClientId(Constants.BROKER_SERVICE_CLIENT_ID).get(0);
role = realm.clients().get(brokerService.getId()).roles().get(Constants.READ_TOKEN_ROLE).toRepresentation();
roles.clear();
roles.add(role);
realm.users().get(childUserId).roles().clientLevel(brokerService.getId()).add(roles);
+ realm.users().get(user2Id).roles().clientLevel(brokerService.getId()).add(roles);
}
@@ -192,11 +198,6 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer
BrokerTestTools.createKcOidcBroker(adminClient, CHILD_IDP, PARENT_IDP, suiteContext);
}
-// @Test
- public void testUi() throws Exception {
- Thread.sleep(1000000000);
-
- }
@Test
public void testErrorConditions() throws Exception {
@@ -388,6 +389,7 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer
String linkUrl = linkBuilder.clone()
.queryParam("realm", CHILD_IDP)
.queryParam("provider", PARENT_IDP).build().toString();
+ System.out.println("linkUrl: " + linkUrl);
navigateTo(linkUrl);
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
Assert.assertTrue(driver.getPageSource().contains(PARENT_IDP));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
index 420fad8..b62ba31 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
@@ -17,6 +17,7 @@
package org.keycloak.testsuite.adapter.servlet;
+import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
@@ -53,7 +54,6 @@ import org.keycloak.saml.BaseSAML2BindingBuilder;
import org.keycloak.saml.SAML2ErrorResponseBuilder;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
-import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
@@ -65,6 +65,7 @@ import org.keycloak.testsuite.page.AbstractPage;
import org.keycloak.testsuite.util.*;
import org.keycloak.testsuite.util.SamlClient.Binding;
+import org.keycloak.testsuite.util.SamlClientBuilder;
import org.openqa.selenium.By;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -95,6 +96,12 @@ import java.security.PublicKey;
import java.util.*;
import java.util.stream.Collectors;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathFactory;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD;
@@ -106,8 +113,6 @@ import static org.keycloak.testsuite.util.IOUtil.loadXML;
import static org.keycloak.testsuite.util.IOUtil.modifyDocElementAttribute;
import static org.keycloak.testsuite.util.Matchers.bodyHC;
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
-import static org.keycloak.testsuite.util.SamlClient.idpInitiatedLogin;
-import static org.keycloak.testsuite.util.SamlClient.login;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
import static org.keycloak.testsuite.util.WaitUtils.*;
@@ -470,10 +475,9 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
@Test
public void employeeAcsTest() {
- SAMLDocumentHolder samlResponse = new SamlClient(employeeAcsServletPage.buildUri()).getSamlResponse(Binding.POST, (client, context, strategy) -> {
- strategy.setRedirectable(false);
- return client.execute(new HttpGet(employeeAcsServletPage.buildUri()), context);
- });
+ SAMLDocumentHolder samlResponse = new SamlClientBuilder()
+ .navigateTo(employeeAcsServletPage.buildUri())
+ .getSamlResponse(Binding.POST);
assertThat(samlResponse.getSamlObject(), instanceOf(AuthnRequestType.class));
assertThat(((AuthnRequestType) samlResponse.getSamlObject()).getAssertionConsumerServiceURL(), notNullValue());
@@ -658,6 +662,50 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
}
@Test
+ public void salesPostEncRejectConsent() throws Exception {
+ ClientRepresentation salesPostEncClient = testRealmResource().clients().findByClientId(SalesPostEncServlet.CLIENT_NAME).get(0);
+ try (Closeable client = new ClientAttributeUpdater(testRealmResource().clients().get(salesPostEncClient.getId()))
+ .setConsentRequired(true)
+ .update()) {
+ new SamlClientBuilder()
+ .navigateTo(salesPostEncServletPage.toString())
+ .processSamlResponse(Binding.POST).build()
+ .login().user(bburkeUser).build()
+ .consentRequired().approveConsent(false).build()
+ .processSamlResponse(Binding.POST).build()
+
+ .execute(r -> {
+ assertThat(r, statusCodeIsHC(Response.Status.OK));
+ assertThat(r, bodyHC(containsString("urn:oasis:names:tc:SAML:2.0:status:RequestDenied"))); // TODO: revisit - should the HTTP status be 403 too?
+ });
+ } finally {
+ salesPostEncServletPage.logout();
+ }
+ }
+
+ @Test
+ public void salesPostRejectConsent() throws Exception {
+ ClientRepresentation salesPostClient = testRealmResource().clients().findByClientId(SalesPostServlet.CLIENT_NAME).get(0);
+ try (Closeable client = new ClientAttributeUpdater(testRealmResource().clients().get(salesPostClient.getId()))
+ .setConsentRequired(true)
+ .update()) {
+ new SamlClientBuilder()
+ .navigateTo(salesPostServletPage.toString())
+ .processSamlResponse(Binding.POST).build()
+ .login().user(bburkeUser).build()
+ .consentRequired().approveConsent(false).build()
+ .processSamlResponse(Binding.POST).build()
+
+ .execute(r -> {
+ assertThat(r, statusCodeIsHC(Response.Status.OK));
+ assertThat(r, bodyHC(containsString("urn:oasis:names:tc:SAML:2.0:status:RequestDenied"))); // TODO: revisit - should the HTTP status be 403 too?
+ });
+ } finally {
+ salesPostServletPage.logout();
+ }
+ }
+
+ @Test
public void salesPostPassiveTest() {
salesPostPassiveServletPage.navigateTo();
@@ -1028,58 +1076,74 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
@Test
//KEYCLOAK-4020
public void testBooleanAttribute() throws Exception {
- AuthnRequestType req = SamlClient.createLoginRequestDocument("http://localhost:8081/employee2/", getAppServerSamlEndpoint(employee2ServletPage).toString(), getAuthServerSamlEndpoint(SAMLSERVLETDEMO));
- Document doc = SAML2Request.convert(req);
+ new SamlClientBuilder()
+ .authnRequest(getAuthServerSamlEndpoint(SAMLSERVLETDEMO), "http://localhost:8081/employee2/", getAppServerSamlEndpoint(employee2ServletPage).toString(), Binding.POST).build()
+ .login().user(bburkeUser).build()
+ .processSamlResponse(Binding.POST)
+ .transformDocument(responseDoc -> {
+ Element attribute = responseDoc.createElement("saml:Attribute");
+ attribute.setAttribute("Name", "boolean-attribute");
+ attribute.setAttribute("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic");
- SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(SAMLSERVLETDEMO), doc, null, SamlClient.Binding.POST, SamlClient.Binding.POST);
- Document responseDoc = res.getSamlDocument();
+ Element attributeValue = responseDoc.createElement("saml:AttributeValue");
+ attributeValue.setAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema");
+ attributeValue.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
+ attributeValue.setAttribute("xsi:type", "xs:boolean");
+ attributeValue.setTextContent("true");
- Element attribute = responseDoc.createElement("saml:Attribute");
- attribute.setAttribute("Name", "boolean-attribute");
- attribute.setAttribute("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic");
+ attribute.appendChild(attributeValue);
+ IOUtil.appendChildInDocument(responseDoc, "samlp:Response/saml:Assertion/saml:AttributeStatement", attribute);
- Element attributeValue = responseDoc.createElement("saml:AttributeValue");
- attributeValue.setAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema");
- attributeValue.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
- attributeValue.setAttribute("xsi:type", "xs:boolean");
- attributeValue.setTextContent("true");
+ return responseDoc;
+ })
+ .build()
- attribute.appendChild(attributeValue);
- IOUtil.appendChildInDocument(responseDoc, "samlp:Response/saml:Assertion/saml:AttributeStatement", attribute);
+ .navigateTo(employee2ServletPage.toString() + "/getAttributes")
- CloseableHttpResponse response = null;
- try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
- HttpClientContext context = HttpClientContext.create();
+ .execute(r -> {
+ assertThat(r, statusCodeIsHC(Response.Status.OK));
+ assertThat(r, bodyHC(containsString("boolean-attribute: true")));
+ });
+ }
- HttpUriRequest post = SamlClient.Binding.POST.createSamlUnsignedResponse(getAppServerSamlEndpoint(employee2ServletPage), null, responseDoc);
- response = client.execute(post, context);
- assertThat(response, statusCodeIsHC(Response.Status.FOUND));
- response.close();
+ @Test
+ public void testNameIDUnset() throws Exception {
+ new SamlClientBuilder()
+ .navigateTo(employee2ServletPage.toString())
+ .processSamlResponse(Binding.POST).build()
+ .login().user(bburkeUser).build()
+ .processSamlResponse(Binding.POST)
+ .transformDocument(responseDoc -> {
+ XPathFactory xPathfactory = XPathFactory.newInstance();
+ XPath xpath = xPathfactory.newXPath();
+ XPathExpression expr = xpath.compile("//*[local-name()='NameID']");
- HttpGet get = new HttpGet(employee2ServletPage.toString() + "/getAttributes");
- response = client.execute(get);
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- assertThat(response, bodyHC(containsString("boolean-attribute: true")));
- } catch (Exception ex) {
- throw new RuntimeException(ex);
- } finally {
- if (response != null) {
- EntityUtils.consumeQuietly(response.getEntity());
- try { response.close(); } catch (IOException ex) { }
- }
- }
+ NodeList nodeList = (NodeList) expr.evaluate(responseDoc, XPathConstants.NODESET);
+ assertThat(nodeList.getLength(), is(1));
+
+ final Node nameIdNode = nodeList.item(0);
+ nameIdNode.getParentNode().removeChild(nameIdNode);
+
+ return responseDoc;
+ })
+ .build()
+
+ .navigateTo(employee2ServletPage.toString())
+
+ .execute(r -> {
+ assertThat(r, statusCodeIsHC(Response.Status.OK));
+ assertThat(r, bodyHC(allOf(containsString("principal="), not(containsString("500")))));
+ });
}
// KEYCLOAK-4329
@Test
public void testEmptyKeyInfoElement() {
- samlidpInitiatedLoginPage.setAuthRealm(SAMLSERVLETDEMO);
- samlidpInitiatedLoginPage.setUrlName("sales-post-sig-email");
- System.out.println(samlidpInitiatedLoginPage.toString());
- URI idpInitiatedLoginPage = URI.create(samlidpInitiatedLoginPage.toString());
-
log.debug("Log in using idp initiated login");
- SAMLDocumentHolder documentHolder = idpInitiatedLogin(bburkeUser, idpInitiatedLoginPage, SamlClient.Binding.POST);
+ SAMLDocumentHolder documentHolder = new SamlClientBuilder()
+ .idpInitiatedLogin(getAuthServerSamlEndpoint(SAMLSERVLETDEMO), "sales-post-sig-email").build()
+ .login().user(bburkeUser).build()
+ .getSamlResponse(Binding.POST);
log.debug("Removing KeyInfo from Keycloak response");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowClientInitiatedAccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowClientInitiatedAccountLinkTest.java
index a1eef97..336d6b7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowClientInitiatedAccountLinkTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowClientInitiatedAccountLinkTest.java
@@ -16,6 +16,7 @@
*/
package org.keycloak.testsuite.adapter.undertow.servlet;
+import org.junit.Test;
import org.keycloak.testsuite.adapter.servlet.AbstractClientInitiatedAccountLinkTest;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
@@ -26,4 +27,15 @@ import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
@AppServerContainer("auth-server-undertow")
public class UndertowClientInitiatedAccountLinkTest extends AbstractClientInitiatedAccountLinkTest {
+ //@Test
+ public void testUi() throws Exception {
+ Thread.sleep(1000000000);
+
+ }
+
+ @Override
+ @Test
+ public void testAccountLink() throws Exception {
+ super.testAccountLink();
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
index 0481518..57fe6de 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
@@ -19,6 +19,8 @@ package org.keycloak.testsuite.admin.authentication;
import org.junit.Assert;
import org.junit.Test;
+import org.keycloak.models.utils.DefaultAuthenticationFlows;
+import org.keycloak.protocol.docker.DockerAuthenticator;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
@@ -155,6 +157,13 @@ public class InitialFlowsTest extends AbstractAuthenticationTest {
addExecInfo(execs, "OTP", "direct-grant-validate-otp", false, 0, 2, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
expected.add(new FlowExecutions(flow, execs));
+ flow = newFlow("docker auth", "Used by Docker clients to authenticate against the IDP", "basic-flow", true, true);
+ addExecExport(flow, null, false, "docker-http-basic-authenticator", false, null, REQUIRED, 10);
+
+ execs = new LinkedList<>();
+ addExecInfo(execs, "Docker Authenticator", "docker-http-basic-authenticator", false, 0, 0, REQUIRED, null, new String[]{REQUIRED});
+ expected.add(new FlowExecutions(flow, execs));
+
flow = newFlow("first broker login", "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"basic-flow", true, true);
addExecExport(flow, null, false, "idp-review-profile", false, "review profile config", REQUIRED, 10);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
index e13794d..f55e90f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
@@ -151,6 +151,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Validates the password supplied as a 'password' form parameter in direct grant request");
addProviderInfo(result, "direct-grant-validate-username", "Username Validation",
"Validates the username supplied as a 'username' form parameter in direct grant request");
+ addProviderInfo(result, "docker-http-basic-authenticator", "Docker Authenticator", "Uses HTTP Basic authentication to validate docker users, returning a docker error token on auth failure");
addProviderInfo(result, "expected-param-authenticator", "TEST: Expected Parameter",
"You will be approved if you send query string parameter 'foo' with expected value.");
addProviderInfo(result, "http-basic-authenticator", "HTTP Basic Authentication", "Validates username and password from Authorization HTTP header");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java
index 2c3aac7..51a2cae 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java
@@ -20,10 +20,12 @@ package org.keycloak.testsuite.admin.client.authorization;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
+import org.keycloak.admin.client.resource.AuthorizationResource;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ResourceScopeResource;
import org.keycloak.admin.client.resource.ResourceScopesResource;
import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.admin.client.AbstractClientTest;
@@ -38,7 +40,7 @@ import static org.junit.Assert.assertFalse;
*/
public abstract class AbstractAuthorizationTest extends AbstractClientTest {
- protected static final String RESOURCE_SERVER_CLIENT_ID = "test-resource-server";
+ protected static final String RESOURCE_SERVER_CLIENT_ID = "resource-server-test";
@BeforeClass
public static void enabled() {
@@ -73,8 +75,17 @@ public abstract class AbstractAuthorizationTest extends AbstractClientTest {
resourceServer.setAuthorizationServicesEnabled(true);
resourceServer.setServiceAccountsEnabled(true);
+ resourceServer.setPublicClient(false);
+ resourceServer.setSecret("secret");
getClientResource().update(resourceServer);
+
+ AuthorizationResource authorization = getClientResource().authorization();
+ ResourceServerRepresentation settings = authorization.exportSettings();
+
+ settings.setAllowRemoteResourceManagement(true);
+
+ authorization.update(settings);
}
protected ResourceScopeResource createDefaultScope() {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java
index 57d86a7..4decccf 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java
@@ -58,16 +58,11 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
return super.createTestRealm().group(GroupBuilder.create().name("Group A")
.subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> {
if ("Group B".equals(name)) {
- return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function<String, GroupRepresentation>() {
- @Override
- public GroupRepresentation apply(String name) {
- return GroupBuilder.create().name(name).build();
- }
- }).collect(Collectors.toList())).build();
+ return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(name1 -> GroupBuilder.create().name(name1).build()).collect(Collectors.toList())).build();
}
return GroupBuilder.create().name(name).build();
}).collect(Collectors.toList()))
- .build()).group(GroupBuilder.create().name("Group E").build());
+ .build()).group(GroupBuilder.create().name("Group F").build());
}
@Test
@@ -81,7 +76,7 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
representation.setLogic(Logic.NEGATIVE);
representation.setGroupsClaim("groups");
representation.addGroupPath("/Group A/Group B/Group C", true);
- representation.addGroupPath("Group E");
+ representation.addGroupPath("Group F");
assertCreated(authorization, representation);
}
@@ -97,7 +92,7 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
representation.setLogic(Logic.NEGATIVE);
representation.setGroupsClaim("groups");
representation.addGroupPath("/Group A/Group B/Group C", true);
- representation.addGroupPath("Group E");
+ representation.addGroupPath("Group F");
assertCreated(authorization, representation);
@@ -114,7 +109,7 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
assertRepresentation(representation, permission);
for (GroupPolicyRepresentation.GroupDefinition roleDefinition : representation.getGroups()) {
- if (roleDefinition.getPath().equals("Group E")) {
+ if (roleDefinition.getPath().equals("Group F")) {
roleDefinition.setExtendChildren(true);
}
}
@@ -137,7 +132,7 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
representation.setName("Delete Group Policy");
representation.setGroupsClaim("groups");
representation.addGroupPath("/Group A/Group B/Group C", true);
- representation.addGroupPath("Group E");
+ representation.addGroupPath("Group F");
GroupPoliciesResource policies = authorization.policies().group();
Response response = policies.create(representation);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java
index 9907472..3c1a2f1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java
@@ -22,17 +22,22 @@ import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.ResourceResource;
import org.keycloak.admin.client.resource.ResourcesResource;
+import org.keycloak.authorization.client.util.HttpResponseException;
+import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
/**
*
@@ -47,9 +52,29 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
enableAuthorizationServices();
}
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ RealmRepresentation testRealmRep = new RealmRepresentation();
+ testRealmRep.setId("authz-test");
+ testRealmRep.setRealm("authz-test");
+ testRealmRep.setEnabled(true);
+ testRealms.add(testRealmRep);
+ }
+
+ @Override
+ public void setDefaultPageUriParameters() {
+ super.setDefaultPageUriParameters();
+ testRealmPage.setAuthRealm("authz-test");
+ }
+
+ @Override
+ protected String getRealmId() {
+ return "authz-test";
+ }
+
@Test
public void testCreate() {
- ResourceRepresentation newResource = createResource().toRepresentation();
+ ResourceRepresentation newResource = createResource();
assertEquals("Test Resource", newResource.getName());
assertEquals("/test/*", newResource.getUri());
@@ -58,17 +83,34 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
}
@Test
+ public void failCreateWithSameName() {
+ ResourceRepresentation newResource = createResource();
+
+ try {
+ doCreateResource(newResource);
+ fail("Can not create resources with the same name and owner");
+ } catch (Exception e) {
+ assertEquals(HttpResponseException.class, e.getCause().getClass());
+ assertEquals(409, HttpResponseException.class.cast(e.getCause()).getStatusCode());
+ }
+
+ newResource.setName(newResource.getName() + " Another");
+
+ newResource = doCreateResource(newResource);
+
+ assertNotNull(newResource.getId());
+ assertEquals("Test Resource Another", newResource.getName());
+ }
+
+ @Test
public void testUpdate() {
- ResourceResource resourceResource = createResource();
- ResourceRepresentation resource = resourceResource.toRepresentation();
+ ResourceRepresentation resource = createResource();
resource.setType("changed");
resource.setIconUri("changed");
resource.setUri("changed");
- resourceResource.update(resource);
-
- resource = resourceResource.toRepresentation();
+ resource = doUpdateResource(resource);
assertEquals("changed", resource.getIconUri());
assertEquals("changed", resource.getType());
@@ -77,17 +119,16 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
@Test(expected = NotFoundException.class)
public void testDelete() {
- ResourceResource resourceResource = createResource();
+ ResourceRepresentation resource = createResource();
- resourceResource.remove();
+ doRemoveResource(resource);
- resourceResource.toRepresentation();
+ getClientResource().authorization().resources().resource(resource.getId()).toRepresentation();
}
@Test
public void testAssociateScopes() {
- ResourceResource resourceResource = createResourceWithDefaultScopes();
- ResourceRepresentation updated = resourceResource.toRepresentation();
+ ResourceRepresentation updated = createResourceWithDefaultScopes();
assertEquals(3, updated.getScopes().size());
@@ -98,8 +139,7 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
@Test
public void testUpdateScopes() {
- ResourceResource resourceResource = createResourceWithDefaultScopes();
- ResourceRepresentation resource = resourceResource.toRepresentation();
+ ResourceRepresentation resource = createResourceWithDefaultScopes();
Set<ScopeRepresentation> scopes = new HashSet<>(resource.getScopes());
assertEquals(3, scopes.size());
@@ -107,9 +147,7 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
resource.setScopes(scopes);
- resourceResource.update(resource);
-
- ResourceRepresentation updated = resourceResource.toRepresentation();
+ ResourceRepresentation updated = doUpdateResource(resource);
assertEquals(2, resource.getScopes().size());
@@ -124,16 +162,13 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
updated.setScopes(scopes);
- resourceResource.update(updated);
-
- updated = resourceResource.toRepresentation();
+ updated = doUpdateResource(updated);
assertEquals(0, updated.getScopes().size());
}
- private ResourceResource createResourceWithDefaultScopes() {
- ResourceResource resourceResource = createResource();
- ResourceRepresentation resource = resourceResource.toRepresentation();
+ private ResourceRepresentation createResourceWithDefaultScopes() {
+ ResourceRepresentation resource = createResource();
assertEquals(0, resource.getScopes().size());
@@ -145,9 +180,7 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
resource.setScopes(scopes);
- resourceResource.update(resource);
-
- return resourceResource;
+ return doUpdateResource(resource);
}
private boolean containsScope(String scopeName, ResourceRepresentation resource) {
@@ -164,7 +197,7 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
return false;
}
- private ResourceResource createResource() {
+ private ResourceRepresentation createResource() {
ResourceRepresentation newResource = new ResourceRepresentation();
newResource.setName("Test Resource");
@@ -172,14 +205,36 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
newResource.setType("test-resource");
newResource.setIconUri("icon-test-resource");
+ return doCreateResource(newResource);
+ }
+
+ protected ResourceRepresentation doCreateResource(ResourceRepresentation newResource) {
ResourcesResource resources = getClientResource().authorization().resources();
Response response = resources.create(newResource);
- assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
+ int status = response.getStatus();
+
+ if (status != Response.Status.CREATED.getStatusCode()) {
+ throw new RuntimeException(new HttpResponseException("Error", status, "", null));
+ }
ResourceRepresentation stored = response.readEntity(ResourceRepresentation.class);
- return resources.resource(stored.getId());
+ return resources.resource(stored.getId()).toRepresentation();
+ }
+
+ protected ResourceRepresentation doUpdateResource(ResourceRepresentation resource) {
+ ResourcesResource resources = getClientResource().authorization().resources();
+ ResourceResource existing = resources.resource(resource.getId());
+
+ existing.update(resource);
+
+ return resources.resource(resource.getId()).toRepresentation();
+ }
+
+ protected void doRemoveResource(ResourceRepresentation resource) {
+ ResourcesResource resources = getClientResource().authorization().resources();
+ resources.resource(resource.getId()).remove();
}
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java
new file mode 100644
index 0000000..5f07b2f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.admin.client.authorization;
+
+import java.io.IOException;
+import java.util.stream.Collectors;
+
+import org.jetbrains.annotations.NotNull;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.RegistrationResponse;
+import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourceManagementWithAuthzClientTest extends ResourceManagementTest {
+
+ private AuthzClient authzClient;
+
+ @Override
+ protected ResourceRepresentation doCreateResource(ResourceRepresentation newResource) {
+ org.keycloak.authorization.client.representation.ResourceRepresentation resource = toResourceRepresentation(newResource);
+
+ AuthzClient authzClient = getAuthzClient();
+ RegistrationResponse response = authzClient.protection().resource().create(resource);
+
+ return toResourceRepresentation(authzClient, response.getId());
+ }
+
+ @Override
+ protected ResourceRepresentation doUpdateResource(ResourceRepresentation resource) {
+ AuthzClient authzClient = getAuthzClient();
+
+ authzClient.protection().resource().update(toResourceRepresentation(resource));
+
+ return toResourceRepresentation(authzClient, resource.getId());
+ }
+
+ @Override
+ protected void doRemoveResource(ResourceRepresentation resource) {
+ getAuthzClient().protection().resource().delete(resource.getId());
+ }
+
+ private ResourceRepresentation toResourceRepresentation(AuthzClient authzClient, String id) {
+ org.keycloak.authorization.client.representation.ResourceRepresentation created = authzClient.protection().resource().findById(id).getResourceDescription();
+ ResourceRepresentation resourceRepresentation = new ResourceRepresentation();
+
+ resourceRepresentation.setId(created.getId());
+ resourceRepresentation.setName(created.getName());
+ resourceRepresentation.setIconUri(created.getIconUri());
+ resourceRepresentation.setUri(created.getUri());
+ resourceRepresentation.setType(created.getType());
+ ResourceOwnerRepresentation owner = new ResourceOwnerRepresentation();
+
+ owner.setId(created.getOwner());
+
+ resourceRepresentation.setOwner(owner);
+ resourceRepresentation.setScopes(created.getScopes().stream().map(scopeRepresentation -> {
+ ScopeRepresentation scope = new ScopeRepresentation();
+
+ scope.setId(scopeRepresentation.getId());
+ scope.setName(scopeRepresentation.getName());
+ scope.setIconUri(scopeRepresentation.getIconUri());
+
+ return scope;
+ }).collect(Collectors.toSet()));
+
+ return resourceRepresentation;
+ }
+
+ private org.keycloak.authorization.client.representation.ResourceRepresentation toResourceRepresentation(ResourceRepresentation newResource) {
+ org.keycloak.authorization.client.representation.ResourceRepresentation resource = new org.keycloak.authorization.client.representation.ResourceRepresentation();
+
+ resource.setId(newResource.getId());
+ resource.setName(newResource.getName());
+ resource.setIconUri(newResource.getIconUri());
+ resource.setUri(newResource.getUri());
+ resource.setType(newResource.getType());
+
+ if (newResource.getOwner() != null) {
+ resource.setOwner(newResource.getOwner().getId());
+ }
+
+ resource.setScopes(newResource.getScopes().stream().map(scopeRepresentation -> {
+ org.keycloak.authorization.client.representation.ScopeRepresentation scope = new org.keycloak.authorization.client.representation.ScopeRepresentation();
+
+ scope.setName(scopeRepresentation.getName());
+ scope.setIconUri(scopeRepresentation.getIconUri());
+
+ return scope;
+ }).collect(Collectors.toSet()));
+
+ return resource;
+ }
+
+ private AuthzClient getAuthzClient() {
+ if (authzClient == null) {
+ try {
+ authzClient = AuthzClient.create(JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"), Configuration.class));
+ } catch (IOException cause) {
+ throw new RuntimeException("Failed to create authz client", cause);
+ }
+ }
+
+ return authzClient;
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceServerManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceServerManagementTest.java
new file mode 100644
index 0000000..73e1961
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceServerManagementTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.admin.client.authorization;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourceServerManagementTest extends AbstractAuthorizationTest {
+
+ @Test
+ public void testCreateAndDeleteResourceServer() throws Exception {
+ ClientsResource clientsResource = testRealmResource().clients();
+
+ clientsResource.create(JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/client-with-authz-settings.json"), ClientRepresentation.class)).close();
+
+ List<ClientRepresentation> clients = clientsResource.findByClientId("authz-client");
+
+ assertFalse(clients.isEmpty());
+
+ String clientId = clients.get(0).getId();
+ AuthorizationResource settings = clientsResource.get(clientId).authorization();
+
+ assertEquals(PolicyEnforcementMode.PERMISSIVE, settings.exportSettings().getPolicyEnforcementMode());
+
+ assertFalse(settings.resources().findByName("Resource 1").isEmpty());
+ assertFalse(settings.resources().findByName("Resource 15").isEmpty());
+ assertFalse(settings.resources().findByName("Resource 20").isEmpty());
+
+ assertNotNull(settings.permissions().resource().findByName("Resource 15 Permission"));
+ assertNotNull(settings.policies().role().findByName("Resource 1 Policy"));
+
+ clientsResource.get(clientId).remove();
+
+ clients = clientsResource.findByClientId("authz-client");
+
+ assertTrue(clients.isEmpty());
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java
index 86526b9..fe20270 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java
@@ -19,85 +19,90 @@ package org.keycloak.testsuite.admin.concurrency;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import java.util.LinkedList;
+import java.util.Collection;
import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import org.keycloak.testsuite.admin.AbstractAdminTest;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public abstract class AbstractConcurrencyTest extends AbstractAdminTest {
+public abstract class AbstractConcurrencyTest extends AbstractTestRealmKeycloakTest {
- private static final int DEFAULT_THREADS = 5;
- private static final int DEFAULT_ITERATIONS = 20;
+ private static final int DEFAULT_THREADS = 4;
+ private static final int DEFAULT_NUMBER_OF_EXECUTIONS = 20 * DEFAULT_THREADS;
+
+ public static final String REALM_NAME = "test";
// If enabled only one request is allowed at the time. Useful for checking that test is working.
private static final boolean SYNCHRONIZED = false;
- protected void run(final KeycloakRunnable runnable) throws Throwable {
- run(runnable, DEFAULT_THREADS, DEFAULT_ITERATIONS);
+ @Override
+ public void configureTestRealm(RealmRepresentation testRealm) {
+ }
+
+ protected void run(final KeycloakRunnable... runnables) {
+ run(DEFAULT_THREADS, DEFAULT_NUMBER_OF_EXECUTIONS, runnables);
}
- protected void run(final KeycloakRunnable runnable, final int numThreads, final int numIterationsPerThread) throws Throwable {
- final CountDownLatch latch = new CountDownLatch(numThreads);
- final AtomicReference<Throwable> failed = new AtomicReference();
- final List<Thread> threads = new LinkedList<>();
- final Lock lock = SYNCHRONIZED ? new ReentrantLock() : null;
-
- for (int t = 0; t < numThreads; t++) {
- final int threadNum = t;
- Thread thread = new Thread() {
- @Override
- public void run() {
- Keycloak keycloak = null;
- try {
- if (lock != null) {
- lock.lock();
- }
-
- keycloak = Keycloak.getInstance(getAuthServerRoot().toString(), "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
- RealmResource realm = keycloak.realm(REALM_NAME);
- for (int i = 0; i < numIterationsPerThread && latch.getCount() > 0; i++) {
- log.infov("thread {0}, iteration {1}", threadNum, i);
- runnable.run(keycloak, realm, threadNum, i);
- }
- latch.countDown();
- } catch (Throwable t) {
- failed.compareAndSet(null, t);
- while (latch.getCount() > 0) {
- latch.countDown();
- }
- } finally {
- keycloak.close();
- if (lock != null) {
- lock.unlock();
- }
- }
+ protected void run(final int numThreads, final int totalNumberOfExecutions, final KeycloakRunnable... runnables) {
+ final ExecutorService service = SYNCHRONIZED
+ ? Executors.newSingleThreadExecutor()
+ : Executors.newFixedThreadPool(numThreads);
+
+ ThreadLocal<Keycloak> keycloaks = new ThreadLocal<Keycloak>() {
+ @Override
+ protected Keycloak initialValue() {
+ return Keycloak.getInstance(getAuthServerRoot().toString(), "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
+ }
+ };
+
+ AtomicInteger currentThreadIndex = new AtomicInteger();
+ Collection<Callable<Void>> tasks = new LinkedList<>();
+ Collection<Throwable> failures = new ConcurrentLinkedQueue<>();
+ final List<Callable<Void>> runnablesToTasks = new LinkedList<>();
+ for (KeycloakRunnable runnable : runnables) {
+ runnablesToTasks.add(() -> {
+ int arrayIndex = currentThreadIndex.getAndIncrement() % numThreads;
+ try {
+ runnable.run(arrayIndex % numThreads, keycloaks.get(), keycloaks.get().realm(REALM_NAME));
+ } catch (Throwable ex) {
+ failures.add(ex);
}
- };
- thread.start();
- threads.add(thread);
+ return null;
+ });
+ }
+ for (int i = 0; i < totalNumberOfExecutions; i ++) {
+ runnablesToTasks.forEach(tasks::add);
}
- latch.await();
-
- for (Thread t : threads) {
- t.join();
+ try {
+ service.invokeAll(tasks);
+ service.shutdown();
+ service.awaitTermination(3, TimeUnit.MINUTES);
+ } catch (InterruptedException ex) {
+ throw new RuntimeException(ex);
}
- if (failed.get() != null) {
- throw failed.get();
+ if (! failures.isEmpty()) {
+ RuntimeException ex = new RuntimeException("There were failures in threads. Failures count: " + failures.size());
+ failures.forEach(ex::addSuppressed);
+ failures.forEach(e -> log.error(e.getMessage(), e));
+ throw ex;
}
}
protected interface KeycloakRunnable {
- void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum);
+ void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java
index a2f4409..2d2053b 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java
@@ -17,12 +17,12 @@
package org.keycloak.testsuite.admin.concurrency;
-import org.junit.Assert;
-import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.RolesResource;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
@@ -31,7 +31,11 @@ import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import org.keycloak.testsuite.admin.ApiUtil;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
@@ -39,203 +43,198 @@ import static org.junit.Assert.fail;
*/
public class ConcurrencyTest extends AbstractConcurrencyTest {
- boolean passedCreateClient = false;
- boolean passedCreateRole = false;
+ public void concurrentTest(KeycloakRunnable... tasks) throws Throwable {
+ System.out.println("***************************");
+ long start = System.currentTimeMillis();
+ run(tasks);
+ long end = System.currentTimeMillis() - start;
+ System.out.println("took " + end + " ms");
+ }
- //@Test
+ @Test
public void testAllConcurrently() throws Throwable {
- Thread client = new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- createClient();
- passedCreateClient = true;
- } catch (Throwable throwable) {
- throw new RuntimeException(throwable);
- }
- }
- });
- Thread role = new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- createRole();
- passedCreateRole = true;
- } catch (Throwable throwable) {
- throw new RuntimeException(throwable);
- }
- }
- });
-
- client.start();
- role.start();
- client.join();
- role.join();
- Assert.assertTrue(passedCreateClient);
- Assert.assertTrue(passedCreateRole);
+ AtomicInteger uniqueCounter = new AtomicInteger(100000);
+ concurrentTest(
+ new CreateClient(uniqueCounter),
+ new CreateRemoveClient(uniqueCounter),
+ new CreateGroup(uniqueCounter),
+ new CreateRole(uniqueCounter)
+ );
}
@Test
public void createClient() throws Throwable {
- System.out.println("***************************");
- long start = System.currentTimeMillis();
- run(new KeycloakRunnable() {
- @Override
- public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
- String name = "c-" + threadNum + "-" + iterationNum;
- ClientRepresentation c = new ClientRepresentation();
- c.setClientId(name);
- Response response = realm.clients().create(c);
- String id = ApiUtil.getCreatedId(response);
- response.close();
-
- c = realm.clients().get(id).toRepresentation();
- assertNotNull(c);
- boolean found = false;
- for (ClientRepresentation r : realm.clients().findAll()) {
- if (r.getClientId().equals(name)) {
- found = true;
- break;
- }
- }
- if (!found) {
- fail("Client " + name + " not found in client list");
- }
- }
- });
- long end = System.currentTimeMillis() - start;
- System.out.println("createClient took " + end);
-
+ AtomicInteger uniqueCounter = new AtomicInteger();
+ concurrentTest(new CreateClient(uniqueCounter));
}
@Test
public void createGroup() throws Throwable {
- System.out.println("***************************");
- long start = System.currentTimeMillis();
- run(new KeycloakRunnable() {
- @Override
- public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
- String name = "c-" + threadNum + "-" + iterationNum;
- GroupRepresentation c = new GroupRepresentation();
- c.setName(name);
- Response response = realm.groups().add(c);
- String id = ApiUtil.getCreatedId(response);
- response.close();
-
- c = realm.groups().group(id).toRepresentation();
- assertNotNull(c);
- boolean found = false;
- for (GroupRepresentation r : realm.groups().groups()) {
- if (r.getName().equals(name)) {
- found = true;
- break;
- }
- }
- if (!found) {
- fail("Group " + name + " not found in group list");
- }
- }
- });
- long end = System.currentTimeMillis() - start;
- System.out.println("createGroup took " + end);
-
+ AtomicInteger uniqueCounter = new AtomicInteger();
+ concurrentTest(new CreateGroup(uniqueCounter));
}
@Test
- @Ignore
public void createRemoveClient() throws Throwable {
// FYI< this will fail as HSQL seems to be trying to perform table locks.
- System.out.println("***************************");
- long start = System.currentTimeMillis();
- run(new KeycloakRunnable() {
- @Override
- public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
- String name = "c-" + threadNum + "-" + iterationNum;
- ClientRepresentation c = new ClientRepresentation();
- c.setClientId(name);
- Response response = realm.clients().create(c);
- String id = ApiUtil.getCreatedId(response);
- response.close();
-
- c = realm.clients().get(id).toRepresentation();
- assertNotNull(c);
- boolean found = false;
- for (ClientRepresentation r : realm.clients().findAll()) {
- if (r.getClientId().equals(name)) {
- found = true;
- break;
- }
- }
- if (!found) {
- fail("Client " + name + " not found in client list");
- }
- realm.clients().get(id).remove();
- try {
- c = realm.clients().get(id).toRepresentation();
- fail("Client " + name + " should not be found. Should throw a 404");
- } catch (NotFoundException e) {
-
- }
- found = false;
- for (ClientRepresentation r : realm.clients().findAll()) {
- if (r.getClientId().equals(name)) {
- found = true;
- break;
- }
- }
- Assert.assertFalse("Client " + name + " should not be in client list", found);
+ AtomicInteger uniqueCounter = new AtomicInteger();
+ concurrentTest(new CreateRemoveClient(uniqueCounter));
+ }
- }
- });
- long end = System.currentTimeMillis() - start;
- System.out.println("createClient took " + end);
+ @Test
+ public void createClientRole() throws Throwable {
+ ClientRepresentation c = new ClientRepresentation();
+ c.setClientId("client");
+ Response response = adminClient.realm(REALM_NAME).clients().create(c);
+ final String clientId = ApiUtil.getCreatedId(response);
+ response.close();
+ AtomicInteger uniqueCounter = new AtomicInteger();
+ concurrentTest(new CreateClientRole(uniqueCounter, clientId));
}
-
@Test
public void createRole() throws Throwable {
- long start = System.currentTimeMillis();
- run(new KeycloakRunnable() {
- @Override
- public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
- String name = "r-" + threadNum + "-" + iterationNum;
- RoleRepresentation r = new RoleRepresentation(name, null, false);
- realm.roles().create(r);
- assertNotNull(realm.roles().get(name).toRepresentation());
- }
- });
- long end = System.currentTimeMillis() - start;
- System.out.println("createRole took " + end);
+ AtomicInteger uniqueCounter = new AtomicInteger();
+ run(new CreateRole(uniqueCounter));
+ }
+ private class CreateClient implements KeycloakRunnable {
+
+ private final AtomicInteger clientIndex;
+
+ public CreateClient(AtomicInteger clientIndex) {
+ this.clientIndex = clientIndex;
+ }
+
+ @Override
+ public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+ String name = "c-" + clientIndex.getAndIncrement();
+ ClientRepresentation c = new ClientRepresentation();
+ c.setClientId(name);
+ Response response = realm.clients().create(c);
+ String id = ApiUtil.getCreatedId(response);
+ response.close();
+
+ c = realm.clients().get(id).toRepresentation();
+ assertNotNull(c);
+ assertTrue("Client " + name + " not found in client list",
+ realm.clients().findAll().stream()
+ .map(ClientRepresentation::getClientId)
+ .filter(Objects::nonNull)
+ .anyMatch(name::equals));
+ }
}
- @Test
- public void createClientRole() throws Throwable {
- long start = System.currentTimeMillis();
- ClientRepresentation c = new ClientRepresentation();
- c.setClientId("client");
- Response response = realm.clients().create(c);
- final String clientId = ApiUtil.getCreatedId(response);
- response.close();
+ private class CreateRemoveClient implements KeycloakRunnable {
+
+ private final AtomicInteger clientIndex;
+
+ public CreateRemoveClient(AtomicInteger clientIndex) {
+ this.clientIndex = clientIndex;
+ }
- System.out.println("*********************************************");
+ @Override
+ public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+ String name = "c-" + clientIndex.getAndIncrement();
+ ClientRepresentation c = new ClientRepresentation();
+ c.setClientId(name);
+ final ClientsResource clients = realm.clients();
- run(new KeycloakRunnable() {
- @Override
- public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
- String name = "r-" + threadNum + "-" + iterationNum;
- RoleRepresentation r = new RoleRepresentation(name, null, false);
+ Response response = clients.create(c);
+ String id = ApiUtil.getCreatedId(response);
+ response.close();
+ final ClientResource client = clients.get(id);
- ClientResource client = realm.clients().get(clientId);
- client.roles().create(r);
+ c = client.toRepresentation();
+ assertNotNull(c);
+ assertTrue("Client " + name + " not found in client list",
+ clients.findAll().stream()
+ .map(ClientRepresentation::getClientId)
+ .filter(Objects::nonNull)
+ .anyMatch(name::equals));
+
+ client.remove();
+ try {
+ client.toRepresentation();
+ fail("Client " + name + " should not be found. Should throw a 404");
+ } catch (NotFoundException e) {
- assertNotNull(client.roles().get(name).toRepresentation());
}
- });
- long end = System.currentTimeMillis() - start;
- System.out.println("createClientRole took " + end);
- System.out.println("*********************************************");
+ assertFalse("Client " + name + " should now not present in client list",
+ clients.findAll().stream()
+ .map(ClientRepresentation::getClientId)
+ .filter(Objects::nonNull)
+ .anyMatch(name::equals));
+ }
}
+
+ private class CreateGroup implements KeycloakRunnable {
+
+ private final AtomicInteger uniqueIndex;
+
+ public CreateGroup(AtomicInteger uniqueIndex) {
+ this.uniqueIndex = uniqueIndex;
+ }
+
+ @Override
+ public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+ String name = "g-" + uniqueIndex.getAndIncrement();
+ GroupRepresentation c = new GroupRepresentation();
+ c.setName(name);
+ Response response = realm.groups().add(c);
+ String id = ApiUtil.getCreatedId(response);
+ response.close();
+
+ c = realm.groups().group(id).toRepresentation();
+ assertNotNull(c);
+ assertTrue("Group " + name + " not found in group list",
+ realm.groups().groups().stream()
+ .map(GroupRepresentation::getName)
+ .filter(Objects::nonNull)
+ .anyMatch(name::equals));
+ }
+ }
+
+ private class CreateClientRole implements KeycloakRunnable {
+
+ private final AtomicInteger uniqueCounter;
+ private final String clientId;
+
+ public CreateClientRole(AtomicInteger uniqueCounter, String clientId) {
+ this.uniqueCounter = uniqueCounter;
+ this.clientId = clientId;
+ }
+
+ @Override
+ public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+ String name = "cr-" + uniqueCounter.getAndIncrement();
+ RoleRepresentation r = new RoleRepresentation(name, null, false);
+
+ final RolesResource roles = realm.clients().get(clientId).roles();
+ roles.create(r);
+ assertNotNull(roles.get(name).toRepresentation());
+ }
+ }
+
+ private class CreateRole implements KeycloakRunnable {
+
+ private final AtomicInteger uniqueCounter;
+
+ public CreateRole(AtomicInteger uniqueCounter) {
+ this.uniqueCounter = uniqueCounter;
+ }
+
+ @Override
+ public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+ String name = "r-" + uniqueCounter.getAndIncrement();
+ RoleRepresentation r = new RoleRepresentation(name, null, false);
+
+ final RolesResource roles = realm.roles();
+ roles.create(r);
+ assertNotNull(roles.get(name).toRepresentation());
+ }
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
index ade3995..ff6f10f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
@@ -17,14 +17,11 @@
package org.keycloak.testsuite.admin.concurrency;
-import java.io.BufferedReader;
import java.io.IOException;
-import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -49,9 +46,21 @@ import org.junit.Before;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.testsuite.Retry;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.OAuthClient;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.http.client.CookieStore;
+import org.apache.http.impl.client.BasicCookieStore;
+import org.hamcrest.Matchers;
@@ -60,88 +69,95 @@ import org.keycloak.testsuite.util.OAuthClient;
*/
public class ConcurrentLoginTest extends AbstractConcurrencyTest {
- private static final int DEFAULT_THREADS = 10;
- private static final int DEFAULT_ITERATIONS = 20;
- private static final int CLIENTS_PER_THREAD = 10;
- private static final int DEFAULT_CLIENTS_COUNT = CLIENTS_PER_THREAD * DEFAULT_THREADS;
-
+ protected static final int DEFAULT_THREADS = 4;
+ protected static final int CLIENTS_PER_THREAD = 30;
+ protected static final int DEFAULT_CLIENTS_COUNT = CLIENTS_PER_THREAD * DEFAULT_THREADS;
+
@Before
public void beforeTest() {
+ createClients();
+ }
+
+ protected void createClients() {
+ final ClientsResource clients = adminClient.realm(REALM_NAME).clients();
for (int i = 0; i < DEFAULT_CLIENTS_COUNT; i++) {
- ClientRepresentation client = new ClientRepresentation();
- client.setClientId("client" + i);
- client.setDirectAccessGrantsEnabled(true);
- client.setRedirectUris(Arrays.asList("http://localhost:8180/auth/realms/master/app/*"));
- client.setWebOrigins(Arrays.asList("http://localhost:8180"));
- client.setSecret("password");
-
- log.debug("creating " + client.getClientId());
- Response create = adminClient.realm("test").clients().create(client);
- Assert.assertEquals(Response.Status.CREATED, create.getStatusInfo());
+ ClientRepresentation client = ClientBuilder.create()
+ .clientId("client" + i)
+ .directAccessGrants()
+ .redirectUris("http://localhost:8180/auth/realms/master/app/*")
+ .addWebOrigin("http://localhost:8180")
+ .secret("password")
+ .build();
+
+ Response create = clients.create(client);
+ String clientId = ApiUtil.getCreatedId(create);
create.close();
+ getCleanup(REALM_NAME).addClientUuid(clientId);
+ log.debugf("created %s [uuid=%s]", client.getClientId(), clientId);
}
log.debug("clients created");
}
-
- @Override
- protected void run(final KeycloakRunnable runnable) throws Throwable {
- run(runnable, DEFAULT_THREADS, DEFAULT_ITERATIONS);
+
+ @Test
+ public void concurrentLoginSingleUser() throws Throwable {
+ log.info("*********************************************");
+ long start = System.currentTimeMillis();
+
+ AtomicReference<String> userSessionId = new AtomicReference<>();
+ LoginTask loginTask = null;
+
+ try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) {
+ loginTask = new LoginTask(httpClient, userSessionId, 100, 1, Arrays.asList(
+ createHttpClientContextForUser(httpClient, "test-user@localhost", "password")
+ ));
+ run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, loginTask);
+ int clientSessionsCount = testingClient.testing().getClientSessionsCountInUserSession("test", userSessionId.get());
+ Assert.assertEquals(1 + DEFAULT_CLIENTS_COUNT, clientSessionsCount);
+ } finally {
+ long end = System.currentTimeMillis() - start;
+ log.infof("Statistics: %s", loginTask == null ? "??" : loginTask.getHistogram());
+ log.info("concurrentLoginSingleUser took " + (end/1000) + "s");
+ log.info("*********************************************");
+ }
}
-
+
+ protected HttpClientContext createHttpClientContextForUser(final CloseableHttpClient httpClient, String userName, String password) throws IOException {
+ final HttpClientContext context = HttpClientContext.create();
+ CookieStore cookieStore = new BasicCookieStore();
+ context.setCookieStore(cookieStore);
+ HttpUriRequest request = handleLogin(getPageContent(oauth.getLoginFormUrl(), httpClient, context), userName, password);
+ log.debug("Executing login request");
+ Assert.assertTrue(parseAndCloseResponse(httpClient.execute(request, context)).contains("<title>AUTH_RESPONSE</title>"));
+ return context;
+ }
+
@Test
- public void concurrentLogin() throws Throwable {
- System.out.println("*********************************************");
+ public void concurrentLoginMultipleUsers() throws Throwable {
+ log.info("*********************************************");
long start = System.currentTimeMillis();
+ AtomicReference<String> userSessionId = new AtomicReference<>();
+ LoginTask loginTask = null;
+
try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) {
-
- HttpUriRequest request = handleLogin(getPageContent(oauth.getLoginFormUrl(), httpClient, null), "test-user@localhost", "password");
-
- log.debug("Executing login request");
-
- Assert.assertTrue(parseAndCloseResponse(httpClient.execute(request)).contains("<title>AUTH_RESPONSE</title>"));
-
- run(new KeycloakRunnable() {
- @Override
- public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
- OAuthClient oauth = new OAuthClient();
- oauth.init(adminClient, driver);
-
- int startIndex = CLIENTS_PER_THREAD * threadNum;
- for (int i = startIndex; i < startIndex + CLIENTS_PER_THREAD; i++) {
- oauth.clientId("client" + i);
- log.trace("Accessing login page for " + oauth.getClientId() + " threat " + threadNum + " iteration " + iterationNum);
- try {
- final HttpClientContext context = HttpClientContext.create();
-
- String pageContent = getPageContent(oauth.getLoginFormUrl(), httpClient, context);
- String currentUrl = context.getRedirectLocations().get(0).toString();
-
- Assert.assertTrue(pageContent.contains("<title>AUTH_RESPONSE</title>"));
-
- String code = getQueryFromUrl(currentUrl).get(OAuth2Constants.CODE);
- OAuthClient.AccessTokenResponse accessRes = oauth.doAccessTokenRequest(code, "password");
- Assert.assertEquals("AccessTokenResponse: error: '" + accessRes.getError() + "' desc: '" + accessRes.getErrorDescription() + "'",
- 200, accessRes.getStatusCode());
-
- OAuthClient.AccessTokenResponse refreshRes = oauth.doRefreshTokenRequest(accessRes.getRefreshToken(), "password");
- Assert.assertEquals("AccessTokenResponse: error: '" + refreshRes.getError() + "' desc: '" + refreshRes.getErrorDescription() + "'",
- 200, refreshRes.getStatusCode());
- } catch (Exception ex) {
- throw new RuntimeException(ex);
- }
- }
- }
- });
- }
+ loginTask = new LoginTask(httpClient, userSessionId, 100, 1, Arrays.asList(
+ createHttpClientContextForUser(httpClient, "test-user@localhost", "password"),
+ createHttpClientContextForUser(httpClient, "john-doh@localhost", "password"),
+ createHttpClientContextForUser(httpClient, "roleRichUser", "password")
+ ));
- long end = System.currentTimeMillis() - start;
- System.out.println("concurrentLogin took " + (end/1000) + "s");
- System.out.println("*********************************************");
+ run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, loginTask);
+ int clientSessionsCount = testingClient.testing().getClientSessionsCountInUserSession("test", userSessionId.get());
+ Assert.assertEquals(1 + DEFAULT_CLIENTS_COUNT / 3 + (DEFAULT_CLIENTS_COUNT % 3 <= 0 ? 0 : 1), clientSessionsCount);
+ } finally {
+ long end = System.currentTimeMillis() - start;
+ log.infof("Statistics: %s", loginTask == null ? "??" : loginTask.getHistogram());
+ log.info("concurrentLoginMultipleUsers took " + (end/1000) + "s");
+ log.info("*********************************************");
+ }
}
-
- private String getPageContent(String url, CloseableHttpClient httpClient, HttpClientContext context) throws Exception {
+ protected String getPageContent(String url, CloseableHttpClient httpClient, HttpClientContext context) throws IOException {
HttpGet request = new HttpGet(url);
request.setHeader("User-Agent", "Mozilla/5.0");
@@ -149,31 +165,18 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
request.setHeader("Accept-Language", "en-US,en;q=0.5");
- if (context != null) {
- return parseAndCloseResponse(httpClient.execute(request, context));
- } else {
- return parseAndCloseResponse(httpClient.execute(request));
- }
-
+ return parseAndCloseResponse(httpClient.execute(request, context));
}
- private String parseAndCloseResponse(CloseableHttpResponse response) throws UnsupportedOperationException, IOException {
+ protected String parseAndCloseResponse(CloseableHttpResponse response) {
try {
int responseCode = response.getStatusLine().getStatusCode();
+ String resp = EntityUtils.toString(response.getEntity());
+
if (responseCode != 200) {
- log.debug("Response Code : " + responseCode);
- }
- BufferedReader rd = new BufferedReader(
- new InputStreamReader(response.getEntity().getContent()));
- StringBuilder result = new StringBuilder();
- String line;
- while ((line = rd.readLine()) != null) {
- result.append(line);
- }
- if (responseCode != 200) {
- log.debug(result.toString());
+ log.debugf("Response Code: %d, Body: %s", responseCode, resp);
}
- return result.toString();
+ return resp;
} catch (IOException | UnsupportedOperationException ex) {
throw new RuntimeException(ex);
} finally {
@@ -185,16 +188,15 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
}
}
}
-
- private HttpUriRequest handleLogin(String html, String username, String password) throws UnsupportedEncodingException {
- System.out.println("Extracting form's data...");
+ protected HttpUriRequest handleLogin(String html, String username, String password) throws UnsupportedEncodingException {
+ log.debug("Extracting form's data...");
// Keycloak form id
Element loginform = Jsoup.parse(html).getElementById("kc-form-login");
String method = loginform.attr("method");
String action = loginform.attr("action");
-
+
List<NameValuePair> paramList = new ArrayList<>();
for (Element inputElement : loginform.getElementsByTag("input")) {
@@ -206,9 +208,9 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
paramList.add(new BasicNameValuePair(key, password));
}
}
-
+
boolean isPost = method != null && "post".equalsIgnoreCase(method);
-
+
if (isPost) {
HttpPost req = new HttpPost(action);
@@ -225,8 +227,8 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
throw new UnsupportedOperationException("not supported yet!");
}
}
-
- private Map<String, String> getQueryFromUrl(String url) throws URISyntaxException {
+
+ private static Map<String, String> getQueryFromUrl(String url) throws URISyntaxException {
Map<String, String> m = new HashMap<>();
List<NameValuePair> pairs = URLEncodedUtils.parse(new URI(url), "UTF-8");
for (NameValuePair p : pairs) {
@@ -235,5 +237,98 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
return m;
}
+ public class LoginTask implements KeycloakRunnable {
+
+ private final AtomicInteger clientIndex = new AtomicInteger();
+ private final ThreadLocal<OAuthClient> oauthClient = new ThreadLocal<OAuthClient>() {
+ @Override
+ protected OAuthClient initialValue() {
+ OAuthClient oauth1 = new OAuthClient();
+ oauth1.init(adminClient, driver);
+ return oauth1;
+ }
+ };
+
+ private final CloseableHttpClient httpClient;
+ private final AtomicReference<String> userSessionId;
+
+ private final int retryDelayMs;
+ private final int retryCount;
+ private final AtomicInteger[] retryHistogram;
+ private final AtomicInteger totalInvocations = new AtomicInteger();
+ private final List<HttpClientContext> clientContexts;
+
+ public LoginTask(CloseableHttpClient httpClient, AtomicReference<String> userSessionId, int retryDelayMs, int retryCount, List<HttpClientContext> clientContexts) {
+ this.httpClient = httpClient;
+ this.userSessionId = userSessionId;
+ this.retryDelayMs = retryDelayMs;
+ this.retryCount = retryCount;
+ this.retryHistogram = new AtomicInteger[retryCount];
+ for (int i = 0; i < retryHistogram.length; i ++) {
+ retryHistogram[i] = new AtomicInteger();
+ }
+ this.clientContexts = clientContexts;
+ }
+
+ @Override
+ public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+ int i = clientIndex.getAndIncrement();
+ OAuthClient oauth1 = oauthClient.get();
+ oauth1.clientId("client" + i);
+ log.infof("%d [%s]: Accessing login page for %s", threadIndex, Thread.currentThread().getName(), oauth1.getClientId());
+
+ final HttpClientContext templateContext = clientContexts.get(i % clientContexts.size());
+ final HttpClientContext context = HttpClientContext.create();
+ context.setCookieStore(templateContext.getCookieStore());
+ String pageContent = getPageContent(oauth1.getLoginFormUrl(), httpClient, context);
+ Assert.assertThat(pageContent, Matchers.containsString("<title>AUTH_RESPONSE</title>"));
+ Assert.assertThat(context.getRedirectLocations(), Matchers.notNullValue());
+ Assert.assertThat(context.getRedirectLocations(), Matchers.not(Matchers.empty()));
+ String currentUrl = context.getRedirectLocations().get(0).toString();
+ String code = getQueryFromUrl(currentUrl).get(OAuth2Constants.CODE);
+
+ AtomicReference<OAuthClient.AccessTokenResponse> accessResRef = new AtomicReference<>();
+ totalInvocations.incrementAndGet();
+
+ // obtain access + refresh token via code-to-token flow
+ OAuthClient.AccessTokenResponse accessRes = oauth1.doAccessTokenRequest(code, "password");
+ Assert.assertEquals("AccessTokenResponse: client: " + oauth1.getClientId() + ", error: '" + accessRes.getError() + "' desc: '" + accessRes.getErrorDescription() + "'",
+ 200, accessRes.getStatusCode());
+ accessResRef.set(accessRes);
+
+ // Refresh access + refresh token using refresh token
+ int invocationIndex = Retry.execute(() -> {
+ OAuthClient.AccessTokenResponse refreshRes = oauth1.doRefreshTokenRequest(accessResRef.get().getRefreshToken(), "password");
+ Assert.assertEquals("AccessTokenResponse: client: " + oauth1.getClientId() + ", error: '" + refreshRes.getError() + "' desc: '" + refreshRes.getErrorDescription() + "'",
+ 200, refreshRes.getStatusCode());
+ }, retryCount, retryDelayMs);
+
+ retryHistogram[invocationIndex].incrementAndGet();
+
+ if (userSessionId.get() == null) {
+ AccessToken token = oauth1.verifyToken(accessResRef.get().getAccessToken());
+ userSessionId.set(token.getSessionState());
+ }
+ }
+
+ public int getRetryDelayMs() {
+ return retryDelayMs;
+ }
+
+ public int getRetryCount() {
+ return retryCount;
+ }
+
+ public Map<Integer, Integer> getHistogram() {
+ Map<Integer, Integer> res = new LinkedHashMap<>(retryCount);
+ for (int i = 0; i < retryHistogram.length; i ++) {
+ AtomicInteger item = retryHistogram[i];
+
+ res.put(i * retryDelayMs, item.get());
+ }
+ return res;
+ }
+ }
+
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java
index 8386aa6..5e9a473 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java
@@ -16,16 +16,15 @@
*/
package org.keycloak.testsuite.admin;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
-import org.keycloak.authorization.AuthorizationProvider;
-import org.keycloak.authorization.AuthorizationProviderFactory;
import org.keycloak.authorization.model.Resource;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.ClientTemplateRepresentation;
import org.keycloak.representations.idm.authorization.Logic;
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
@@ -48,12 +47,15 @@ import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.AdminClientUtil;
import javax.ws.rs.ClientErrorException;
+import javax.ws.rs.core.Response;
import java.util.LinkedList;
import java.util.List;
+import static org.keycloak.testsuite.admin.ImpersonationDisabledTest.IMPERSONATION_DISABLED;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
/**
@@ -65,6 +67,11 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
public static final String CLIENT_NAME = "application";
+ @Deployment
+ public static WebArchive deploy() {
+ return RunOnServerDeployment.create(FineGrainAdminUnitTest.class);
+ }
+
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation testRealmRep = new RealmRepresentation();
@@ -75,37 +82,20 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
}
public static void setupDemo(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName(TEST);
- ClientModel client = realm.addClient("sales-pipeline-application");
+ realm.addRole("realm-role");
+ ClientModel client = realm.addClient("sales-application");
RoleModel clientAdmin = client.addRole("admin");
client.addRole("leader-creator");
client.addRole("viewLeads");
- ClientModel client2 = realm.addClient("market-analysis-application");
- RoleModel client2Admin = client2.addRole("admin");
- client2.addRole("market-manager");
- client2.addRole("viewMarkets");
GroupModel sales = realm.createGroup("sales");
- RoleModel salesAppsAdminRole = realm.addRole("sales-apps-admin");
- salesAppsAdminRole.addCompositeRole(clientAdmin);
- salesAppsAdminRole.addCompositeRole(client2Admin);
- ClientModel realmManagementClient = realm.getClientByClientId("realm-management");
- RoleModel queryClient = realmManagementClient.getRole(AdminRoles.QUERY_CLIENTS);
UserModel admin = session.users().addUser(realm, "salesManager");
admin.setEnabled(true);
session.userCredentialManager().updateCredential(realm, admin, UserCredentialModel.password("password"));
- admin = session.users().addUser(realm, "sales-group-admin");
- admin.setEnabled(true);
- session.userCredentialManager().updateCredential(realm, admin, UserCredentialModel.password("password"));
- admin = session.users().addUser(realm, "sales-it");
- admin.setEnabled(true);
- session.userCredentialManager().updateCredential(realm, admin, UserCredentialModel.password("password"));
- admin = session.users().addUser(realm, "sales-pipeline-admin");
- admin.setEnabled(true);
- session.userCredentialManager().updateCredential(realm, admin, UserCredentialModel.password("password"));
- admin = session.users().addUser(realm, "client-admin");
+
+ admin = session.users().addUser(realm, "sales-admin");
admin.setEnabled(true);
- admin.grantRole(queryClient);
session.userCredentialManager().updateCredential(realm, admin, UserCredentialModel.password("password"));
UserModel user = session.users().addUser(realm, "salesman");
@@ -383,10 +373,11 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
}
-
+ @Override
protected boolean isImportAfterEachMethod() {
return true;
}
+
//@Test
public void testDemo() throws Exception {
testingClient.server().run(FineGrainAdminUnitTest::setupDemo);
@@ -394,7 +385,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
}
- @Test
+ //@Test
public void testEvaluationLocal() throws Exception {
testingClient.server().run(FineGrainAdminUnitTest::setupPolices);
testingClient.server().run(FineGrainAdminUnitTest::setupUsers);
@@ -432,7 +423,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
realmClient.realm(TEST).clients().get(client.getId()).update(client);
Assert.fail("should fail with forbidden exception");
} catch (ClientErrorException e) {
- Assert.assertEquals(e.getResponse().getStatus(), 403);
+ Assert.assertEquals(403, e.getResponse().getStatus());
}
client.setFullScopeAllowed(false);
@@ -443,7 +434,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
realmClient.realm(TEST).clients().get(client.getId()).update(client);
Assert.fail("should fail with forbidden exception");
} catch (ClientErrorException e) {
- Assert.assertEquals(e.getResponse().getStatus(), 403);
+ Assert.assertEquals(403, e.getResponse().getStatus());
}
client.setClientTemplate(null);
@@ -453,13 +444,13 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
realmClient.realm(TEST).clients().get(client.getId()).getScopeMappings().realmLevel().add(realmRoleSet);
Assert.fail("should fail with forbidden exception");
} catch (ClientErrorException e) {
- Assert.assertEquals(e.getResponse().getStatus(), 403);
+ Assert.assertEquals(403, e.getResponse().getStatus());
}
}
// test illegal impersonation
- {
+ if (!IMPERSONATION_DISABLED) {
Keycloak realmClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(),
TEST, "nomap-admin", "password", Constants.ADMIN_CLI_CLIENT_ID, null);
realmClient.realm(TEST).users().get(user1.getId()).impersonate();
@@ -470,7 +461,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
realmClient.realm(TEST).users().get(anotherAdmin.getId()).impersonate();
Assert.fail("should fail with forbidden exception");
} catch (ClientErrorException e) {
- Assert.assertEquals(e.getResponse().getStatus(), 403);
+ Assert.assertEquals(403, e.getResponse().getStatus());
}
@@ -536,7 +527,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
realmClient.realm(TEST).users().get(user1.getId()).roles().realmLevel().add(realmRoleSet);
Assert.fail("should fail with forbidden exception");
} catch (ClientErrorException e) {
- Assert.assertEquals(e.getResponse().getStatus(), 403);
+ Assert.assertEquals(403, e.getResponse().getStatus());
}
}
@@ -547,7 +538,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
realmClient.realm(TEST).users().get(user1.getId()).roles().realmLevel().add(realmRoleSet);
Assert.fail("should fail with forbidden exception");
} catch (ClientErrorException e) {
- Assert.assertEquals(e.getResponse().getStatus(), 403);
+ Assert.assertEquals(403, e.getResponse().getStatus());
}
}
@@ -564,21 +555,21 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
realmClient.realm(TEST).users().get(groupMember.getId()).roles().clientLevel(client.getId()).remove(clientRoleSet);
roles = realmClient.realm(TEST).users().get(groupMember.getId()).roles().realmLevel().listAvailable();
- Assert.assertEquals(roles.size(), 1);
+ Assert.assertEquals(1, roles.size());
realmClient.realm(TEST).users().get(groupMember.getId()).roles().realmLevel().add(realmRoleSet);
realmClient.realm(TEST).users().get(groupMember.getId()).roles().realmLevel().remove(realmRoleSet);
try {
realmClient.realm(TEST).users().get(groupMember.getId()).roles().realmLevel().add(realmRole2Set);
Assert.fail("should fail with forbidden exception");
} catch (ClientErrorException e) {
- Assert.assertEquals(e.getResponse().getStatus(), 403);
+ Assert.assertEquals(403, e.getResponse().getStatus());
}
try {
realmClient.realm(TEST).users().get(user1.getId()).roles().realmLevel().add(realmRoleSet);
Assert.fail("should fail with forbidden exception");
} catch (ClientErrorException e) {
- Assert.assertEquals(e.getResponse().getStatus(), 403);
+ Assert.assertEquals(403, e.getResponse().getStatus());
}
@@ -603,7 +594,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
realmClient.realm(TEST).users().get(user1.getId()).roles().realmLevel().add(realmRoleSet);
Assert.fail("should fail with forbidden exception");
} catch (ClientErrorException e) {
- Assert.assertEquals(e.getResponse().getStatus(), 403);
+ Assert.assertEquals(403, e.getResponse().getStatus());
}
}
@@ -658,6 +649,57 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
}
}
+
+ // KEYCLOAK-5152
+ @Test
+ public void testMasterRealmWithComposites() throws Exception {
+ RoleRepresentation composite = new RoleRepresentation();
+ composite.setName("composite");
+ composite.setComposite(true);
+ adminClient.realm(TEST).roles().create(composite);
+ composite = adminClient.realm(TEST).roles().get("composite").toRepresentation();
+
+ ClientRepresentation client = adminClient.realm(TEST).clients().findByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID).get(0);
+ RoleRepresentation createClient = adminClient.realm(TEST).clients().get(client.getId()).roles().get(AdminRoles.CREATE_CLIENT).toRepresentation();
+ RoleRepresentation queryRealms = adminClient.realm(TEST).clients().get(client.getId()).roles().get(AdminRoles.QUERY_REALMS).toRepresentation();
+ List<RoleRepresentation> composites = new LinkedList<>();
+ composites.add(createClient);
+ composites.add(queryRealms);
+ adminClient.realm(TEST).rolesById().addComposites(composite.getId(), composites);
+ }
+
+ public static void setup5152(KeycloakSession session) {
+ RealmModel realm = session.realms().getRealmByName(TEST);
+ ClientModel realmAdminClient = realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID);
+ RoleModel realmAdminRole = realmAdminClient.getRole(AdminRoles.REALM_ADMIN);
+
+ UserModel realmUser = session.users().addUser(realm, "realm-admin");
+ realmUser.grantRole(realmAdminRole);
+ realmUser.setEnabled(true);
+ session.userCredentialManager().updateCredential(realm, realmUser, UserCredentialModel.password("password"));
+ }
+
+ // KEYCLOAK-5152
+ @Test
+ public void testRealmWithComposites() throws Exception {
+ testingClient.server().run(FineGrainAdminUnitTest::setup5152);
+
+ Keycloak realmClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(),
+ TEST, "realm-admin", "password", Constants.ADMIN_CLI_CLIENT_ID, null);
+
+ RoleRepresentation composite = new RoleRepresentation();
+ composite.setName("composite");
+ composite.setComposite(true);
+ realmClient.realm(TEST).roles().create(composite);
+ composite = adminClient.realm(TEST).roles().get("composite").toRepresentation();
+
+ ClientRepresentation client = adminClient.realm(TEST).clients().findByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID).get(0);
+ RoleRepresentation viewUsers = adminClient.realm(TEST).clients().get(client.getId()).roles().get(AdminRoles.CREATE_CLIENT).toRepresentation();
+
+ List<RoleRepresentation> composites = new LinkedList<>();
+ composites.add(viewUsers);
+ realmClient.realm(TEST).rolesById().addComposites(composite.getId(), composites);
+ }
// testRestEvaluationMasterRealm
// testRestEvaluationMasterAdminTestRealm
@@ -700,6 +742,91 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
testingClient.server().run(FineGrainAdminUnitTest::invokeDelete);
}
+ // KEYCLOAK-5211
+ @Test
+ public void testCreateRealmCreateClient() throws Exception {
+ ClientRepresentation rep = new ClientRepresentation();
+ rep.setName("fullScopedClient");
+ rep.setClientId("fullScopedClient");
+ rep.setFullScopeAllowed(true);
+ rep.setSecret("618268aa-51e6-4e64-93c4-3c0bc65b8171");
+ rep.setProtocol("openid-connect");
+ rep.setPublicClient(false);
+ rep.setEnabled(true);
+ adminClient.realm("master").clients().create(rep);
+
+ Keycloak realmClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(),
+ "master", "admin", "admin", "fullScopedClient", "618268aa-51e6-4e64-93c4-3c0bc65b8171");
+
+ RealmRepresentation newRealm=new RealmRepresentation();
+ newRealm.setRealm("anotherRealm");
+ newRealm.setId("anotherRealm");
+ newRealm.setEnabled(true);
+ realmClient.realms().create(newRealm);
+
+ ClientRepresentation newClient = new ClientRepresentation();
+
+ try {
+ newClient.setName("newClient");
+ newClient.setClientId("newClient");
+ newClient.setFullScopeAllowed(true);
+ newClient.setSecret("secret");
+ newClient.setProtocol("openid-connect");
+ newClient.setPublicClient(false);
+ newClient.setEnabled(true);
+ Response response = realmClient.realm("anotherRealm").clients().create(newClient);
+ Assert.assertEquals(403, response.getStatus());
+
+ realmClient.close();
+ realmClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(),
+ "master", "admin", "admin", "fullScopedClient", "618268aa-51e6-4e64-93c4-3c0bc65b8171");
+ response = realmClient.realm("anotherRealm").clients().create(newClient);
+ Assert.assertEquals(201, response.getStatus());
+ } finally {
+ adminClient.realm("anotherRealm").remove();
+
+ }
+
+
+ }
+
+ // KEYCLOAK-5211
+ @Test
+ public void testCreateRealmCreateClientWithMaster() throws Exception {
+ ClientRepresentation rep = new ClientRepresentation();
+ rep.setName("fullScopedClient");
+ rep.setClientId("fullScopedClient");
+ rep.setFullScopeAllowed(true);
+ rep.setSecret("618268aa-51e6-4e64-93c4-3c0bc65b8171");
+ rep.setProtocol("openid-connect");
+ rep.setPublicClient(false);
+ rep.setEnabled(true);
+ adminClient.realm("master").clients().create(rep);
+
+ RealmRepresentation newRealm=new RealmRepresentation();
+ newRealm.setRealm("anotherRealm");
+ newRealm.setId("anotherRealm");
+ newRealm.setEnabled(true);
+ adminClient.realms().create(newRealm);
+
+ try {
+ ClientRepresentation newClient = new ClientRepresentation();
+
+ newClient.setName("newClient");
+ newClient.setClientId("newClient");
+ newClient.setFullScopeAllowed(true);
+ newClient.setSecret("secret");
+ newClient.setProtocol("openid-connect");
+ newClient.setPublicClient(false);
+ newClient.setEnabled(true);
+ Response response = adminClient.realm("anotherRealm").clients().create(newClient);
+ Assert.assertEquals(201, response.getStatus());
+ } finally {
+ adminClient.realm("anotherRealm").remove();
+
+ }
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java
index 7c6ced5..3318a6d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java
@@ -16,6 +16,8 @@
*/
package org.keycloak.testsuite.admin;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
@@ -40,6 +42,7 @@ import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.AdminClientUtil;
import javax.ws.rs.ClientErrorException;
@@ -58,6 +61,11 @@ public class IllegalAdminUpgradeTest extends AbstractKeycloakTest {
public static final String CLIENT_NAME = "application";
+ @Deployment
+ public static WebArchive deploy() {
+ return RunOnServerDeployment.create(FineGrainAdminUnitTest.class);
+ }
+
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation testRealmRep = new RealmRepresentation();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ImpersonationDisabledTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ImpersonationDisabledTest.java
index ef1c1c3..b1485c5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ImpersonationDisabledTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ImpersonationDisabledTest.java
@@ -32,11 +32,13 @@ import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
* @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
*/
public class ImpersonationDisabledTest extends AbstractAdminTest {
+
+ public static boolean IMPERSONATION_DISABLED = "impersonation".equals(System.getProperty("feature.name"))
+ && "disabled".equals(System.getProperty("feature.value"));
@BeforeClass
public static void enabled() {
- Assume.assumeTrue("impersonation".equals(System.getProperty("feature.name"))
- && "disabled".equals(System.getProperty("feature.value")));
+ Assume.assumeTrue(IMPERSONATION_DISABLED);
}
@Test
@@ -44,6 +46,7 @@ public class ImpersonationDisabledTest extends AbstractAdminTest {
String impersonatedUserId = adminClient.realm(TEST).users().search("test-user@localhost", 0, 1).get(0).getId();
try {
+ log.debug("--Expected javax.ws.rs.WebApplicationException--");
adminClient.realms().realm("test").users().get(impersonatedUserId).impersonate();
} catch (ServerErrorException e) {
assertEquals(Response.Status.NOT_IMPLEMENTED.getStatusCode(), e.getResponse().getStatus());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.java
new file mode 100644
index 0000000..303cfd6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.admin;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.util.GreenMailRule;
+import org.keycloak.testsuite.util.UserBuilder;
+
+import javax.mail.internet.MimeMessage;
+import javax.ws.rs.core.Response;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.keycloak.util.JsonSerialization.writeValueAsPrettyString;
+
+/**
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>
+ */
+public class SMTPConnectionTest extends AbstractKeycloakTest {
+
+ @Rule
+ public GreenMailRule greenMailRule = new GreenMailRule();
+ private RealmResource realm;
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ }
+
+ @Before
+ public void before() {
+ realm = adminClient.realm("master");
+ List<UserRepresentation> admin = realm.users().search("admin", 0, 1);
+ UserRepresentation user = UserBuilder.edit(admin.get(0)).email("admin@localhost").build();
+ realm.users().get(user.getId()).update(user);
+ }
+
+ private String settings(String host, String port, String from, String auth, String ssl, String starttls,
+ String username, String password) throws Exception {
+ Map<String, String> config = new HashMap<>();
+ config.put("host", host);
+ config.put("port", port);
+ config.put("from", from);
+ config.put("auth", auth);
+ config.put("ssl", ssl);
+ config.put("starttls", starttls);
+ config.put("user", username);
+ config.put("password", password);
+ return writeValueAsPrettyString(config);
+ }
+
+ @Test
+ public void testWithEmptySettings() throws Exception {
+ Response response = realm.testSMTPConnection(settings(null, null, null, null, null, null,
+ null, null));
+ assertStatus(response, 500);
+ }
+
+ @Test
+ public void testWithProperSettings() throws Exception {
+ Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", null, null, null,
+ null, null));
+ assertStatus(response, 204);
+ assertMailReceived();
+ }
+
+ @Test
+ public void testWithAuthEnabledCredentialsEmpty() throws Exception {
+ Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", "true", null, null,
+ null, null));
+ assertStatus(response, 500);
+ }
+
+ @Test
+ public void testWithAuthEnabledValidCredentials() throws Exception {
+ greenMailRule.credentials("admin@localhost", "admin");
+ Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", "true", null, null,
+ "admin@localhost", "admin"));
+ assertStatus(response, 204);
+ }
+
+ private void assertStatus(Response response, int status) {
+ assertEquals(status, response.getStatus());
+ response.close();
+ }
+
+ private void assertMailReceived() {
+ if (greenMailRule.getReceivedMessages().length == 1) {
+ try {
+ MimeMessage message = greenMailRule.getReceivedMessages()[0];
+ assertEquals("[KEYCLOAK] - SMTP test message", message.getSubject());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ } else {
+ fail("E-mail was not received");
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 567b284..58193e9 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -18,9 +18,11 @@
package org.keycloak.testsuite.admin;
import org.hamcrest.Matchers;
+import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
@@ -29,9 +31,13 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RoleMappingResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.common.util.Base64;
+import org.keycloak.credential.CredentialModel;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.Constants;
+import org.keycloak.models.PasswordPolicy;
+import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
@@ -44,10 +50,13 @@ import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.page.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.ProceedPage;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.GreenMailRule;
@@ -65,12 +74,15 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
-
import java.util.concurrent.atomic.AtomicInteger;
+
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -90,7 +102,6 @@ public class UserTest extends AbstractAdminTest {
@Page
protected LoginPasswordUpdatePage passwordUpdatePage;
-
@ArquillianResource
protected OAuthClient oAuthClient;
@@ -98,11 +109,22 @@ public class UserTest extends AbstractAdminTest {
protected InfoPage infoPage;
@Page
+ protected ProceedPage proceedPage;
+
+ @Page
protected ErrorPage errorPage;
@Page
protected LoginPage loginPage;
+ @Deployment
+ public static WebArchive deploy() {
+ return RunOnServerDeployment.create(
+ AbstractAdminTest.class,
+ AbstractTestRealmKeycloakTest.class,
+ UserResource.class);
+ }
+
public String createUser() {
return createUser("user1", "user1@localhost");
}
@@ -169,6 +191,73 @@ public class UserTest extends AbstractAdminTest {
}
@Test
+ public void createUserWithHashedCredentials() {
+ UserRepresentation user = new UserRepresentation();
+ user.setUsername("user_creds");
+ user.setEmail("email@localhost");
+
+ CredentialRepresentation hashedPassword = new CredentialRepresentation();
+ hashedPassword.setAlgorithm("my-algorithm");
+ hashedPassword.setCounter(11);
+ hashedPassword.setCreatedDate(1001l);
+ hashedPassword.setDevice("deviceX");
+ hashedPassword.setDigits(6);
+ hashedPassword.setHashIterations(22);
+ hashedPassword.setHashedSaltedValue("ABC");
+ hashedPassword.setPeriod(99);
+ hashedPassword.setSalt(Base64.encodeBytes("theSalt".getBytes()));
+ hashedPassword.setType(CredentialRepresentation.PASSWORD);
+
+ user.setCredentials(Arrays.asList(hashedPassword));
+
+ createUser(user);
+
+ CredentialModel credentialHashed = fetchCredentials("user_creds");
+ assertNotNull("Expecting credential", credentialHashed);
+ assertEquals("my-algorithm", credentialHashed.getAlgorithm());
+ assertEquals(11, credentialHashed.getCounter());
+ assertEquals(Long.valueOf(1001), credentialHashed.getCreatedDate());
+ assertEquals("deviceX", credentialHashed.getDevice());
+ assertEquals(6, credentialHashed.getDigits());
+ assertEquals(22, credentialHashed.getHashIterations());
+ assertEquals("ABC", credentialHashed.getValue());
+ assertEquals(99, credentialHashed.getPeriod());
+ assertEquals("theSalt", new String(credentialHashed.getSalt()));
+ assertEquals(CredentialRepresentation.PASSWORD, credentialHashed.getType());
+ }
+
+ @Test
+ public void createUserWithRawCredentials() {
+ UserRepresentation user = new UserRepresentation();
+ user.setUsername("user_rawpw");
+ user.setEmail("email.raw@localhost");
+
+ CredentialRepresentation rawPassword = new CredentialRepresentation();
+ rawPassword.setValue("ABCD");
+ rawPassword.setType(CredentialRepresentation.PASSWORD);
+ user.setCredentials(Arrays.asList(rawPassword));
+
+ createUser(user);
+
+ CredentialModel credential = fetchCredentials("user_rawpw");
+ assertNotNull("Expecting credential", credential);
+ assertEquals(PasswordPolicy.HASH_ALGORITHM_DEFAULT, credential.getAlgorithm());
+ assertEquals(PasswordPolicy.HASH_ITERATIONS_DEFAULT, credential.getHashIterations());
+ assertNotEquals("ABCD", credential.getValue());
+ assertEquals(CredentialRepresentation.PASSWORD, credential.getType());
+ }
+
+ private CredentialModel fetchCredentials(String username) {
+ return getTestingClient().server(REALM_NAME).fetch(session -> {
+ RealmModel realm = session.getContext().getRealm();
+ UserModel user = session.users().getUserByUsername(username, realm);
+ List<CredentialModel> storedCredentialsByType = session.userCredentialManager().getStoredCredentialsByType(realm, user, CredentialRepresentation.PASSWORD);
+ System.out.println(storedCredentialsByType.size());
+ return storedCredentialsByType.get(0);
+ }, CredentialModel.class);
+ }
+
+ @Test
public void createDuplicatedUser3() {
createUser();
@@ -543,6 +632,9 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
+ proceedPage.assertCurrent();
+ Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+ proceedPage.clickProceedLink();
passwordUpdatePage.assertCurrent();
passwordUpdatePage.changePassword("new-pass", "new-pass");
@@ -579,6 +671,9 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
+ proceedPage.assertCurrent();
+ Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+ proceedPage.clickProceedLink();
passwordUpdatePage.assertCurrent();
passwordUpdatePage.changePassword("new-pass" + i, "new-pass" + i);
@@ -621,6 +716,9 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
+ proceedPage.assertCurrent();
+ Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+ proceedPage.clickProceedLink();
passwordUpdatePage.assertCurrent();
passwordUpdatePage.changePassword("new-pass" + i, "new-pass" + i);
@@ -659,6 +757,9 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
+ proceedPage.assertCurrent();
+ Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+ proceedPage.clickProceedLink();
passwordUpdatePage.assertCurrent();
driver.manage().deleteAllCookies();
@@ -666,6 +767,9 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
+ proceedPage.assertCurrent();
+ Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+ proceedPage.clickProceedLink();
passwordUpdatePage.assertCurrent();
passwordUpdatePage.changePassword("new-pass", "new-pass");
@@ -706,7 +810,7 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
errorPage.assertCurrent();
- assertEquals("An error occurred, please login again through your application.", errorPage.getError());
+ assertEquals("Action expired.", errorPage.getError());
} finally {
setTimeOffset(0);
@@ -765,6 +869,9 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
+ proceedPage.assertCurrent();
+ Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+ proceedPage.clickProceedLink();
passwordUpdatePage.assertCurrent();
passwordUpdatePage.changePassword("new-pass", "new-pass");
@@ -825,6 +932,9 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
+ proceedPage.assertCurrent();
+ Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+ proceedPage.clickProceedLink();
passwordUpdatePage.assertCurrent();
passwordUpdatePage.changePassword("new-pass", "new-pass");
@@ -896,11 +1006,17 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
+ proceedPage.assertCurrent();
+ Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Verify Email"));
+ proceedPage.clickProceedLink();
Assert.assertEquals("Your account has been updated.", infoPage.getInfo());
driver.navigate().to("about:blank");
driver.navigate().to(link); // It should be possible to use the same action token multiple times
+ proceedPage.assertCurrent();
+ Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Verify Email"));
+ proceedPage.clickProceedLink();
Assert.assertEquals("Your account has been updated.", infoPage.getInfo());
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
index 6500ad0..6332dd0 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
@@ -1,6 +1,5 @@
package org.keycloak.testsuite.broker;
-import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -14,7 +13,6 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.Assert;
-import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.ConsentPage;
import org.keycloak.testsuite.util.*;
@@ -40,6 +38,8 @@ import static org.keycloak.testsuite.broker.BrokerTestTools.*;
public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
+ protected IdentityProviderResource identityProviderResource;
+
@Before
public void beforeBrokerTest() {
log.debug("creating user for realm " + bc.providerRealmName());
@@ -61,7 +61,8 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
log.debug("adding identity provider to realm " + bc.consumerRealmName());
RealmResource realm = adminClient.realm(bc.consumerRealmName());
- realm.identityProviders().create(bc.setUpIdentityProvider(suiteContext));
+ realm.identityProviders().create(bc.setUpIdentityProvider(suiteContext)).close();
+ identityProviderResource = realm.identityProviders().get(bc.getIDPAlias());
// addClients
List<ClientRepresentation> clients = bc.createProviderClients(suiteContext);
@@ -70,7 +71,7 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getName() + " to realm " + bc.providerRealmName());
- providerRealm.clients().create(client);
+ providerRealm.clients().create(client).close();
}
}
@@ -80,7 +81,7 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getName() + " to realm " + bc.consumerRealmName());
- consumerRealm.clients().create(client);
+ consumerRealm.clients().create(client).close();
}
}
@@ -90,6 +91,12 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
@Test
public void testLogInAsUserInIDP() {
+ loginUser();
+
+ testSingleLogout();
+ }
+
+ protected void loginUser() {
driver.navigate().to(getAccountUrl(bc.consumerRealmName()));
log.debug("Clicking social " + bc.getIDPAlias());
@@ -98,16 +105,16 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
waitForPage(driver, "log in to");
Assert.assertTrue("Driver should be on the provider realm page right now",
- driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
+ driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
log.debug("Logging in");
accountLoginPage.login(bc.getUserLogin(), bc.getUserPassword());
waitForPage(driver, "update account information");
- Assert.assertTrue(updateAccountInformationPage.isCurrent());
+ updateAccountInformationPage.assertCurrent();
Assert.assertTrue("We must be on correct realm right now",
- driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
+ driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
log.debug("Updating info on updateAccount page");
updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname");
@@ -128,9 +135,7 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
}
Assert.assertTrue("There must be user " + bc.getUserLogin() + " in realm " + bc.consumerRealmName(),
- isUserFound);
-
- testSingleLogout();
+ isUserFound);
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
index e5f5b8d..da0bc2b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
@@ -6,6 +6,7 @@
package org.keycloak.testsuite.broker;
import org.keycloak.protocol.ProtocolMapperUtils;
+import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper;
@@ -22,6 +23,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import static org.keycloak.broker.saml.SAMLIdentityProviderConfig.*;
import static org.keycloak.testsuite.broker.BrokerTestConstants.*;
import static org.keycloak.testsuite.broker.BrokerTestTools.*;
@@ -63,17 +65,17 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
Map<String, String> attributes = new HashMap<>();
- attributes.put("saml.authnstatement", "true");
- attributes.put("saml_single_logout_service_url_post",
+ attributes.put(SamlConfigAttributes.SAML_AUTHNSTATEMENT, "true");
+ attributes.put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE,
getAuthRoot(suiteContext) + "/auth/realms/" + REALM_CONS_NAME + "/broker/" + IDP_SAML_ALIAS + "/endpoint");
- attributes.put("saml_assertion_consumer_url_post",
+ attributes.put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE,
getAuthRoot(suiteContext) + "/auth/realms/" + REALM_CONS_NAME + "/broker/" + IDP_SAML_ALIAS + "/endpoint");
- attributes.put("saml_force_name_id_format", "true");
- attributes.put("saml_name_id_format", "username");
- attributes.put("saml.assertion.signature", "false");
- attributes.put("saml.server.signature", "false");
- attributes.put("saml.client.signature", "false");
- attributes.put("saml.encrypt", "false");
+ attributes.put(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, "true");
+ attributes.put(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, "username");
+ attributes.put(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, "false");
+ attributes.put(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false");
+ attributes.put(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false");
+ attributes.put(SamlConfigAttributes.SAML_ENCRYPT, "false");
client.setAttributes(attributes);
@@ -133,15 +135,15 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
Map<String, String> config = idp.getConfig();
- config.put("singleSignOnServiceUrl", getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml");
- config.put("singleLogoutServiceUrl", getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml");
- config.put("nameIDPolicyFormat", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
- config.put("forceAuthn", "true");
- config.put("postBindingResponse", "true");
- config.put("postBindingAuthnRequest", "true");
- config.put("validateSignature", "false");
- config.put("wantAuthnRequestsSigned", "false");
- config.put("backchannelSupported", "true");
+ config.put(SINGLE_SIGN_ON_SERVICE_URL, getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml");
+ config.put(SINGLE_LOGOUT_SERVICE_URL, getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml");
+ config.put(NAME_ID_POLICY_FORMAT, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
+ config.put(FORCE_AUTHN, "true");
+ config.put(POST_BINDING_RESPONSE, "true");
+ config.put(POST_BINDING_AUTHN_REQUEST, "true");
+ config.put(VALIDATE_SIGNATURE, "false");
+ config.put(WANT_AUTHN_REQUESTS_SIGNED, "false");
+ config.put(BACKCHANNEL_SUPPORTED, "true");
return idp;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java
index b4825cd..ef23a9a 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java
@@ -1,19 +1,29 @@
package org.keycloak.testsuite.broker;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
+import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.arquillian.SuiteContext;
+import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
+import org.keycloak.testsuite.updaters.IdentityProviderAttributeUpdater;
+import java.io.Closeable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.Test;
import static org.keycloak.testsuite.broker.BrokerTestConstants.*;
+import static org.keycloak.testsuite.broker.BrokerTestTools.encodeUrl;
public class KcSamlSignedBrokerTest extends KcSamlBrokerTest {
- public static class KcSamlSignedBrokerConfiguration extends KcSamlBrokerConfiguration {
+ public class KcSamlSignedBrokerConfiguration extends KcSamlBrokerConfiguration {
@Override
public RealmRepresentation createProviderRealm() {
@@ -39,6 +49,9 @@ public class KcSamlSignedBrokerTest extends KcSamlBrokerTest {
public List<ClientRepresentation> createProviderClients(SuiteContext suiteContext) {
List<ClientRepresentation> clientRepresentationList = super.createProviderClients(suiteContext);
+ String consumerCert = adminClient.realm(consumerRealmName()).keys().getKeyMetadata().getKeys().get(0).getCertificate();
+ Assert.assertThat(consumerCert, Matchers.notNullValue());
+
for (ClientRepresentation client : clientRepresentationList) {
client.setClientAuthenticatorType("client-secret");
client.setSurrogateAuthRequired(false);
@@ -49,12 +62,11 @@ public class KcSamlSignedBrokerTest extends KcSamlBrokerTest {
client.setAttributes(attributes);
}
- attributes.put("saml.assertion.signature", "true");
- attributes.put("saml.server.signature", "true");
- attributes.put("saml.client.signature", "true");
- attributes.put("saml.signature.algorithm", "RSA_SHA256");
- attributes.put("saml.signing.private.key", IDP_SAML_SIGN_KEY);
- attributes.put("saml.signing.certificate", IDP_SAML_SIGN_CERT);
+ attributes.put(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, "true");
+ attributes.put(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true");
+ attributes.put(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "true");
+ attributes.put(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM, "RSA_SHA256");
+ attributes.put(SamlConfigAttributes.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, consumerCert);
}
return clientRepresentationList;
@@ -64,11 +76,15 @@ public class KcSamlSignedBrokerTest extends KcSamlBrokerTest {
public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) {
IdentityProviderRepresentation result = super.setUpIdentityProvider(suiteContext);
+ String providerCert = adminClient.realm(providerRealmName()).keys().getKeyMetadata().getKeys().get(0).getCertificate();
+ Assert.assertThat(providerCert, Matchers.notNullValue());
+
Map<String, String> config = result.getConfig();
- config.put("validateSignature", "true");
- config.put("wantAuthnRequestsSigned", "true");
- config.put("signingCertificate", IDP_SAML_SIGN_CERT);
+ config.put(SAMLIdentityProviderConfig.VALIDATE_SIGNATURE, "true");
+ config.put(SAMLIdentityProviderConfig.WANT_ASSERTIONS_SIGNED, "true");
+ config.put(SAMLIdentityProviderConfig.WANT_AUTHN_REQUESTS_SIGNED, "true");
+ config.put(SAMLIdentityProviderConfig.SIGNING_CERTIFICATE_KEY, providerCert);
return result;
}
@@ -76,7 +92,50 @@ public class KcSamlSignedBrokerTest extends KcSamlBrokerTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
- return KcSamlSignedBrokerConfiguration.INSTANCE;
+ return new KcSamlSignedBrokerConfiguration();
}
+ @Test
+ public void testSignedEncryptedAssertions() throws Exception {
+ ClientRepresentation client = adminClient.realm(bc.providerRealmName())
+ .clients()
+ .findByClientId(bc.getIDPClientIdInProviderRealm(suiteContext))
+ .get(0);
+
+ final ClientResource clientResource = realmsResouce().realm(bc.providerRealmName()).clients().get(client.getId());
+ Assert.assertThat(clientResource, Matchers.notNullValue());
+
+ String providerCert = adminClient.realm(bc.providerRealmName()).keys().getKeyMetadata().getKeys().get(0).getCertificate();
+ Assert.assertThat(providerCert, Matchers.notNullValue());
+
+ String consumerCert = adminClient.realm(bc.consumerRealmName()).keys().getKeyMetadata().getKeys().get(0).getCertificate();
+ Assert.assertThat(consumerCert, Matchers.notNullValue());
+
+ try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource)
+ .setAttribute(SAMLIdentityProviderConfig.VALIDATE_SIGNATURE, "true")
+ .setAttribute(SAMLIdentityProviderConfig.WANT_ASSERTIONS_SIGNED, "true")
+ .setAttribute(SAMLIdentityProviderConfig.WANT_ASSERTIONS_ENCRYPTED, "true")
+ .setAttribute(SAMLIdentityProviderConfig.WANT_AUTHN_REQUESTS_SIGNED, "false")
+ .setAttribute(SAMLIdentityProviderConfig.SIGNING_CERTIFICATE_KEY, providerCert)
+ .update();
+ Closeable clientUpdater = new ClientAttributeUpdater(clientResource)
+ .setAttribute(SamlConfigAttributes.SAML_ENCRYPT, "true")
+ .setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, consumerCert)
+ .setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false") // only sign assertions
+ .setAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, "true")
+ .setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false")
+ .update())
+ {
+ // Login should pass because assertion is signed.
+ loginUser();
+
+ // Logout should fail because logout response is not signed.
+ driver.navigate().to(BrokerTestTools.getAuthRoot(suiteContext)
+ + "/auth/realms/" + bc.providerRealmName()
+ + "/protocol/" + "openid-connect"
+ + "/logout?redirect_uri=" + encodeUrl(getAccountUrl(bc.providerRealmName())));
+
+ errorPage.assertCurrent();
+ }
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java
index 40d755a..e171cca 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java
@@ -455,7 +455,7 @@ public abstract class AbstractRegCliTest extends AbstractCliTest {
ClientRepresentation client3 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
Assert.assertEquals("clientId", "test-client", client3.getClientId());
- Assert.assertNotEquals("registrationAccessToken in returned json is different than one returned by create",
+ Assert.assertEquals("registrationAccessToken in returned json is different than one returned by create",
client.getRegistrationAccessToken(), client3.getRegistrationAccessToken());
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java
index d590322..86edae7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java
@@ -187,6 +187,23 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest {
}
@Test
+ public void updateClientSecret() throws ClientRegistrationException {
+ authManageClients();
+
+ registerClient();
+
+ ClientRepresentation client = reg.get(CLIENT_ID);
+ assertNotNull(client.getSecret());
+ client.setSecret("mysecret");
+
+ reg.update(client);
+
+ ClientRepresentation updatedClient = reg.get(CLIENT_ID);
+
+ assertEquals("mysecret", updatedClient.getSecret());
+ }
+
+ @Test
public void updateClientAsAdminWithCreateOnly() throws ClientRegistrationException {
authCreateClients();
try {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
index 4b4c9ba..57f71b2 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
@@ -139,7 +139,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
OIDCClientRepresentation rep = reg.oidc().get(response.getClientId());
assertNotNull(rep);
- assertNotEquals(response.getRegistrationAccessToken(), rep.getRegistrationAccessToken());
+ assertEquals(response.getRegistrationAccessToken(), rep.getRegistrationAccessToken());
assertTrue(CollectionUtil.collectionEquals(Arrays.asList("code", "none"), response.getResponseTypes()));
assertTrue(CollectionUtil.collectionEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes()));
assertNotNull(response.getClientSecret());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java
index 8666e04..0601879 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java
@@ -28,6 +28,8 @@ import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper;
import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.UserInfo;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
@@ -41,6 +43,7 @@ import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResou
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.UserInfoClientUtil;
+import org.keycloak.testsuite.util.UserManager;
import javax.ws.rs.client.Client;
import javax.ws.rs.core.Response;
@@ -49,6 +52,8 @@ import java.util.Base64;
import java.util.Collections;
import java.util.List;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrationTest {
@@ -77,6 +82,14 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati
return response;
}
+ public OIDCClientRepresentation createPairwise() throws ClientRegistrationException {
+ // Create pairwise client
+ OIDCClientRepresentation clientRep = createRep();
+ clientRep.setSubjectType("pairwise");
+ OIDCClientRepresentation pairwiseClient = reg.oidc().create(clientRep);
+ return pairwiseClient;
+ }
+
private void assertCreateFail(OIDCClientRepresentation client, int expectedStatusCode, String expectedErrorContains) {
try {
@@ -351,6 +364,109 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati
}
}
+ @Test
+ public void refreshPairwiseToken() throws Exception {
+ // Create pairwise client
+ OIDCClientRepresentation pairwiseClient = createPairwise();
+
+ // Login to pairwise client
+ OAuthClient.AccessTokenResponse accessTokenResponse = login(pairwiseClient, "test-user@localhost", "password");
+
+ // Verify tokens
+ oauth.verifyRefreshToken(accessTokenResponse.getAccessToken());
+ IDToken idToken = oauth.verifyIDToken(accessTokenResponse.getIdToken());
+ oauth.verifyRefreshToken(accessTokenResponse.getRefreshToken());
+
+ // Refresh token
+ OAuthClient.AccessTokenResponse refreshTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), pairwiseClient.getClientSecret());
+
+ // Verify refreshed tokens
+ oauth.verifyToken(refreshTokenResponse.getAccessToken());
+ RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(refreshTokenResponse.getRefreshToken());
+ IDToken refreshedIdToken = oauth.verifyIDToken(refreshTokenResponse.getIdToken());
+
+ // If an ID Token is returned as a result of a token refresh request, the following requirements apply:
+ // its iss Claim Value MUST be the same as in the ID Token issued when the original authentication occurred
+ Assert.assertEquals(idToken.getIssuer(), refreshedRefreshToken.getIssuer());
+
+ // its sub Claim Value MUST be the same as in the ID Token issued when the original authentication occurred
+ Assert.assertEquals(idToken.getSubject(), refreshedRefreshToken.getSubject());
+
+ // its iat Claim MUST represent the time that the new ID Token is issued
+ Assert.assertEquals(refreshedIdToken.getIssuedAt(), refreshedRefreshToken.getIssuedAt());
+
+ // its aud Claim Value MUST be the same as in the ID Token issued when the original authentication occurred
+ Assert.assertArrayEquals(idToken.getAudience(), refreshedRefreshToken.getAudience());
+
+ // if the ID Token contains an auth_time Claim, its value MUST represent the time of the original authentication
+ // - not the time that the new ID token is issued
+ Assert.assertEquals(idToken.getAuthTime(), refreshedIdToken.getAuthTime());
+
+ // its azp Claim Value MUST be the same as in the ID Token issued when the original authentication occurred; if
+ // no azp Claim was present in the original ID Token, one MUST NOT be present in the new ID Token
+ Assert.assertEquals(idToken.getIssuedFor(), refreshedIdToken.getIssuedFor());
+ }
+
+ @Test
+ public void refreshPairwiseTokenDeletedUser() throws Exception {
+ String userId = createUser(REALM_NAME, "delete-me@localhost", "password");
+
+ // Create pairwise client
+ OIDCClientRepresentation pairwiseClient = createPairwise();
+
+ // Login to pairwise client
+ oauth.clientId(pairwiseClient.getClientId());
+ oauth.clientId(pairwiseClient.getClientId());
+ OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin("delete-me@localhost", "password");
+ OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(loginResponse.getCode(), pairwiseClient.getClientSecret());
+
+ assertEquals(200, accessTokenResponse.getStatusCode());
+
+ // Delete user
+ adminClient.realm(REALM_NAME).users().delete(userId);
+
+ OAuthClient.AccessTokenResponse refreshTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), pairwiseClient.getClientSecret());
+ assertEquals(400, refreshTokenResponse.getStatusCode());
+ assertEquals("invalid_grant", refreshTokenResponse.getError());
+ assertNull(refreshTokenResponse.getAccessToken());
+ assertNull(refreshTokenResponse.getIdToken());
+ assertNull(refreshTokenResponse.getRefreshToken());
+ }
+
+ @Test
+ public void refreshPairwiseTokenDisabledUser() throws Exception {
+ createUser(REALM_NAME, "disable-me@localhost", "password");
+
+ // Create pairwise client
+ OIDCClientRepresentation pairwiseClient = createPairwise();
+
+ // Login to pairwise client
+ oauth.clientId(pairwiseClient.getClientId());
+ oauth.clientId(pairwiseClient.getClientId());
+ OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin("disable-me@localhost", "password");
+ OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(loginResponse.getCode(), pairwiseClient.getClientSecret());
+ assertEquals(200, accessTokenResponse.getStatusCode());
+
+ try {
+ UserManager.realm(adminClient.realm(REALM_NAME)).username("disable-me@localhost").enabled(false);
+
+ OAuthClient.AccessTokenResponse refreshTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), pairwiseClient.getClientSecret());
+ assertEquals(400, refreshTokenResponse.getStatusCode());
+ assertEquals("invalid_grant", refreshTokenResponse.getError());
+ assertNull(refreshTokenResponse.getAccessToken());
+ assertNull(refreshTokenResponse.getIdToken());
+ assertNull(refreshTokenResponse.getRefreshToken());
+ } finally {
+ UserManager.realm(adminClient.realm(REALM_NAME)).username("disable-me@localhost").enabled(true);
+ }
+ }
+
+ private OAuthClient.AccessTokenResponse login(OIDCClientRepresentation client, String username, String password) {
+ oauth.clientId(client.getClientId());
+ OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin(username, password);
+ return oauth.doAccessTokenRequest(loginResponse.getCode(), client.getClientSecret());
+ }
+
private String getPayload(String token) {
String payloadBase64 = token.split("\\.")[1];
return new String(Base64.getDecoder().decode(payloadBase64));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java
index 3eb0d7e..d7ea7f1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java
@@ -82,13 +82,16 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
@Test
public void getClientWithRegistrationToken() throws ClientRegistrationException {
+ setTimeOffset(10);
+
ClientRepresentation rep = reg.get(client.getClientId());
assertNotNull(rep);
- assertNotEquals(client.getRegistrationAccessToken(), rep.getRegistrationAccessToken());
- // check registration access token is updated
- assertRead(client.getClientId(), client.getRegistrationAccessToken(), false);
- assertRead(client.getClientId(), rep.getRegistrationAccessToken(), true);
+ assertEquals(client.getRegistrationAccessToken(), rep.getRegistrationAccessToken());
+ assertNotNull(rep.getRegistrationAccessToken());
+
+ // KEYCLOAK-4984 check registration access token is not updated
+ assertRead(client.getClientId(), client.getRegistrationAccessToken(), true);
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java
new file mode 100644
index 0000000..0986c17
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.cluster;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.jboss.arquillian.container.test.api.ContainerController;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.junit.Before;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.admin.concurrency.ConcurrentLoginTest;
+import org.keycloak.testsuite.arquillian.ContainerInfo;
+import org.keycloak.testsuite.rest.representation.JGroupsStats;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ConcurrentLoginClusterTest extends ConcurrentLoginTest {
+
+
+ @ArquillianResource
+ protected ContainerController controller;
+
+
+ // Need to postpone that
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ }
+
+
+ @Before
+ @Override
+ public void beforeTest() {
+ // Start backend nodes
+ log.info("Starting 2 backend nodes now");
+ for (ContainerInfo node : suiteContext.getAuthServerBackendsInfo()) {
+ if (!controller.isStarted(node.getQualifier())) {
+ log.info("Starting backend node: " + node);
+ controller.start(node.getQualifier());
+ Assert.assertTrue(controller.isStarted(node.getQualifier()));
+ }
+ }
+
+ // Import realms
+ log.info("Importing realms");
+ List<RealmRepresentation> testRealms = new LinkedList<>();
+ super.addTestRealms(testRealms);
+ for (RealmRepresentation testRealm : testRealms) {
+ importRealm(testRealm);
+ }
+ log.info("Realms imported");
+
+ // Finally create clients
+ createClients();
+ }
+
+
+ @Override
+ public void concurrentLoginSingleUser() throws Throwable {
+ super.concurrentLoginSingleUser();
+ JGroupsStats stats = testingClient.testing().cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getJgroupsStats();
+ log.info("JGroups statistics: " + stats.statsAsString());
+ }
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java
index 2ad3cc3..27fed71 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java
@@ -96,7 +96,7 @@ public abstract class AbstractAdminCrossDCTest extends AbstractCrossDCTest {
Matcher<? super T> matcherInstance = matcherOnOldStat.apply(oldStat);
assertThat(newStat, matcherInstance);
- }, 5, 200);
+ }, 20, 200);
}
protected void assertStatistics(InfinispanStatistics stats, Runnable testedCode, BiConsumer<Map<String, Object>, Map<String, Object>> assertionOnStats) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
index b4d4236..fd6300e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
@@ -23,6 +23,8 @@ import org.keycloak.testsuite.arquillian.ContainerInfo;
import org.keycloak.testsuite.arquillian.LoadBalancerController;
import org.keycloak.testsuite.arquillian.annotation.LoadBalancer;
import org.keycloak.testsuite.auth.page.AuthRealm;
+
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -31,13 +33,14 @@ import org.jboss.arquillian.container.test.api.ContainerController;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.junit.After;
import org.junit.Before;
+import org.keycloak.testsuite.client.KeycloakTestingClient;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
- *
+ * Abstract cross-data-centre test that defines primitives for handling cross-DC setup.
* @author hmlnarik
*/
public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest {
@@ -54,14 +57,16 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
protected Map<ContainerInfo, Keycloak> backendAdminClients = new HashMap<>();
+ protected Map<ContainerInfo, KeycloakTestingClient> backendTestingClients = new HashMap<>();
+
@After
@Before
public void enableOnlyFirstNodeInFirstDc() {
this.loadBalancerCtrl.disableAllBackendNodes();
- loadBalancerCtrl.enableBackendNodeByName(getAutomaticallyStartedBackendNodes(0)
- .findFirst()
- .orElseThrow(() -> new IllegalStateException("No node is started automatically"))
- .getQualifier()
+ loadBalancerCtrl.enableBackendNodeByName(getAutomaticallyStartedBackendNodes(DC.FIRST)
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("No node is started automatically"))
+ .getQualifier()
);
}
@@ -72,8 +77,23 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
.flatMap(List::stream)
.filter(ContainerInfo::isStarted)
.filter(ContainerInfo::isManual)
- .map(ContainerInfo::getQualifier)
- .forEach(containerController::stop);
+ .forEach(containerInfo -> {
+ containerController.stop(containerInfo.getQualifier());
+ removeRESTClientsForNode(containerInfo);
+ });
+ }
+
+ @Before
+ public void initRESTClientsForStartedNodes() {
+ log.debug("Init REST clients for automatically started nodes");
+ this.suiteContext.getDcAuthServerBackendsInfo().stream()
+ .flatMap(List::stream)
+ .filter(ContainerInfo::isStarted)
+ .filter(containerInfo -> !containerInfo.isManual())
+ .forEach(containerInfo -> {
+ createRESTClientsForNode(containerInfo);
+ });
+
}
@Override
@@ -98,7 +118,7 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
public void initLoadBalancer() {
log.debug("Initializing load balancer - only enabling started nodes in the first DC");
this.loadBalancerCtrl.disableAllBackendNodes();
- // Enable only the started nodes in each datacenter
+ // Enable only the started nodes in first datacenter
this.suiteContext.getDcAuthServerBackendsInfo().get(0).stream()
.filter(ContainerInfo::isStarted)
.map(ContainerInfo::getQualifier)
@@ -110,8 +130,22 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
return Keycloak.getInstance(node.getContextRoot() + "/auth", AuthRealm.MASTER, AuthRealm.ADMIN, AuthRealm.ADMIN, Constants.ADMIN_CLI_CLIENT_ID);
}
+ protected KeycloakTestingClient createTestingClientFor(ContainerInfo node) {
+ log.info("Initializing testing client for " + node.getContextRoot() + "/auth");
+ return KeycloakTestingClient.getInstance(node.getContextRoot() + "/auth");
+ }
+
+
+ protected Keycloak getAdminClientForStartedNodeInDc(int dcIndex) {
+ ContainerInfo firstStartedNode = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).stream()
+ .filter(ContainerInfo::isStarted)
+ .findFirst().get();
+
+ return getAdminClientFor(firstStartedNode);
+ }
+
/**
- * Creates admin client directed to the given node.
+ * Get admin client directed to the given node.
* @param node
* @return
*/
@@ -123,11 +157,58 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
return client;
}
+
+ protected KeycloakTestingClient getTestingClientForStartedNodeInDc(int dcIndex) {
+ ContainerInfo firstStartedNode = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).stream()
+ .filter(ContainerInfo::isStarted)
+ .findFirst().get();
+
+ return getTestingClientFor(firstStartedNode);
+ }
+
+
+ /**
+ * Get testing client directed to the given node.
+ * @param node
+ * @return
+ */
+ protected KeycloakTestingClient getTestingClientFor(ContainerInfo node) {
+ KeycloakTestingClient client = backendTestingClients.get(node);
+ if (client == null && node.equals(suiteContext.getAuthServerInfo())) {
+ client = this.testingClient;
+ }
+ return client;
+ }
+
+ protected void createRESTClientsForNode(ContainerInfo node) {
+ if (!backendAdminClients.containsKey(node)) {
+ backendAdminClients.put(node, createAdminClientFor(node));
+ }
+
+ if (!backendTestingClients.containsKey(node)) {
+ backendTestingClients.put(node, createTestingClientFor(node));
+ }
+ }
+
+ protected void removeRESTClientsForNode(ContainerInfo node) {
+ if (backendAdminClients.containsKey(node)) {
+ backendAdminClients.get(node).close();
+ backendAdminClients.remove(node);
+ }
+
+ if (backendTestingClients.containsKey(node)) {
+ backendTestingClients.get(node).close();
+ backendTestingClients.remove(node);
+ }
+ }
+
+
/**
* Disables routing requests to the given data center in the load balancer.
* @param dcIndex
*/
- public void disableDcOnLoadBalancer(int dcIndex) {
+ public void disableDcOnLoadBalancer(DC dc) {
+ int dcIndex = dc.ordinal();
log.infof("Disabling load balancer for dc=%d", dcIndex);
this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).forEach(c -> loadBalancerCtrl.disableBackendNodeByName(c.getQualifier()));
}
@@ -136,7 +217,8 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
* Enables routing requests to all started nodes to the given data center in the load balancer.
* @param dcIndex
*/
- public void enableDcOnLoadBalancer(int dcIndex) {
+ public void enableDcOnLoadBalancer(DC dc) {
+ int dcIndex = dc.ordinal();
log.infof("Enabling load balancer for dc=%d", dcIndex);
final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
if (! dcNodes.stream().anyMatch(ContainerInfo::isStarted)) {
@@ -153,7 +235,8 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
* @param dcIndex
* @param nodeIndex
*/
- public void disableLoadBalancerNode(int dcIndex, int nodeIndex) {
+ public void disableLoadBalancerNode(DC dc, int nodeIndex) {
+ int dcIndex = dc.ordinal();
log.infof("Disabling load balancer for dc=%d, node=%d", dcIndex, nodeIndex);
loadBalancerCtrl.disableBackendNodeByName(this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).get(nodeIndex).getQualifier());
}
@@ -163,7 +246,8 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
* @param dcIndex
* @param nodeIndex
*/
- public void enableLoadBalancerNode(int dcIndex, int nodeIndex) {
+ public void enableLoadBalancerNode(DC dc, int nodeIndex) {
+ int dcIndex = dc.ordinal();
log.infof("Enabling load balancer for dc=%d, node=%d", dcIndex, nodeIndex);
final ContainerInfo backendNode = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).get(nodeIndex);
if (backendNode == null) {
@@ -181,13 +265,17 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
* @param nodeIndex
* @return Started instance descriptor.
*/
- public ContainerInfo startBackendNode(int dcIndex, int nodeIndex) {
+ public ContainerInfo startBackendNode(DC dc, int nodeIndex) {
+ int dcIndex = dc.ordinal();
assertThat((Integer) dcIndex, lessThan(this.suiteContext.getDcAuthServerBackendsInfo().size()));
final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
assertThat((Integer) nodeIndex, lessThan(dcNodes.size()));
ContainerInfo dcNode = dcNodes.get(nodeIndex);
assertTrue("Node " + dcNode.getQualifier() + " has to be controlled manually", dcNode.isManual());
containerController.start(dcNode.getQualifier());
+
+ createRESTClientsForNode(dcNode);
+
return dcNode;
}
@@ -197,11 +285,15 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
* @param nodeIndex
* @return Stopped instance descriptor.
*/
- public ContainerInfo stopBackendNode(int dcIndex, int nodeIndex) {
+ public ContainerInfo stopBackendNode(DC dc, int nodeIndex) {
+ int dcIndex = dc.ordinal();
assertThat((Integer) dcIndex, lessThan(this.suiteContext.getDcAuthServerBackendsInfo().size()));
final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
assertThat((Integer) nodeIndex, lessThan(dcNodes.size()));
ContainerInfo dcNode = dcNodes.get(nodeIndex);
+
+ removeRESTClientsForNode(dcNode);
+
assertTrue("Node " + dcNode.getQualifier() + " has to be controlled manually", dcNode.isManual());
containerController.stop(dcNode.getQualifier());
return dcNode;
@@ -212,7 +304,8 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
* @param dcIndex
* @return
*/
- public Stream<ContainerInfo> getManuallyStartedBackendNodes(int dcIndex) {
+ public Stream<ContainerInfo> getManuallyStartedBackendNodes(DC dc) {
+ int dcIndex = dc.ordinal();
final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
return dcNodes.stream().filter(ContainerInfo::isManual);
}
@@ -222,8 +315,39 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
* @param dcIndex
* @return
*/
- public Stream<ContainerInfo> getAutomaticallyStartedBackendNodes(int dcIndex) {
+ public Stream<ContainerInfo> getAutomaticallyStartedBackendNodes(DC dc) {
+ int dcIndex = dc.ordinal();
final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
return dcNodes.stream().filter(c -> ! c.isManual());
}
+
+
+ /**
+ * Sets time offset on all the started containers.
+ *
+ * @param offset
+ */
+ @Override
+ public void setTimeOffset(int offset) {
+ super.setTimeOffset(offset);
+ setTimeOffsetOnAllStartedContainers(offset);
+ }
+
+ private void setTimeOffsetOnAllStartedContainers(int offset) {
+ backendTestingClients.entrySet().stream()
+ .filter(testingClientEntry -> testingClientEntry.getKey().isStarted())
+ .forEach(testingClientEntry -> {
+ KeycloakTestingClient testingClient = testingClientEntry.getValue();
+ testingClient.testing().setTimeOffset(Collections.singletonMap("offset", String.valueOf(offset)));
+ });
+ }
+
+ /**
+ * Resets time offset on all the started containers.
+ */
+ @Override
+ public void resetTimeOffset() {
+ super.resetTimeOffset();
+ setTimeOffsetOnAllStartedContainers(0);
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java
index 972be31..1a4e079 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java
@@ -41,6 +41,7 @@ import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanCacheStatistics
import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanChannelStatistics;
import org.keycloak.testsuite.arquillian.InfinispanStatistics;
import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants;
+import org.keycloak.testsuite.pages.ProceedPage;
import java.util.concurrent.TimeUnit;
import org.hamcrest.Matchers;
import static org.hamcrest.Matchers.greaterThan;
@@ -59,6 +60,9 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
protected LoginPasswordUpdatePage passwordUpdatePage;
@Page
+ protected ProceedPage proceedPage;
+
+ @Page
protected ErrorPage errorPage;
private String createUser(UserRepresentation userRep) {
@@ -73,11 +77,11 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
@Test
public void sendResetPasswordEmailSuccessWorksInCrossDc(
- @JmxInfinispanCacheStatistics(dcIndex=0, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node0Statistics,
- @JmxInfinispanCacheStatistics(dcIndex=0, dcNodeIndex=1, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node1Statistics,
- @JmxInfinispanCacheStatistics(dcIndex=1, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc1Node0Statistics,
+ @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node0Statistics,
+ @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=1, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node1Statistics,
+ @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc1Node0Statistics,
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
- startBackendNode(0, 1);
+ startBackendNode(DC.FIRST, 1);
cacheDc0Node1Statistics.waitToBecomeAvailable(10, TimeUnit.SECONDS);
Comparable originalNumberOfEntries = cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
@@ -107,6 +111,8 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
Matchers::is
);
+ proceedPage.assertCurrent();
+ proceedPage.clickProceedLink();
passwordUpdatePage.assertCurrent();
// Verify that there was at least one message sent via the channel
@@ -120,8 +126,8 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
// Verify that there was an action token added in the node which was targetted by the link
assertThat(cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES), greaterThan(originalNumberOfEntries));
- disableDcOnLoadBalancer(0);
- enableDcOnLoadBalancer(1);
+ disableDcOnLoadBalancer(DC.FIRST);
+ enableDcOnLoadBalancer(DC.SECOND);
// Make sure that after going to the link, the invalidated action token has been retrieved from Infinispan server cluster in the other DC
assertSingleStatistics(cacheDc1Node0Statistics, Constants.STAT_CACHE_NUMBER_OF_ENTRIES,
@@ -134,7 +140,7 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
@Test
public void sendResetPasswordEmailAfterNewNodeAdded() throws IOException, MessagingException {
- disableDcOnLoadBalancer(1);
+ disableDcOnLoadBalancer(DC.SECOND);
UserRepresentation userRep = new UserRepresentation();
userRep.setEnabled(true);
@@ -156,14 +162,16 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
driver.navigate().to(link);
+ proceedPage.assertCurrent();
+ proceedPage.clickProceedLink();
passwordUpdatePage.assertCurrent();
passwordUpdatePage.changePassword("new-pass", "new-pass");
assertEquals("Your account has been updated.", driver.getTitle());
- disableDcOnLoadBalancer(0);
- getManuallyStartedBackendNodes(1)
+ disableDcOnLoadBalancer(DC.FIRST);
+ getManuallyStartedBackendNodes(DC.SECOND)
.findFirst()
.ifPresent(c -> {
containerController.start(c.getQualifier());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java
new file mode 100644
index 0000000..b710943
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.crossdc;
+
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.RealmResource;
+import java.util.List;
+
+import org.jboss.arquillian.container.test.api.ContainerController;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.keycloak.testsuite.admin.concurrency.ConcurrentLoginTest;
+import org.keycloak.testsuite.arquillian.ContainerInfo;
+import org.keycloak.testsuite.arquillian.LoadBalancerController;
+import org.keycloak.testsuite.arquillian.annotation.LoadBalancer;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.LaxRedirectStrategy;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ConcurrentLoginCrossDCTest extends ConcurrentLoginTest {
+
+ @ArquillianResource
+ @LoadBalancer(value = AbstractCrossDCTest.QUALIFIER_NODE_BALANCER)
+ protected LoadBalancerController loadBalancerCtrl;
+
+ @ArquillianResource
+ protected ContainerController containerController;
+
+ private static final int INVOCATIONS_BEFORE_SIMULATING_DC_FAILURE = 10;
+ private static final int LOGIN_TASK_DELAY_MS = 100;
+ private static final int LOGIN_TASK_RETRIES = 15;
+
+ @Override
+ public void beforeAbstractKeycloakTestRealmImport() {
+ log.debug("Initializing load balancer - enabling all started nodes across DCs");
+ this.loadBalancerCtrl.disableAllBackendNodes();
+
+ this.suiteContext.getDcAuthServerBackendsInfo().stream()
+ .flatMap(List::stream)
+ .filter(ContainerInfo::isStarted)
+ .map(ContainerInfo::getQualifier)
+ .forEach(loadBalancerCtrl::enableBackendNodeByName);
+ }
+
+ @Test
+ public void concurrentLoginWithRandomDcFailures() throws Throwable {
+ log.info("*********************************************");
+ long start = System.currentTimeMillis();
+
+ AtomicReference<String> userSessionId = new AtomicReference<>();
+ LoginTask loginTask = null;
+
+ try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) {
+ loginTask = new LoginTask(httpClient, userSessionId, LOGIN_TASK_DELAY_MS, LOGIN_TASK_RETRIES, Arrays.asList(
+ createHttpClientContextForUser(httpClient, "test-user@localhost", "password")
+ ));
+ HttpUriRequest request = handleLogin(getPageContent(oauth.getLoginFormUrl(), httpClient, HttpClientContext.create()), "test-user@localhost", "password");
+ log.debug("Executing login request");
+ org.junit.Assert.assertTrue(parseAndCloseResponse(httpClient.execute(request)).contains("<title>AUTH_RESPONSE</title>"));
+
+ run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, loginTask, new SwapDcAvailability());
+ int clientSessionsCount = testingClient.testing().getClientSessionsCountInUserSession("test", userSessionId.get());
+ org.junit.Assert.assertEquals(1 + DEFAULT_CLIENTS_COUNT, clientSessionsCount);
+ } finally {
+ long end = System.currentTimeMillis() - start;
+ log.infof("Statistics: %s", loginTask == null ? "??" : loginTask.getHistogram());
+ log.info("concurrentLoginWithRandomDcFailures took " + (end/1000) + "s");
+ log.info("*********************************************");
+ }
+ }
+
+ private class SwapDcAvailability implements KeycloakRunnable {
+
+ private final AtomicInteger invocationCounter = new AtomicInteger();
+
+ @Override
+ public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+ final int currentInvocarion = invocationCounter.getAndIncrement();
+ if (currentInvocarion % INVOCATIONS_BEFORE_SIMULATING_DC_FAILURE == 0) {
+ int failureIndex = currentInvocarion / INVOCATIONS_BEFORE_SIMULATING_DC_FAILURE;
+ int dcToEnable = failureIndex % 2;
+ int dcToDisable = (failureIndex + 1) % 2;
+ suiteContext.getDcAuthServerBackendsInfo().get(dcToDisable).forEach(c -> loadBalancerCtrl.disableBackendNodeByName(c.getQualifier()));
+ suiteContext.getDcAuthServerBackendsInfo().get(dcToEnable).forEach(c -> loadBalancerCtrl.enableBackendNodeByName(c.getQualifier()));
+ }
+ }
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java
new file mode 100644
index 0000000..bf42536
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.crossdc;
+
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Test;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.Retry;
+import org.keycloak.testsuite.arquillian.ContainerInfo;
+import org.keycloak.testsuite.client.KeycloakTestingClient;
+import org.keycloak.testsuite.rest.representation.RemoteCacheStats;
+import org.keycloak.testsuite.util.OAuthClient;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
+
+ @Test
+ public void testRevokeRefreshToken() {
+ // Enable revokeRefreshToken
+ RealmRepresentation realmRep = testRealm().toRepresentation();
+ realmRep.setRevokeRefreshToken(true);
+ testRealm().update(realmRep);
+
+ // Enable second DC
+ enableDcOnLoadBalancer(DC.SECOND);
+
+ // Login
+ OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
+ String code = response1.getCode();
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ Assert.assertNotNull(tokenResponse.getAccessToken());
+ String sessionId = oauth.verifyToken(tokenResponse.getAccessToken()).getSessionState();
+ String refreshToken1 = tokenResponse.getRefreshToken();
+
+
+ // Get statistics
+ int lsr00 = getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId);
+ int lsr10 = getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId);
+ int lsrr0 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId);
+ log.infof("lsr00: %d, lsr10: %d, lsrr0: %d", lsr00, lsr10, lsrr0);
+
+ Assert.assertEquals(lsr00, lsr10);
+ Assert.assertEquals(lsr00, lsrr0);
+
+
+ // Set time offset to some point in future. TODO This won't be needed once we have single-use cache based solution for refresh tokens
+ setTimeOffset(10);
+
+ // refresh token on DC0
+ disableDcOnLoadBalancer(DC.SECOND);
+ tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
+ String refreshToken2 = tokenResponse.getRefreshToken();
+
+ // Assert times changed on DC0, DC1 and remoteCache
+ Retry.execute(() -> {
+ int lsr01 = getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId);
+ int lsr11 = getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId);
+ int lsrr1 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId);
+ log.infof("lsr01: %d, lsr11: %d, lsrr1: %d", lsr01, lsr11, lsrr1);
+
+ Assert.assertEquals(lsr01, lsr11);
+ Assert.assertEquals(lsr01, lsrr1);
+ Assert.assertTrue(lsr01 > lsr00);
+ }, 50, 50);
+
+ // try refresh with old token on DC1. It should fail.
+ disableDcOnLoadBalancer(DC.FIRST);
+ enableDcOnLoadBalancer(DC.SECOND);
+ tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
+ Assert.assertNull(tokenResponse.getAccessToken());
+ Assert.assertNotNull(tokenResponse.getError());
+
+ // try refresh with new token on DC1. It should pass.
+ tokenResponse = oauth.doRefreshTokenRequest(refreshToken2, "password");
+ Assert.assertNotNull(tokenResponse.getAccessToken());
+ Assert.assertNull(tokenResponse.getError());
+
+ // Revert
+ realmRep = testRealm().toRepresentation();
+ realmRep.setRevokeRefreshToken(false);
+ testRealm().update(realmRep);
+ }
+
+
+ @Test
+ public void testLastSessionRefreshUpdate() {
+ // Disable DC1 on loadbalancer
+ disableDcOnLoadBalancer(DC.SECOND);
+
+ // Get statistics
+ int stores0 = getRemoteCacheStats(0).getGlobalStores();
+
+ // Login
+ OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
+ String code = response1.getCode();
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ Assert.assertNotNull(tokenResponse.getAccessToken());
+ String sessionId = oauth.verifyToken(tokenResponse.getAccessToken()).getSessionState();
+ String refreshToken1 = tokenResponse.getRefreshToken();
+
+
+ // Get statistics
+ this.suiteContext.getDcAuthServerBackendsInfo().get(0).stream()
+ .filter(ContainerInfo::isStarted).findFirst().get();
+
+ AtomicInteger stores1 = new AtomicInteger(-1);
+ Retry.execute(() -> {
+ stores1.set(getRemoteCacheStats(0).getGlobalStores());
+ log.infof("stores0=%d, stores1=%d", stores0, stores1.get());
+ Assert.assertTrue(stores1.get() > stores0);
+ }, 50, 50);
+
+ int lsr00 = getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId);
+ int lsr10 = getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId);
+ Assert.assertEquals(lsr00, lsr10);
+
+ // Set time offset to some point in future.
+ setTimeOffset(10);
+
+ // refresh token on DC0
+ tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
+ String refreshToken2 = tokenResponse.getRefreshToken();
+
+ // assert that hotrod statistics were NOT updated
+ AtomicInteger stores2 = new AtomicInteger(-1);
+
+ // TODO: not sure why stores2 < stores1 at first run. Probably should be replaced with JMX statistics
+ Retry.execute(() -> {
+ stores2.set(getRemoteCacheStats(0).getGlobalStores());
+ log.infof("stores1=%d, stores2=%d", stores1.get(), stores2.get());
+ Assert.assertEquals(stores1.get(), stores2.get());
+ }, 50, 50);
+
+ // assert that lastSessionRefresh on DC0 updated, but on DC1 still the same
+ AtomicInteger lsr01 = new AtomicInteger(-1);
+ AtomicInteger lsr11 = new AtomicInteger(-1);
+ Retry.execute(() -> {
+ lsr01.set(getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId));
+ lsr11.set(getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId));
+ log.infof("lsr01: %d, lsr11: %d", lsr01.get(), lsr11.get());
+ Assert.assertTrue(lsr01.get() > lsr00);
+ }, 50, 100);
+ Assert.assertEquals(lsr10, lsr11.get());
+
+ // assert that lastSessionRefresh still the same on remoteCache
+ int lsrr1 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId);
+ Assert.assertEquals(lsr00, lsrr1);
+ log.infof("lsrr1: %d", lsrr1);
+
+ // setTimeOffset to greater value
+ setTimeOffset(100);
+
+ // refresh token
+ tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
+
+ // assert that lastSessionRefresh on both DC0 and DC1 was updated, but on remoteCache still the same
+ AtomicInteger lsr02 = new AtomicInteger(-1);
+ AtomicInteger lsr12 = new AtomicInteger(-1);
+ AtomicInteger lsrr2 = new AtomicInteger(-1);
+ AtomicInteger stores3 = new AtomicInteger(-1);
+ Retry.execute(() -> {
+ lsr02.set(getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId));
+ lsr12.set(getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId));
+ log.infof("lsr02: %d, lsr12: %d", lsr02.get(), lsr12.get());
+ Assert.assertEquals(lsr02.get(), lsr12.get());
+ Assert.assertTrue(lsr02.get() > lsr01.get());
+ Assert.assertTrue(lsr12.get() > lsr11.get());
+
+ lsrr2.set(getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId));
+ log.infof("lsrr2: %d", lsrr2.get());
+ Assert.assertEquals(lsrr1, lsrr2.get());
+
+ // assert that hotrod statistics were NOT updated on DC0
+ stores3.set(getRemoteCacheStats(0).getGlobalStores());
+ log.infof("stores2=%d, stores3=%d", stores2.get(), stores3.get());
+ Assert.assertEquals(stores2.get(), stores3.get());
+ }, 50, 100);
+
+ // Increase time offset even more
+ setTimeOffset(1500);
+
+ // refresh token
+ tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
+ Assert.assertNull("Error: " + tokenResponse.getError() + ", error description: " + tokenResponse.getErrorDescription(), tokenResponse.getError());
+ Assert.assertNotNull(tokenResponse.getRefreshToken());
+
+ // assert that lastSessionRefresh updated everywhere including remoteCache
+ AtomicInteger lsr03 = new AtomicInteger(-1);
+ AtomicInteger lsr13 = new AtomicInteger(-1);
+ AtomicInteger lsrr3 = new AtomicInteger(-1);
+ AtomicInteger stores4 = new AtomicInteger(-1);
+ Retry.execute(() -> {
+ lsr03.set(getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId));
+ lsr13.set(getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId));
+ log.infof("lsr03: %d, lsr13: %d", lsr03.get(), lsr13.get());
+ Assert.assertEquals(lsr03.get(), lsr13.get());
+ Assert.assertTrue(lsr03.get() > lsr02.get());
+ Assert.assertTrue(lsr13.get() > lsr12.get());
+
+ lsrr3.set(getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId));
+ log.infof("lsrr3: %d", lsrr3.get());
+ Assert.assertTrue(lsrr3.get() > lsrr2.get());
+
+ // assert that hotrod statistics were NOT updated on DC0
+ stores4.set(getRemoteCacheStats(0).getGlobalStores());
+ log.infof("stores3=%d, stores4=%d", stores3.get(), stores4.get());
+ Assert.assertTrue(stores4.get() > stores3.get());
+ }, 50, 100);
+ }
+
+
+ private RemoteCacheStats getRemoteCacheStats(int dcIndex) {
+ return getTestingClientForStartedNodeInDc(dcIndex).testing("test")
+ .cache(InfinispanConnectionProvider.SESSION_CACHE_NAME)
+ .getRemoteCacheStats();
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LoginCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LoginCrossDCTest.java
new file mode 100644
index 0000000..c0eb849
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LoginCrossDCTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.crossdc;
+
+import javax.ws.rs.core.Response;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.junit.Test;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.Retry;
+import org.keycloak.testsuite.util.Matchers;
+import org.keycloak.testsuite.util.OAuthClient;
+
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LoginCrossDCTest extends AbstractAdminCrossDCTest {
+
+ @Test
+ public void loginTest() throws Exception {
+ enableDcOnLoadBalancer(DC.SECOND);
+
+ //log.info("Started to sleep");
+ //Thread.sleep(10000000);
+ for (int i=0 ; i<30 ; i++) {
+ OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
+ String code = response1.getCode();
+ OAuthClient.AccessTokenResponse response2 = oauth.doAccessTokenRequest(code, "password");
+ Assert.assertNotNull(response2.getAccessToken());
+
+ try (CloseableHttpResponse response3 = oauth.doLogout(response2.getRefreshToken(), "password")) {
+ assertThat(response3, Matchers.statusCodeIsHC(Response.Status.NO_CONTENT));
+ //assertNotNull(testingClient.testApp().getAdminLogoutAction());
+ }
+
+ log.infof("Iteration %d finished", i);
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java
new file mode 100644
index 0000000..96a59d8
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.crossdc;
+
+
+import javax.ws.rs.NotFoundException;
+
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.Constants;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.Retry;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics;
+import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanCacheStatistics;
+import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanChannelStatistics;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+
+/**
+ * Tests the bulk removal of user sessions and expiration scenarios (eg. removing realm, removing user etc)
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
+
+ private static final String REALM_NAME = "expiration-test";
+
+ private static final int SESSIONS_COUNT = 20;
+
+ private int sessions01;
+ private int sessions02;
+ private int remoteSessions01;
+ private int remoteSessions02;
+
+ private int authSessions01;
+ private int authSessions02;
+
+
+ @Before
+ public void beforeTest() {
+ try {
+ adminClient.realm(REALM_NAME).remove();
+ } catch (NotFoundException ignore) {
+ }
+
+ UserRepresentation user = UserBuilder.create()
+ .id("login-test")
+ .username("login-test")
+ .email("login@test.com")
+ .enabled(true)
+ .password("password")
+ .addRoles(Constants.OFFLINE_ACCESS_ROLE)
+ .build();
+
+ ClientRepresentation client = ClientBuilder.create()
+ .clientId("test-app")
+ .directAccessGrants()
+ .redirectUris("http://localhost:8180/auth/realms/master/app/*")
+ .addWebOrigin("http://localhost:8180")
+ .secret("password")
+ .build();
+
+ RealmRepresentation realmRep = RealmBuilder.create()
+ .name(REALM_NAME)
+ .user(user)
+ .client(client)
+ .build();
+
+ adminClient.realms().create(realmRep);
+ }
+
+
+ @Test
+ public void testRealmRemoveSessions(
+ @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+ @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+ @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
+ createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics);
+
+// log.infof("Sleeping!");
+// Thread.sleep(10000000);
+
+ channelStatisticsCrossDc.reset();
+
+ // Remove test realm
+ getAdminClient().realm(REALM_NAME).remove();
+
+ // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big.
+ assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+ sessions01, sessions02, remoteSessions01, remoteSessions02, 40l);
+ }
+
+
+ // Return last used accessTokenResponse
+ private OAuthClient.AccessTokenResponse createInitialSessions(String cacheName, boolean offline, InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics) throws Exception {
+
+ // Enable second DC
+ enableDcOnLoadBalancer(DC.SECOND);
+
+ // Check sessions count before test
+ sessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(cacheName).size();
+ sessions02 = getTestingClientForStartedNodeInDc(1).testing().cache(cacheName).size();
+ remoteSessions01 = (Integer) cacheDc1Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
+ remoteSessions02 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
+ log.infof("Before creating sessions: sessions01: %d, sessions02: %d, remoteSessions01: %d, remoteSessions02: %d", sessions01, sessions02, remoteSessions01, remoteSessions02);
+
+ // Create 20 user sessions
+ oauth.realm(REALM_NAME);
+
+ if (offline) {
+ oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+ }
+
+ OAuthClient.AccessTokenResponse lastAccessTokenResponse = null;
+ for (int i=0 ; i<SESSIONS_COUNT ; i++) {
+ lastAccessTokenResponse = oauth.doGrantAccessTokenRequest("password", "login-test", "password");
+ }
+
+ // Assert 20 sessions exists on node1 and node2 and on remote caches
+ Retry.execute(() -> {
+ int sessions11 = getTestingClientForStartedNodeInDc(0).testing().cache(cacheName).size();
+ int sessions12 = getTestingClientForStartedNodeInDc(1).testing().cache(cacheName).size();
+ int remoteSessions11 = (Integer) cacheDc1Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
+ int remoteSessions12 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
+ log.infof("After creating sessions: sessions11: %d, sessions12: %d, remoteSessions11: %d, remoteSessions12: %d", sessions11, sessions12, remoteSessions11, remoteSessions12);
+
+ Assert.assertEquals(sessions11, sessions01 + SESSIONS_COUNT);
+ Assert.assertEquals(sessions12, sessions02 + SESSIONS_COUNT);
+ Assert.assertEquals(remoteSessions11, remoteSessions01 + SESSIONS_COUNT);
+ Assert.assertEquals(remoteSessions12, remoteSessions02 + SESSIONS_COUNT);
+ }, 50, 50);
+
+ return lastAccessTokenResponse;
+ }
+
+
+ private void assertStatisticsExpected(String messagePrefix, String cacheName, InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics, InfinispanStatistics channelStatisticsCrossDc,
+ int sessions1Expected, int sessions2Expected, int remoteSessions1Expected, int remoteSessions2Expected, long sentMessagesHigherBound) {
+ Retry.execute(() -> {
+ int sessions1 = getTestingClientForStartedNodeInDc(0).testing().cache(cacheName).size();
+ int sessions2 = getTestingClientForStartedNodeInDc(1).testing().cache(cacheName).size();
+ int remoteSessions1 = (Integer) cacheDc1Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
+ int remoteSessions2 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
+ long messagesCount = (Long) channelStatisticsCrossDc.getSingleStatistics(InfinispanStatistics.Constants.STAT_CHANNEL_SENT_MESSAGES);
+ log.infof(messagePrefix + ": sessions1: %d, sessions2: %d, remoteSessions1: %d, remoteSessions2: %d, sentMessages: %d", sessions1, sessions2, remoteSessions1, remoteSessions2, messagesCount);
+
+ Assert.assertEquals(sessions1, sessions1Expected);
+ Assert.assertEquals(sessions2, sessions2Expected);
+ Assert.assertEquals(remoteSessions1, remoteSessions1Expected);
+ Assert.assertEquals(remoteSessions2, remoteSessions2Expected);
+
+ // Workaround...
+ if (sentMessagesHigherBound > 5) {
+ Assert.assertThat(messagesCount, Matchers.greaterThan(0l));
+ }
+
+ Assert.assertThat(messagesCount, Matchers.lessThan(sentMessagesHigherBound));
+ }, 50, 50);
+ }
+
+
+ @Test
+ public void testRealmRemoveOfflineSessions(
+ @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+ @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+ @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
+
+ createInitialSessions(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics);
+
+ channelStatisticsCrossDc.reset();
+
+ // Remove test realm
+ getAdminClient().realm(REALM_NAME).remove();
+
+ // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big.
+ assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+ sessions01, sessions02, remoteSessions01, remoteSessions02, 70l); // Might be bigger messages as online sessions removed too.
+ }
+
+
+ @Test
+ public void testLogoutAllInRealm(
+ @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+ @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+ @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
+
+ createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics);
+
+ channelStatisticsCrossDc.reset();
+
+ // Logout all in realm
+ getAdminClient().realm(REALM_NAME).logoutAll();
+
+ // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big.
+ assertStatisticsExpected("After realm logout", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+ sessions01, sessions02, remoteSessions01, remoteSessions02, 40l);
+ }
+
+
+ @Test
+ public void testPeriodicExpiration(
+ @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+ @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+ @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
+
+ OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics);
+
+ // Assert I am able to refresh
+ OAuthClient.AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
+ Assert.assertNotNull(refreshResponse.getRefreshToken());
+ Assert.assertNull(refreshResponse.getError());
+
+ channelStatisticsCrossDc.reset();
+
+ // Remove expired in DC0
+ getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
+
+ // Nothing yet expired. Limit 5 for sent_messages is just if "lastSessionRefresh" periodic thread happened
+ assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+ sessions01 + SESSIONS_COUNT, sessions02 + SESSIONS_COUNT, remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, 5l);
+
+
+ // Set time offset
+ setTimeOffset(10000000);
+
+ // Assert I am not able to refresh anymore
+ refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
+ Assert.assertNull(refreshResponse.getRefreshToken());
+ Assert.assertNotNull(refreshResponse.getError());
+
+
+ channelStatisticsCrossDc.reset();
+
+ // Remove expired in DC0
+ getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
+
+ // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big.
+ assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+ sessions01, sessions02, remoteSessions01, remoteSessions02, 40l);
+ }
+
+
+
+
+ // AUTH SESSIONS
+
+ @Test
+ public void testPeriodicExpirationAuthSessions(
+ @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+ @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+ @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
+ createInitialAuthSessions();
+
+ channelStatisticsCrossDc.reset();
+
+ // Remove expired in DC0 and DC1
+ getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
+ getTestingClientForStartedNodeInDc(1).testing().removeExpired(REALM_NAME);
+
+ // Nothing yet expired. Limit 5 for sent_messages is just if "lastSessionRefresh" periodic thread happened
+ assertAuthSessionsStatisticsExpected("After remove expired auth sessions - 1", channelStatisticsCrossDc,
+ SESSIONS_COUNT, 5l);
+
+ // Set time offset
+ setTimeOffset(10000000);
+
+ channelStatisticsCrossDc.reset();
+
+ // Remove expired in DC0 and DC1. Need to trigger it on both!
+ getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
+ getTestingClientForStartedNodeInDc(1).testing().removeExpired(REALM_NAME);
+
+ // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big.
+ assertAuthSessionsStatisticsExpected("After remove expired auth sessions - 2", channelStatisticsCrossDc,
+ 0, 5l);
+
+ }
+
+
+ // Return last used accessTokenResponse
+ private void createInitialAuthSessions() throws Exception {
+
+ // Enable second DC
+ enableDcOnLoadBalancer(DC.SECOND);
+
+ // Check sessions count before test
+ authSessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME).size();
+ authSessions02 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME).size();
+ log.infof("Before creating authentication sessions: authSessions01: %d, authSessions02: %d", authSessions01, authSessions02);
+
+ // Create 20 authentication sessions
+ oauth.realm(REALM_NAME);
+
+ for (int i=0 ; i<SESSIONS_COUNT ; i++) {
+ oauth.openLoginForm();
+ driver.manage().deleteAllCookies();
+ }
+
+ // Assert 20 authentication sessions exists on node1 and node2 and on remote caches
+ Retry.execute(() -> {
+ int authSessions11 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME).size();
+ int authSessions12 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME).size();
+ log.infof("After creating authentication sessions: sessions11: %d, authSessions12: %d", authSessions11, authSessions12);
+
+ // There are 20 new authentication sessions created totally in both datacenters
+ int diff1 = authSessions11 - authSessions01;
+ int diff2 = authSessions12 - authSessions02;
+ Assert.assertEquals(SESSIONS_COUNT, diff1 + diff2);
+ }, 50, 50);
+ }
+
+
+ private void assertAuthSessionsStatisticsExpected(String messagePrefix, InfinispanStatistics channelStatisticsCrossDc,
+ int expectedAuthSessionsCountDiff, long sentMessagesHigherBound) {
+ Retry.execute(() -> {
+ int authSessions1 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME).size();
+ int authSessions2 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME).size();
+ long messagesCount = (Long) channelStatisticsCrossDc.getSingleStatistics(InfinispanStatistics.Constants.STAT_CHANNEL_SENT_MESSAGES);
+ log.infof(messagePrefix + ": authSessions1: %d, authSessions2: %d, sentMessages: %d", authSessions1, authSessions2, messagesCount);
+
+ int diff1 = authSessions1 - authSessions01;
+ int diff2 = authSessions2 - authSessions02;
+
+ Assert.assertEquals(expectedAuthSessionsCountDiff, diff1 + diff2);
+
+ // Workaround...
+ if (sentMessagesHigherBound > 5) {
+ Assert.assertThat(messagesCount, Matchers.greaterThan(0l));
+ }
+
+ Assert.assertThat(messagesCount, Matchers.lessThan(sentMessagesHigherBound));
+ }, 50, 50);
+ }
+
+
+ @Test
+ public void testRealmRemoveAuthSessions(
+ @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
+
+ createInitialAuthSessions();
+
+ channelStatisticsCrossDc.reset();
+
+ // Remove test realm
+ getAdminClient().realm(REALM_NAME).remove();
+
+ // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big, however there are some messages due to removed realm
+ assertAuthSessionsStatisticsExpected("After realm removed", channelStatisticsCrossDc,
+ 0, 40l);
+ }
+
+
+ @Test
+ public void testClientRemoveAuthSessions(
+ @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
+
+ createInitialAuthSessions();
+
+ channelStatisticsCrossDc.reset();
+
+ // Remove test-app client
+ ApiUtil.findClientByClientId(getAdminClient().realm(REALM_NAME), "test-app").remove();
+
+ // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big, however there are some messages due to removed client
+ assertAuthSessionsStatisticsExpected("After client removed", channelStatisticsCrossDc,
+ 0, 5l);
+ }
+
+
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerClientTest.java
new file mode 100644
index 0000000..f947d9e
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerClientTest.java
@@ -0,0 +1,200 @@
+package org.keycloak.testsuite.docker;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.keycloak.common.Profile;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.ProfileAssume;
+import org.keycloak.testsuite.util.WaitUtils;
+import org.rnorth.ducttape.ratelimits.RateLimiterBuilder;
+import org.rnorth.ducttape.unreliables.Unreliables;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+import org.testcontainers.images.builder.ImageFromDockerfile;
+import org.testcontainers.shaded.com.github.dockerjava.api.model.ContainerNetwork;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assume.assumeTrue;
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+
+public class DockerClientTest extends AbstractKeycloakTest {
+ public static final Logger LOGGER = LoggerFactory.getLogger(DockerClientTest.class);
+
+ public static final String REALM_ID = "docker-test-realm";
+ public static final String AUTH_FLOW = "docker-basic-auth-flow";
+ public static final String CLIENT_ID = "docker-test-client";
+ public static final String DOCKER_USER = "docker-user";
+ public static final String DOCKER_USER_PASSWORD = "password";
+
+ public static final String REGISTRY_HOSTNAME = "registry.localdomain";
+ public static final Integer REGISTRY_PORT = 5000;
+ public static final String MINIMUM_DOCKER_VERSION = "1.8.0";
+ public static final String IMAGE_NAME = "busybox";
+
+ private GenericContainer dockerRegistryContainer = null;
+ private GenericContainer dockerClientContainer = null;
+
+ private static String hostIp;
+
+ @BeforeClass
+ public static void verifyEnvironment() {
+ ProfileAssume.assumeFeatureEnabled(Profile.Feature.DOCKER);
+
+ final Optional<DockerVersion> dockerVersion = new DockerHostVersionSupplier().get();
+ assumeTrue("Could not determine docker version for host machine. It either is not present or accessible to the JVM running the test harness.", dockerVersion.isPresent());
+ assumeTrue("Docker client on host machine is not a supported version. Please upgrade and try again.", DockerVersion.COMPARATOR.compare(dockerVersion.get(), DockerVersion.parseVersionString(MINIMUM_DOCKER_VERSION)) >= 0);
+ LOGGER.debug("Discovered valid docker client on host. version: {}", dockerVersion);
+
+ hostIp = System.getProperty("host.ip");
+
+ if (hostIp == null) {
+ final Optional<String> foundHostIp = new DockerHostIpSupplier().get();
+ if (foundHostIp.isPresent()) {
+ hostIp = foundHostIp.get();
+ }
+ }
+ Assert.assertNotNull("Could not resolve host machine's IP address for docker adapter, and 'host.ip' system poperty not set. Client will not be able to authenticate against the keycloak server!", hostIp);
+ }
+
+ @Override
+ public void addTestRealms(final List<RealmRepresentation> testRealms) {
+ final RealmRepresentation dockerRealm = loadJson(getClass().getResourceAsStream("/docker-test-realm.json"), RealmRepresentation.class);
+
+ /**
+ * TODO fix test harness/importer NPEs when attempting to create realm from scratch.
+ * Need to fix those, would be preferred to do this programmatically such that we don't have to keep realm elements
+ * (I.E. certs, realm url) in sync with a flat file
+ *
+ * final RealmRepresentation dockerRealm = DockerTestRealmSetup.createRealm(REALM_ID);
+ * DockerTestRealmSetup.configureDockerAuthenticationFlow(dockerRealm, AUTH_FLOW);
+ */
+
+ DockerTestRealmSetup.configureDockerRegistryClient(dockerRealm, CLIENT_ID);
+ DockerTestRealmSetup.configureUser(dockerRealm, DOCKER_USER, DOCKER_USER_PASSWORD);
+
+ testRealms.add(dockerRealm);
+ }
+
+ @Override
+ public void beforeAbstractKeycloakTest() throws Exception {
+ super.beforeAbstractKeycloakTest();
+
+ final Map<String, String> environment = new HashMap<>();
+ environment.put("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp");
+ environment.put("REGISTRY_HTTP_TLS_CERTIFICATE", "/opt/certs/localhost.crt");
+ environment.put("REGISTRY_HTTP_TLS_KEY", "/opt/certs/localhost.key");
+ environment.put("REGISTRY_AUTH_TOKEN_REALM", "http://" + hostIp + ":8180/auth/realms/docker-test-realm/protocol/docker-v2/auth");
+ environment.put("REGISTRY_AUTH_TOKEN_SERVICE", CLIENT_ID);
+ environment.put("REGISTRY_AUTH_TOKEN_ISSUER", "http://" + hostIp + ":8180/auth/realms/docker-test-realm");
+ environment.put("REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE", "/opt/certs/docker-realm-public-key.pem");
+ environment.put("INSECURE_REGISTRY", "--insecure-registry " + REGISTRY_HOSTNAME + ":" + REGISTRY_PORT);
+
+ String dockerioPrefix = Boolean.parseBoolean(System.getProperty("docker.io-prefix-explicit")) ? "docker.io/" : "";
+
+ // TODO this required me to turn selinux off :(. Add BindMode options for :z and :Z. Make selinux enforcing again!
+ dockerRegistryContainer = new GenericContainer(dockerioPrefix + "registry:2")
+ .withClasspathResourceMapping("dockerClientTest/keycloak-docker-compose-yaml/certs", "/opt/certs", BindMode.READ_ONLY)
+ .withEnv(environment)
+ .withPrivilegedMode(true);
+ dockerRegistryContainer.start();
+ dockerRegistryContainer.followOutput(new Slf4jLogConsumer(LOGGER));
+
+ dockerClientContainer = new GenericContainer(
+ new ImageFromDockerfile()
+ .withDockerfileFromBuilder(dockerfileBuilder -> {
+ dockerfileBuilder.from("centos/systemd:latest")
+ .run("yum", "install", "-y", "docker", "iptables", ";", "yum", "clean", "all")
+ .cmd("/usr/sbin/init")
+ .volume("/sys/fs/cgroup")
+ .build();
+ })
+ )
+ .withClasspathResourceMapping("dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt", "/opt/docker/certs.d/" + REGISTRY_HOSTNAME + "/localhost.crt", BindMode.READ_ONLY)
+ .withClasspathResourceMapping("dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker", "/etc/sysconfig/docker", BindMode.READ_WRITE)
+ .withPrivilegedMode(true);
+
+ final Optional<ContainerNetwork> network = dockerRegistryContainer.getContainerInfo().getNetworkSettings().getNetworks().values().stream().findFirst();
+ assumeTrue("Could not find a network adapter whereby the docker client container could connect to host!", network.isPresent());
+ dockerClientContainer.withExtraHost(REGISTRY_HOSTNAME, network.get().getIpAddress());
+
+ dockerClientContainer.start();
+ dockerClientContainer.followOutput(new Slf4jLogConsumer(LOGGER));
+
+ int i = 0;
+ String stdErr = "";
+ while (i++ < 30) {
+ log.infof("Trying to start docker service; attempt: %d", i);
+ stdErr = dockerClientContainer.execInContainer("systemctl", "start", "docker.service").getStderr();
+ if (stdErr.isEmpty()) {
+ break;
+ }
+ else {
+ log.info("systemctl failed: " + stdErr);
+ }
+ WaitUtils.pause(1000);
+ }
+
+ assumeTrue("Cannot start docker service!", stdErr.isEmpty());
+
+ log.info("Waiting for docker service...");
+ validateDockerStarted();
+ log.info("Docker service successfully started");
+ }
+
+ private void validateDockerStarted() {
+ final Callable<Boolean> checkStrategy = () -> {
+ try {
+ final String commandResult = dockerClientContainer.execInContainer("docker", "ps").getStderr();
+ return !commandResult.contains("Cannot connect");
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } catch (Exception e) {
+ return false;
+ }
+ };
+
+ Unreliables.retryUntilTrue(30, TimeUnit.SECONDS, () -> RateLimiterBuilder.newBuilder().withRate(1, TimeUnit.SECONDS).withConstantThroughput().build().getWhenReady(() -> {
+ try {
+ return checkStrategy.call();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }));
+ }
+
+ @Test
+ public void shouldPerformDockerAuthAgainstRegistry() throws Exception {
+ Container.ExecResult dockerLoginResult = dockerClientContainer.execInContainer("docker", "login", "-u", DOCKER_USER, "-p", DOCKER_USER_PASSWORD, REGISTRY_HOSTNAME + ":" + REGISTRY_PORT);
+ printNonEmpties(dockerLoginResult.getStdout(), dockerLoginResult.getStderr());
+ assertThat(dockerLoginResult.getStdout(), containsString("Login Succeeded"));
+ }
+
+ private static void printNonEmpties(final String... results) {
+ Arrays.stream(results)
+ .forEachOrdered(DockerClientTest::printNonEmpty);
+ }
+
+ private static void printNonEmpty(final String result) {
+ if (nullOrEmpty.negate().test(result)) {
+ LOGGER.info(result);
+ }
+ }
+
+ public static final Predicate<String> nullOrEmpty = string -> string == null || string.isEmpty();
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostIpSupplier.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostIpSupplier.java
new file mode 100644
index 0000000..b73471c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostIpSupplier.java
@@ -0,0 +1,45 @@
+package org.keycloak.testsuite.docker;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+
+/**
+ * Docker doesn't provide a static/reliable way to grab the host machine's IP.
+ * <p>
+ * this currently just returns the first address for the bridge adapter starting with 'docker'. Not the most elegant solution,
+ * but I'm open to suggestions.
+ *
+ * @see https://github.com/moby/moby/issues/1143 and related issues referenced therein.
+ */
+public class DockerHostIpSupplier implements Supplier<Optional<String>> {
+
+ @Override
+ public Optional<String> get() {
+ final Enumeration<NetworkInterface> networkInterfaces;
+ try {
+ networkInterfaces = NetworkInterface.getNetworkInterfaces();
+ } catch (SocketException e) {
+ return Optional.empty();
+ }
+
+ return Collections.list(networkInterfaces).stream()
+ .filter(networkInterface -> networkInterface.getDisplayName().startsWith("docker"))
+ .flatMap(networkInterface -> Collections.list(networkInterface.getInetAddresses()).stream())
+ .map(InetAddress::getHostAddress)
+ .filter(DockerHostIpSupplier::looksLikeIpv4Address)
+ .findFirst();
+ }
+
+ public static boolean looksLikeIpv4Address(final String ip) {
+ return IPv4RegexPattern.matcher(ip).matches();
+ }
+
+ private static final Pattern IPv4RegexPattern = Pattern.compile("^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostVersionSupplier.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostVersionSupplier.java
new file mode 100644
index 0000000..eac0092
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostVersionSupplier.java
@@ -0,0 +1,43 @@
+package org.keycloak.testsuite.docker;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class DockerHostVersionSupplier implements Supplier<Optional<DockerVersion>> {
+ private static final Logger log = LoggerFactory.getLogger(DockerHostVersionSupplier.class);
+
+ @Override
+ public Optional<DockerVersion> get() {
+ try {
+ Process process = new ProcessBuilder()
+ .command("docker", "version", "--format", "'{{.Client.Version}}'")
+ .start();
+
+ final BufferedReader stdout = getReader(process, Process::getInputStream);
+ final BufferedReader err = getReader(process, Process::getErrorStream);
+
+ int exitCode = process.waitFor();
+ if (exitCode == 0) {
+ final String versionString = stdout.lines().collect(Collectors.joining()).replaceAll("'", "");
+ return Optional.ofNullable(DockerVersion.parseVersionString(versionString));
+ }
+ } catch (IOException | InterruptedException e) {
+ log.error("Could not determine host machine's docker version: ", e);
+ }
+
+ return Optional.empty();
+ }
+
+ private static BufferedReader getReader(final Process process, final Function<Process, InputStream> streamSelector) {
+ return new BufferedReader(new InputStreamReader(streamSelector.apply(process)));
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerTestRealmSetup.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerTestRealmSetup.java
new file mode 100644
index 0000000..727af1d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerTestRealmSetup.java
@@ -0,0 +1,87 @@
+package org.keycloak.testsuite.docker;
+
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.protocol.docker.DockerAuthV2Protocol;
+import org.keycloak.protocol.docker.DockerAuthenticator;
+import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
+import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public final class DockerTestRealmSetup {
+
+ private DockerTestRealmSetup() {
+ }
+
+ public static RealmRepresentation createRealm(final String realmId) {
+ final RealmRepresentation createdRealm = new RealmRepresentation();
+ createdRealm.setId(UUID.randomUUID().toString());
+ createdRealm.setRealm(realmId);
+ createdRealm.setEnabled(true);
+ createdRealm.setAuthenticatorConfig(new ArrayList<>());
+
+ return createdRealm;
+ }
+
+ public static void configureDockerAuthenticationFlow(final RealmRepresentation dockerRealm, final String authFlowAlais) {
+ final AuthenticationFlowRepresentation dockerBasicAuthFlow = new AuthenticationFlowRepresentation();
+ dockerBasicAuthFlow.setId(UUID.randomUUID().toString());
+ dockerBasicAuthFlow.setAlias(authFlowAlais);
+ dockerBasicAuthFlow.setProviderId("basic-flow");
+ dockerBasicAuthFlow.setTopLevel(true);
+ dockerBasicAuthFlow.setBuiltIn(false);
+
+ final AuthenticationExecutionExportRepresentation dockerBasicAuthExecution = new AuthenticationExecutionExportRepresentation();
+ dockerBasicAuthExecution.setAuthenticator(DockerAuthenticator.ID);
+ dockerBasicAuthExecution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.name());
+ dockerBasicAuthExecution.setPriority(0);
+ dockerBasicAuthExecution.setUserSetupAllowed(false);
+ dockerBasicAuthExecution.setAutheticatorFlow(false);
+
+ final List<AuthenticationExecutionExportRepresentation> authenticationExecutions = Optional.ofNullable(dockerBasicAuthFlow.getAuthenticationExecutions()).orElse(new ArrayList<>());
+ authenticationExecutions.add(dockerBasicAuthExecution);
+ dockerBasicAuthFlow.setAuthenticationExecutions(authenticationExecutions);
+
+ final List<AuthenticationFlowRepresentation> authenticationFlows = Optional.ofNullable(dockerRealm.getAuthenticationFlows()).orElse(new ArrayList<>());
+ authenticationFlows.add(dockerBasicAuthFlow);
+ dockerRealm.setAuthenticationFlows(authenticationFlows);
+ dockerRealm.setBrowserFlow(dockerBasicAuthFlow.getAlias());
+ }
+
+
+ public static void configureDockerRegistryClient(final RealmRepresentation dockerRealm, final String clientId) {
+ final ClientRepresentation dockerClient = new ClientRepresentation();
+ dockerClient.setClientId(clientId);
+ dockerClient.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL);
+ dockerClient.setEnabled(true);
+
+ final List<ClientRepresentation> clients = Optional.ofNullable(dockerRealm.getClients()).orElse(new ArrayList<>());
+ clients.add(dockerClient);
+ dockerRealm.setClients(clients);
+ }
+
+ public static void configureUser(final RealmRepresentation dockerRealm, final String username, final String password) {
+ final UserRepresentation dockerUser = new UserRepresentation();
+ dockerUser.setUsername(username);
+ dockerUser.setEnabled(true);
+ dockerUser.setEmail("docker-users@localhost.localdomain");
+ dockerUser.setFirstName("docker");
+ dockerUser.setLastName("user");
+
+ final CredentialRepresentation dockerUserCreds = new CredentialRepresentation();
+ dockerUserCreds.setType(CredentialRepresentation.PASSWORD);
+ dockerUserCreds.setValue(password);
+ dockerUser.setCredentials(Collections.singletonList(dockerUserCreds));
+
+ dockerRealm.setUsers(Collections.singletonList(dockerUser));
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerVersion.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerVersion.java
new file mode 100644
index 0000000..7182c54
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerVersion.java
@@ -0,0 +1,99 @@
+package org.keycloak.testsuite.docker;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public class DockerVersion {
+
+ public static final Integer MAJOR_VERSION_INDEX = 0;
+ public static final Integer MINOR_VERSION_INDEX = 1;
+ public static final Integer PATCH_VERSION_INDEX = 2;
+
+ private final Integer major;
+ private final Integer minor;
+ private final Integer patch;
+
+ public static final Comparator<DockerVersion> COMPARATOR = (lhs, rhs) -> Comparator.comparing(DockerVersion::getMajor)
+ .thenComparing(Comparator.comparing(DockerVersion::getMinor)
+ .thenComparing(Comparator.comparing(DockerVersion::getPatch)))
+ .compare(lhs, rhs);
+
+ /**
+ * Major version is required. minor and patch versions will be assumed '0' if not provided.
+ */
+ public DockerVersion(final Integer major, final Optional<Integer> minor, final Optional<Integer> patch) {
+ Objects.requireNonNull(major, "Invalid docker version - no major release number given");
+
+ this.major = major;
+ this.minor = minor.orElse(0);
+ this.patch = patch.orElse(0);
+ }
+
+ /**
+ * @param versionString given in the form '1.12.6'
+ */
+ public static DockerVersion parseVersionString(final String versionString) {
+ Objects.requireNonNull(versionString, "Cannot parse null docker version string");
+
+ final List<Integer> versionNumberList = Arrays.stream(stripDashAndEdition(versionString).trim().split("\\."))
+ .map(Integer::parseInt)
+ .collect(Collectors.toList());
+
+ return new DockerVersion(versionNumberList.get(MAJOR_VERSION_INDEX),
+ Optional.ofNullable(versionNumberList.get(MINOR_VERSION_INDEX)),
+ Optional.ofNullable(versionNumberList.get(PATCH_VERSION_INDEX)));
+ }
+
+ private static String stripDashAndEdition(final String versionString) {
+ if (versionString.contains("-")) {
+ return versionString.substring(0, versionString.indexOf("-"));
+ }
+
+ return versionString;
+ }
+
+ public Integer getMajor() {
+ return major;
+ }
+
+ public Integer getMinor() {
+ return minor;
+ }
+
+ public Integer getPatch() {
+ return patch;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ DockerVersion that = (DockerVersion) o;
+
+ if (major != null ? !major.equals(that.major) : that.major != null) return false;
+ if (minor != null ? !minor.equals(that.minor) : that.minor != null) return false;
+ return patch != null ? patch.equals(that.patch) : that.patch == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = major != null ? major.hashCode() : 0;
+ result = 31 * result + (minor != null ? minor.hashCode() : 0);
+ result = 31 * result + (patch != null ? patch.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "DockerVersion{" +
+ "major=" + major +
+ ", minor=" + minor +
+ ", patch=" + patch +
+ '}';
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java
index 08d8265..da54a72 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java
@@ -263,11 +263,12 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest {
registerPage.clickBackToLogin();
loginPage.assertCurrent();
- // Click browser "back" button. Should be back on register page
+ // Click browser "back" button.
driver.navigate().back();
registerPage.assertCurrent();
}
+
@Test
public void clickBackButtonFromRegisterPage() {
loginPage.open();
@@ -280,6 +281,28 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest {
}
+ // KEYCLOAK-5136
+ @Test
+ public void clickRefreshButtonOnRegisterPage() {
+ loginPage.open();
+ loginPage.clickRegister();
+ registerPage.assertCurrent();
+
+ // Click browser "refresh" button. Should be still on register page
+ driver.navigate().refresh();
+ registerPage.assertCurrent();
+
+ // Click 'back to login'. Should be on login page
+ registerPage.clickBackToLogin();
+ loginPage.assertCurrent();
+
+ // Click browser 'refresh'. Should be still on login page
+ driver.navigate().refresh();
+ loginPage.assertCurrent();
+
+ }
+
+
@Test
public void backButtonToAuthorizationEndpoint() {
loginPage.open();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index 16d85ef..c5147b9 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -371,7 +371,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
loginPage.assertCurrent();
- assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError());
+ assertEquals("Action expired. Please start again.", loginPage.getError());
events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent();
} finally {
@@ -407,7 +407,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
loginPage.assertCurrent();
- assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError());
+ assertEquals("Action expired. Please start again.", loginPage.getError());
events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent();
} finally {
@@ -450,7 +450,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
driver.navigate().to(changePasswordUrl.trim());
errorPage.assertCurrent();
- Assert.assertEquals("Reset Credential not allowed", errorPage.getError());
+ Assert.assertEquals("Action expired.", errorPage.getError());
String backToAppLink = errorPage.getBackToApplicationLink();
Assert.assertTrue(backToAppLink.endsWith("/app/auth"));
@@ -463,6 +463,57 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
}
}
+
+ // KEYCLOAK-5061
+ @Test
+ public void resetPasswordExpiredCodeForgotPasswordFlow() throws IOException, MessagingException, InterruptedException {
+ final AtomicInteger originalValue = new AtomicInteger();
+
+ RealmRepresentation realmRep = testRealm().toRepresentation();
+ originalValue.set(realmRep.getActionTokenGeneratedByUserLifespan());
+ realmRep.setActionTokenGeneratedByUserLifespan(60);
+ testRealm().update(realmRep);
+
+ try {
+ // Redirect directly to KC "forgot password" endpoint instead of "authenticate" endpoint
+ String loginUrl = oauth.getLoginFormUrl();
+ String forgotPasswordUrl = loginUrl.replace("/auth?", "/forgot-credentials?"); // Workaround, but works
+
+ driver.navigate().to(forgotPasswordUrl);
+ resetPasswordPage.assertCurrent();
+ resetPasswordPage.changePassword("login-test");
+
+ loginPage.assertCurrent();
+ assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ expectedMessagesCount++;
+
+ events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
+ .session((String)null)
+ .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
+
+ assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String changePasswordUrl = getPasswordResetEmailLink(message);
+
+ setTimeOffset(70);
+
+ driver.navigate().to(changePasswordUrl.trim());
+
+ resetPasswordPage.assertCurrent();
+
+ assertEquals("Action expired. Please start again.", loginPage.getError());
+
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent();
+ } finally {
+ setTimeOffset(0);
+
+ realmRep.setActionTokenGeneratedByUserLifespan(originalValue.get());
+ testRealm().update(realmRep);
+ }
+ }
+
@Test
public void resetPasswordDisabledUser() throws IOException, MessagingException, InterruptedException {
UserRepresentation user = findUser("login-test");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java
index 068c426..9700a29 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java
@@ -23,6 +23,9 @@ import org.jboss.arquillian.graphene.page.Page;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.RSATokenVerifier;
+import org.keycloak.client.registration.Auth;
+import org.keycloak.client.registration.ClientRegistration;
+import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.MultivaluedHashMap;
@@ -31,6 +34,8 @@ import org.keycloak.keys.Attributes;
import org.keycloak.keys.GeneratedHmacKeyProviderFactory;
import org.keycloak.keys.KeyProvider;
import org.keycloak.keys.ImportedRsaKeyProviderFactory;
+import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
+import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation;
@@ -41,11 +46,11 @@ import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.KeycloakModelUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.UserInfoClientUtil;
-import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.security.KeyPair;
@@ -127,12 +132,27 @@ public class KeyRotationTest extends AbstractKeycloakTest {
assertTokenSignature(key1, response.getAccessToken());
assertTokenSignature(key1, response.getRefreshToken());
+ // Create client with keys #1
+ ClientInitialAccessCreatePresentation initialToken = new ClientInitialAccessCreatePresentation();
+ initialToken.setCount(100);
+ initialToken.setExpiration(0);
+ ClientInitialAccessPresentation accessRep = adminClient.realm("test").clientInitialAccess().create(initialToken);
+ String initialAccessToken = accessRep.getToken();
+
+ ClientRegistration reg = ClientRegistration.create().url(suiteContext.getAuthServerInfo().getContextRoot() + "/auth", "test").build();
+ reg.auth(Auth.token(initialAccessToken));
+ ClientRepresentation clientRep = reg.create(ClientBuilder.create().clientId("test").build());
+
// Userinfo with keys #1
assertUserInfo(response.getAccessToken(), 200);
// Token introspection with keys #1
assertTokenIntrospection(response.getAccessToken(), true);
+ // Get client with keys #1 - registration access token should not have changed
+ ClientRepresentation clientRep2 = reg.auth(Auth.token(clientRep.getRegistrationAccessToken())).get("test");
+ assertEquals(clientRep.getRegistrationAccessToken(), clientRep2.getRegistrationAccessToken());
+
// Create keys #2
PublicKey key2 = createKeys2();
@@ -148,6 +168,10 @@ public class KeyRotationTest extends AbstractKeycloakTest {
// Token introspection with keys #2
assertTokenIntrospection(response.getAccessToken(), true);
+ // Get client with keys #2 - registration access token should be changed
+ ClientRepresentation clientRep3 = reg.auth(Auth.token(clientRep.getRegistrationAccessToken())).get("test");
+ assertNotEquals(clientRep.getRegistrationAccessToken(), clientRep3.getRegistrationAccessToken());
+
// Drop key #1
dropKeys1();
@@ -162,6 +186,17 @@ public class KeyRotationTest extends AbstractKeycloakTest {
// Token introspection with keys #1 dropped
assertTokenIntrospection(response.getAccessToken(), true);
+ // Get client with keys #1 - should fail
+ try {
+ reg.auth(Auth.token(clientRep.getRegistrationAccessToken())).get("test");
+ fail("Expected to fail");
+ } catch (ClientRegistrationException e) {
+ }
+
+ // Get client with keys #2 - should succeed
+ ClientRepresentation clientRep4 = reg.auth(Auth.token(clientRep3.getRegistrationAccessToken())).get("test");
+ assertNotEquals(clientRep2.getRegistrationAccessToken(), clientRep4.getRegistrationAccessToken());
+
// Drop key #2
dropKeys2();
@@ -292,7 +327,7 @@ public class KeyRotationTest extends AbstractKeycloakTest {
}
private void assertUserInfo(String token, int expectedStatus) {
- Response userInfoResponse = UserInfoClientUtil.executeUserInfoRequest_getMethod(ClientBuilder.newClient(), token);
+ Response userInfoResponse = UserInfoClientUtil.executeUserInfoRequest_getMethod(javax.ws.rs.client.ClientBuilder.newClient(), token);
assertEquals(expectedStatus, userInfoResponse.getStatus());
userInfoResponse.close();
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java
index a769687..cfdf0b7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java
@@ -59,6 +59,7 @@ import org.keycloak.testsuite.runonserver.RunHelpers;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.OAuthClient;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS;
@@ -216,6 +217,20 @@ public class MigrationTest extends AbstractKeycloakTest {
private void testMigrationTo3_2_0() {
assertNull(masterRealm.toRepresentation().getPasswordPolicy());
assertNull(migrationRealm.toRepresentation().getPasswordPolicy());
+
+ testDockerAuthenticationFlow(masterRealm, migrationRealm);
+ }
+
+ private void testDockerAuthenticationFlow(RealmResource... realms) {
+ for (RealmResource realm : realms) {
+ AuthenticationFlowRepresentation flow = null;
+ for (AuthenticationFlowRepresentation f : realm.flows().getFlows()) {
+ if (DefaultAuthenticationFlows.DOCKER_AUTH.equals(f.getAlias())) {
+ flow = f;
+ }
+ }
+ assertNotNull(flow);
+ }
}
private void testRoleManageAccountLinks(RealmResource... realms) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java
index 84144f6..b480cdb 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java
@@ -30,13 +30,19 @@ import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.common.Version;
import org.keycloak.models.Constants;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.ActionURIUtils;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
+import org.keycloak.testsuite.runonserver.ServerVersion;
import java.io.IOException;
import java.net.URLEncoder;
@@ -56,6 +62,11 @@ import static org.junit.Assert.assertTrue;
*/
public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest {
+ @Deployment
+ public static WebArchive deploy() {
+ return RunOnServerDeployment.create(LoginStatusIframeEndpointTest.class, ServerVersion.class);
+ }
+
@Test
public void checkIframe() throws IOException {
CookieStore cookieStore = new BasicCookieStore();
@@ -185,6 +196,28 @@ public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest {
}
}
+ @Test
+ public void checkIframeCache() throws IOException {
+ String version = testingClient.server().fetch(new ServerVersion());
+
+ CloseableHttpClient client = HttpClients.createDefault();
+ try {
+ HttpGet get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html");
+ CloseableHttpResponse response = client.execute(get);
+
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ assertEquals("no-cache, must-revalidate, no-transform, no-store", response.getHeaders("Cache-Control")[0].getValue());
+
+ get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html?version=" + version);
+ response = client.execute(get);
+
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ assertTrue(response.getHeaders("Cache-Control")[0].getValue().contains("max-age"));
+ } finally {
+ client.close();
+ }
+ }
+
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriStateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriStateTest.java
new file mode 100644
index 0000000..db410d5
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriStateTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.oauth;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.util.OAuthClient;
+
+public class OAuthRedirectUriStateTest extends AbstractTestRealmKeycloakTest {
+
+ @Override
+ public void configureTestRealm(RealmRepresentation testRealm) {
+ }
+
+ @Before
+ public void clientConfiguration() {
+ oauth.clientId("test-app");
+ oauth.responseType(OIDCResponseType.CODE);
+ oauth.stateParamRandom();
+ }
+
+ void assertStateReflected(String state) {
+ oauth.stateParamHardcoded(state);
+
+ OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
+ Assert.assertNotNull(response.getCode());
+
+ URL url;
+ try {
+ url = new URL(driver.getCurrentUrl());
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ Assert.assertTrue(url.getQuery().contains("state=" + state));
+ }
+
+ @Test
+ public void testSimpleStateParameter() {
+ assertStateReflected("VeryLittleGravitasIndeed");
+ }
+
+ @Test
+ public void testJsonStateParameter() {
+ assertStateReflected("%7B%22csrf_token%22%3A%2B%22hlvZNIsWyqdkEhbjlQIia0ty2YY4TXat%22%2C%2B%22destination%22%3A%2B%22eyJhbGciOiJIUzI1NiJ9.Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9wcml2YXRlIg.T18WeIV29komDl8jav-3bSnUZDlMD8VOfIrd2ikP5zE%22%7D");
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java
index c0c8601..6f4e394 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java
@@ -143,6 +143,14 @@ public class OAuthRedirectUriTest extends AbstractKeycloakTest {
}
@Test
+ public void testFileUri() throws IOException {
+ oauth.redirectUri("file://test");
+ oauth.openLoginForm();
+ Assert.assertTrue(errorPage.isCurrent());
+ Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError());
+ }
+
+ @Test
public void testNoParamMultipleValidUris() throws IOException {
ClientManager.realm(adminClient.realm("test")).clientId("test-app").addRedirectUris("http://localhost:8180/app2");
try {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
index 208e6e3..7e3594f 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
@@ -27,15 +27,18 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.IDToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.RealmManager;
+import org.keycloak.testsuite.util.UserManager;
import org.keycloak.util.BasicAuthHelper;
import javax.ws.rs.client.Client;
@@ -488,6 +491,61 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
}
+ @Test
+ public void refreshTokenUserDisabled() throws Exception {
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+ String sessionId = loginEvent.getSessionId();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+ String refreshTokenString = response.getRefreshToken();
+ RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString);
+
+ events.expectCodeToToken(codeId, sessionId).assertEvent();
+
+ try {
+ UserManager.realm(adminClient.realm("test")).username("test-user@localhost").enabled(false);
+ response = oauth.doRefreshTokenRequest(refreshTokenString, "password");
+ assertEquals(400, response.getStatusCode());
+ assertEquals("invalid_grant", response.getError());
+
+ events.expectRefresh(refreshToken.getId(), sessionId).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
+ } finally {
+ UserManager.realm(adminClient.realm("test")).username("test-user@localhost").enabled(true);
+ }
+ }
+
+ @Test
+ public void refreshTokenUserDeleted() throws Exception {
+ String userId = createUser("test", "temp-user@localhost", "password");
+ oauth.doLogin("temp-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.expectLogin().user(userId).assertEvent();
+
+ String sessionId = loginEvent.getSessionId();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+ String refreshTokenString = response.getRefreshToken();
+ RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString);
+
+ events.expectCodeToToken(codeId, sessionId).user(userId).assertEvent();
+
+ adminClient.realm("test").users().delete(userId);
+ response = oauth.doRefreshTokenRequest(refreshTokenString, "password");
+ assertEquals(400, response.getStatusCode());
+ assertEquals("invalid_grant", response.getError());
+
+ events.expectRefresh(refreshToken.getId(), sessionId).user(userId).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
+ }
+
protected Response executeRefreshToken(WebTarget refreshTarget, String refreshToken) {
String header = BasicAuthHelper.createHeader("test-app", "password");
Form form = new Form();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java
new file mode 100755
index 0000000..2889118
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.oauth;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.TokenVerifier;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
+import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
+import org.keycloak.services.resources.admin.permissions.AdminPermissions;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
+import org.keycloak.testsuite.util.OAuthClient;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class TokenExchangeTest extends AbstractKeycloakTest {
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ @Deployment
+ public static WebArchive deploy() {
+ return RunOnServerDeployment.create(TokenExchangeTest.class);
+ }
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ RealmRepresentation testRealmRep = new RealmRepresentation();
+ testRealmRep.setId(TEST);
+ testRealmRep.setRealm(TEST);
+ testRealmRep.setEnabled(true);
+ testRealms.add(testRealmRep);
+ }
+
+ public static void setupRealm(KeycloakSession session) {
+ RealmModel realm = session.realms().getRealmByName(TEST);
+
+ RoleModel exampleRole = realm.addRole("example");
+
+ ClientModel target = realm.addClient("target");
+ target.setDirectAccessGrantsEnabled(true);
+ target.setEnabled(true);
+ target.setSecret("secret");
+ target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ target.setFullScopeAllowed(false);
+ target.addScopeMapping(exampleRole);
+
+ ClientModel clientExchanger = realm.addClient("client-exchanger");
+ clientExchanger.setClientId("client-exchanger");
+ clientExchanger.setPublicClient(false);
+ clientExchanger.setDirectAccessGrantsEnabled(true);
+ clientExchanger.setEnabled(true);
+ clientExchanger.setSecret("secret");
+ clientExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ clientExchanger.setFullScopeAllowed(false);
+
+ ClientModel illegal = realm.addClient("illegal");
+ illegal.setClientId("illegal");
+ illegal.setPublicClient(false);
+ illegal.setDirectAccessGrantsEnabled(true);
+ illegal.setEnabled(true);
+ illegal.setSecret("secret");
+ illegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ illegal.setFullScopeAllowed(false);
+
+ ClientModel illegalTo = realm.addClient("illegal-to");
+ illegalTo.setClientId("illegal-to");
+ illegalTo.setPublicClient(false);
+ illegalTo.setDirectAccessGrantsEnabled(true);
+ illegalTo.setEnabled(true);
+ illegalTo.setSecret("secret");
+ illegalTo.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ illegalTo.setFullScopeAllowed(false);
+
+ ClientModel legal = realm.addClient("legal");
+ legal.setClientId("legal");
+ legal.setPublicClient(false);
+ legal.setDirectAccessGrantsEnabled(true);
+ legal.setEnabled(true);
+ legal.setSecret("secret");
+ legal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ legal.setFullScopeAllowed(false);
+
+ AdminPermissionManagement management = AdminPermissions.management(session, realm);
+
+ management.clients().setPermissionsEnabled(target, true);
+ ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
+ clientRep.setName("to");
+ clientRep.addClient(clientExchanger.getId());
+ clientRep.addClient(legal.getId());
+ ResourceServer server = management.realmResourceServer();
+ Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
+ management.clients().exchangeToPermission(target).addAssociatedPolicy(clientPolicy);
+
+ management.clients().setPermissionsEnabled(clientExchanger, true);
+ ClientPolicyRepresentation client2Rep = new ClientPolicyRepresentation();
+ client2Rep.setName("from");
+ client2Rep.addClient(legal.getId());
+ client2Rep.addClient(illegalTo.getId());
+ Policy client2Policy = management.authz().getStoreFactory().getPolicyStore().create(client2Rep, server);
+ management.clients().exchangeFromPermission(clientExchanger).addAssociatedPolicy(client2Policy);
+
+
+ UserModel user = session.users().addUser(realm, "user");
+ user.setEnabled(true);
+ session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
+ user.grantRole(exampleRole);
+
+ }
+
+ @Override
+ protected boolean isImportAfterEachMethod() {
+ return true;
+ }
+
+
+ @Test
+ public void testExchange() throws Exception {
+ testingClient.server().run(TokenExchangeTest::setupRealm);
+
+ oauth.realm(TEST);
+ oauth.clientId("client-exchanger");
+
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password");
+ String accessToken = response.getAccessToken();
+ TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
+ AccessToken token = accessTokenVerifier.parse().getToken();
+ Assert.assertEquals(token.getPreferredUsername(), "user");
+ Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
+
+ {
+ response = oauth.doTokenExchange(TEST, accessToken, "target", "client-exchanger", "secret");
+
+ String exchangedTokenString = response.getAccessToken();
+ TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
+ AccessToken exchangedToken = verifier.parse().getToken();
+ Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor());
+ Assert.assertEquals("target", exchangedToken.getAudience()[0]);
+ Assert.assertEquals(exchangedToken.getPreferredUsername(), "user");
+ Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
+ }
+
+ {
+ response = oauth.doTokenExchange(TEST, accessToken, "target", "legal", "secret");
+
+ String exchangedTokenString = response.getAccessToken();
+ TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
+ AccessToken exchangedToken = verifier.parse().getToken();
+ Assert.assertEquals("legal", exchangedToken.getIssuedFor());
+ Assert.assertEquals("target", exchangedToken.getAudience()[0]);
+ Assert.assertEquals(exchangedToken.getPreferredUsername(), "user");
+ Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
+ }
+ {
+ response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal", "secret");
+ Assert.assertEquals(403, response.getStatusCode());
+ }
+ {
+ response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal-to", "secret");
+ Assert.assertEquals(403, response.getStatusCode());
+ }
+
+
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java
index 0203eb1..c4a6c45 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java
@@ -29,6 +29,7 @@ import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.clientregistration.ClientRegistrationService;
import org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory;
+import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
@@ -38,12 +39,16 @@ import org.keycloak.testsuite.util.OAuthClient;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.util.List;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@@ -75,10 +80,10 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
OIDCConfigurationRepresentation oidcConfig = getOIDCDiscoveryConfiguration(client);
// URIs are filled
- Assert.assertEquals(oidcConfig.getAuthorizationEndpoint(), OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT)).build("test").toString());
- Assert.assertEquals(oidcConfig.getTokenEndpoint(), oauth.getAccessTokenUrl());
- Assert.assertEquals(oidcConfig.getUserinfoEndpoint(), OIDCLoginProtocolService.userInfoUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT)).build("test").toString());
- Assert.assertEquals(oidcConfig.getJwksUri(), oauth.getCertsUrl("test"));
+ assertEquals(oidcConfig.getAuthorizationEndpoint(), OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT)).build("test").toString());
+ assertEquals(oidcConfig.getTokenEndpoint(), oauth.getAccessTokenUrl());
+ assertEquals(oidcConfig.getUserinfoEndpoint(), OIDCLoginProtocolService.userInfoUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT)).build("test").toString());
+ assertEquals(oidcConfig.getJwksUri(), oauth.getCertsUrl("test"));
String registrationUri = UriBuilder
.fromUri(OAuthClient.AUTH_SERVER_ROOT)
@@ -87,7 +92,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
.path(ClientRegistrationService.class, "provider")
.build("test", OIDCClientRegistrationProviderFactory.ID)
.toString();
- Assert.assertEquals(oidcConfig.getRegistrationEndpoint(), registrationUri);
+ assertEquals(oidcConfig.getRegistrationEndpoint(), registrationUri);
// Support standard + implicit + hybrid flow
assertContains(oidcConfig.getResponseTypesSupported(), OAuth2Constants.CODE, OIDCResponseType.ID_TOKEN, "id_token token", "code id_token", "code token", "code id_token token");
@@ -123,7 +128,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
public void testIssuerMatches() throws Exception {
OAuthClient.AuthorizationEndpointResponse authzResp = oauth.doLogin("test-user@localhost", "password");
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(authzResp.getCode(), "password");
- Assert.assertEquals(200, response.getStatusCode());
+ assertEquals(200, response.getStatusCode());
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
Client client = ClientBuilder.newClient();
@@ -131,18 +136,36 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
OIDCConfigurationRepresentation oidcConfig = getOIDCDiscoveryConfiguration(client);
// assert issuer matches
- Assert.assertEquals(idToken.getIssuer(), oidcConfig.getIssuer());
+ assertEquals(idToken.getIssuer(), oidcConfig.getIssuer());
} finally {
client.close();
}
}
+ @Test
+ public void corsTest() {
+ Client client = ClientBuilder.newClient();
+ UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT);
+ URI oidcDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder).build("test", OIDCWellKnownProviderFactory.PROVIDER_ID);
+ WebTarget oidcDiscoveryTarget = client.target(oidcDiscoveryUri);
+
+
+ Invocation.Builder request = oidcDiscoveryTarget.request();
+ request.header(Cors.ORIGIN_HEADER, "http://somehost");
+ Response response = request.get();
+
+ assertEquals("*", response.getHeaders().getFirst(Cors.ACCESS_CONTROL_ALLOW_ORIGIN));
+ }
+
private OIDCConfigurationRepresentation getOIDCDiscoveryConfiguration(Client client) {
UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT);
URI oidcDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder).build("test", OIDCWellKnownProviderFactory.PROVIDER_ID);
WebTarget oidcDiscoveryTarget = client.target(oidcDiscoveryUri);
Response response = oidcDiscoveryTarget.request().get();
+
+ assertEquals("no-cache, must-revalidate, no-transform, no-store", response.getHeaders().getFirst("Cache-Control"));
+
return response.readEntity(OIDCConfigurationRepresentation.class);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java
index c57b64e..2a84673 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java
@@ -232,13 +232,12 @@ public class UserInfoTest extends AbstractKeycloakTest {
Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessTokenResponse.getToken());
- assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+ assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
response.close();
events.expect(EventType.USER_INFO_REQUEST_ERROR)
.error(Errors.USER_SESSION_NOT_FOUND)
- .client((String) null)
.user(Matchers.nullValue(String.class))
.session(Matchers.nullValue(String.class))
.detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN)
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/RunOnServerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/RunOnServerTest.java
index 4101ffc..4c0862d 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/RunOnServerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/RunOnServerTest.java
@@ -45,6 +45,12 @@ public class RunOnServerTest extends AbstractKeycloakTest {
}
@Test
+ public void runOnServerString() throws IOException {
+ String string = testingClient.server().fetch(session -> "Hello world!", String.class);
+ assertEquals("Hello world!", string);
+ }
+
+ @Test
public void runOnServerRep() throws IOException {
final String realmName = "master";
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/ServerVersion.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/ServerVersion.java
new file mode 100644
index 0000000..3e565dc
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/ServerVersion.java
@@ -0,0 +1,22 @@
+package org.keycloak.testsuite.runonserver;
+
+import org.keycloak.common.Version;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.representations.idm.ComponentRepresentation;
+
+/**
+ * Created by st on 26.01.17.
+ */
+public class ServerVersion implements FetchOnServerWrapper<String> {
+
+ @Override
+ public FetchOnServer getRunOnServer() {
+ return (FetchOnServer) session -> Version.RESOURCES_VERSION;
+ }
+
+ @Override
+ public Class<String> getResultClass() {
+ return String.class;
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java
index ffa5651..484b9cc 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java
@@ -19,42 +19,21 @@ package org.keycloak.testsuite.saml;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
-import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.protocol.saml.SamlConfigAttributes;
-import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
-import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
-import org.keycloak.services.resources.RealmsResource;
-import org.keycloak.testsuite.AbstractAuthTest;
-import org.keycloak.testsuite.util.SamlClient;
-
-import java.io.IOException;
+import org.keycloak.testsuite.util.SamlClientBuilder;
import java.net.URI;
import java.util.List;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriBuilderException;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.client.protocol.HttpClientContext;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClientBuilder;
-import org.apache.http.util.EntityUtils;
import org.hamcrest.Matcher;
import org.junit.Test;
-import org.w3c.dom.Document;
import static org.hamcrest.Matchers.*;
import static org.keycloak.testsuite.util.SamlClient.*;
import static org.junit.Assert.assertThat;
-import static org.keycloak.testsuite.util.IOUtil.loadRealm;
-import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
/**
*
@@ -63,12 +42,18 @@ import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
public class AuthnRequestNameIdFormatTest extends AbstractSamlTest {
private void testLoginWithNameIdPolicy(Binding requestBinding, Binding responseBinding, NameIDPolicyType nameIDPolicy, Matcher<String> nameIdMatcher) throws Exception {
- AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
- loginRep.setProtocolBinding(requestBinding.getBindingUri());
- loginRep.setNameIDPolicy(nameIDPolicy);
-
- Document samlRequest = SAML2Request.convert(loginRep);
- SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, requestBinding, responseBinding);
+ SAMLDocumentHolder res = new SamlClientBuilder()
+ .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, requestBinding)
+ .transformObject(so -> {
+ so.setProtocolBinding(requestBinding.getBindingUri());
+ so.setNameIDPolicy(nameIDPolicy);
+ return so;
+ })
+ .build()
+
+ .login().user(bburkeUser).build()
+
+ .getSamlResponse(responseBinding);
assertThat(res.getSamlObject(), notNullValue());
assertThat(res.getSamlObject(), instanceOf(ResponseType.class));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java
index 78cf93d..abfd001 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java
@@ -12,6 +12,7 @@ import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.util.SamlClient;
import org.keycloak.testsuite.util.SamlClient.Binding;
import org.keycloak.testsuite.util.SamlClient.RedirectStrategyWithSwitchableFollowRedirect;
+import org.keycloak.testsuite.util.SamlClientBuilder;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import org.apache.http.client.methods.CloseableHttpResponse;
@@ -25,10 +26,10 @@ import org.w3c.dom.Document;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_NAME;
import static org.keycloak.testsuite.util.IOUtil.documentToString;
import static org.keycloak.testsuite.util.IOUtil.setDocElementAttributeValue;
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
-import static org.keycloak.testsuite.util.SamlClient.login;
/**
* @author mhajas
@@ -38,13 +39,15 @@ public class BasicSamlTest extends AbstractSamlTest {
// KEYCLOAK-4160
@Test
public void testPropertyValueInAssertion() throws ParsingException, ConfigurationException, ProcessingException {
- AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
-
- Document doc = SAML2Request.convert(loginRep);
-
- setDocElementAttributeValue(doc, "samlp:AuthnRequest", "ID", "${java.version}" );
-
- SAMLDocumentHolder document = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), doc, null, SamlClient.Binding.POST, SamlClient.Binding.POST);
+ SAMLDocumentHolder document = new SamlClientBuilder()
+ .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, Binding.POST)
+ .transformDocument(doc -> {
+ setDocElementAttributeValue(doc, "samlp:AuthnRequest", "ID", "${java.version}" );
+ return doc;
+ })
+ .build()
+ .login().user(bburkeUser).build()
+ .getSamlResponse(Binding.POST);
assertThat(documentToString(document.getSamlDocument()), not(containsString("InResponseTo=\"" + System.getProperty("java.version") + "\"")));
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java
index 31cc14d..13370ac 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java
@@ -22,6 +22,7 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.testsuite.util.SamlClient;
+import org.keycloak.testsuite.util.saml.LoginBuilder;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
@@ -90,7 +91,7 @@ public class ConcurrentAuthnRequestTest extends AbstractSamlTest {
String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
response.close();
- HttpUriRequest loginRequest = handleLoginPage(user, loginPageText);
+ HttpUriRequest loginRequest = LoginBuilder.handleLoginPage(user, loginPageText);
strategy.setRedirectable(false);
response = client.execute(loginRequest, context);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java
index cec6e1a..5c9ee3e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java
@@ -23,24 +23,21 @@ import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.dom.saml.v2.assertion.ConditionAbstractType;
import org.keycloak.dom.saml.v2.assertion.ConditionsType;
import org.keycloak.dom.saml.v2.assertion.OneTimeUseType;
-import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.saml.common.exceptions.ConfigurationException;
-import org.keycloak.saml.common.exceptions.ParsingException;
-import org.keycloak.saml.common.exceptions.ProcessingException;
-import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
-import org.keycloak.testsuite.util.SamlClient;
-import org.w3c.dom.Document;
+import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
+import org.keycloak.testsuite.util.SamlClient.Binding;
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import java.io.Closeable;
+import java.io.IOException;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
-import static org.keycloak.testsuite.util.SamlClient.login;
/**
* KEYCLOAK-4360
@@ -60,38 +57,38 @@ public class IncludeOneTimeUseConditionTest extends AbstractSamlTest
testOneTimeUseConditionIncluded(Boolean.FALSE);
}
- private void testOneTimeUseConditionIncluded(Boolean oneTimeUseConditionShouldBeIncluded) throws ProcessingException, ConfigurationException, ParsingException
+ private void testOneTimeUseConditionIncluded(Boolean oneTimeUseConditionShouldBeIncluded) throws IOException
{
ClientsResource clients = adminClient.realm(REALM_NAME).clients();
List<ClientRepresentation> foundClients = clients.findByClientId(SAML_CLIENT_ID_SALES_POST);
assertThat(foundClients, hasSize(1));
ClientResource clientRes = clients.get(foundClients.get(0).getId());
- ClientRepresentation client = clientRes.toRepresentation();
- client.getAttributes().put(SamlConfigAttributes.SAML_ONETIMEUSE_CONDITION, oneTimeUseConditionShouldBeIncluded.toString());
- clientRes.update(client);
- AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
- loginRep.setProtocolBinding(SamlClient.Binding.POST.getBindingUri());
+ try (Closeable c = new ClientAttributeUpdater(clientRes)
+ .setAttribute(SamlConfigAttributes.SAML_ONETIMEUSE_CONDITION, oneTimeUseConditionShouldBeIncluded.toString())
+ .update()) {
- Document samlRequest = SAML2Request.convert(loginRep);
- SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, SamlClient.Binding.POST,
- SamlClient.Binding.POST);
+ SAMLDocumentHolder res = new SamlClientBuilder()
+ .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, Binding.POST).build()
+ .login().user(bburkeUser).build()
+ .getSamlResponse(Binding.POST);
- assertThat(res.getSamlObject(), notNullValue());
- assertThat(res.getSamlObject(), instanceOf(ResponseType.class));
+ assertThat(res.getSamlObject(), notNullValue());
+ assertThat(res.getSamlObject(), instanceOf(ResponseType.class));
- ResponseType rt = (ResponseType) res.getSamlObject();
- assertThat(rt.getAssertions(), not(empty()));
- final ConditionsType conditionsType = rt.getAssertions().get(0).getAssertion().getConditions();
- assertThat(conditionsType, notNullValue());
- assertThat(conditionsType.getConditions(), not(empty()));
+ ResponseType rt = (ResponseType) res.getSamlObject();
+ assertThat(rt.getAssertions(), not(empty()));
+ final ConditionsType conditionsType = rt.getAssertions().get(0).getAssertion().getConditions();
+ assertThat(conditionsType, notNullValue());
+ assertThat(conditionsType.getConditions(), not(empty()));
- final List<ConditionAbstractType> conditions = conditionsType.getConditions();
+ final List<ConditionAbstractType> conditions = conditionsType.getConditions();
- final Collection<ConditionAbstractType> oneTimeUseConditions = Collections2.filter(conditions, input -> input instanceof OneTimeUseType);
+ final Collection<ConditionAbstractType> oneTimeUseConditions = Collections2.filter(conditions, input -> input instanceof OneTimeUseType);
- final boolean oneTimeUseConditionAdded = !oneTimeUseConditions.isEmpty();
- assertThat(oneTimeUseConditionAdded, is(oneTimeUseConditionShouldBeIncluded));
+ final boolean oneTimeUseConditionAdded = !oneTimeUseConditions.isEmpty();
+ assertThat(oneTimeUseConditionAdded, is(oneTimeUseConditionShouldBeIncluded));
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java
index 7870eba..eb0888b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java
@@ -16,34 +16,27 @@
*/
package org.keycloak.testsuite.saml;
+import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
-import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
+import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.saml.SAML2LogoutRequestBuilder;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
-import org.keycloak.saml.common.exceptions.ConfigurationException;
-import org.keycloak.saml.common.exceptions.ParsingException;
-import org.keycloak.saml.common.exceptions.ProcessingException;
-import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
+import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.testsuite.util.ClientBuilder;
-import org.keycloak.testsuite.util.Matchers;
-import org.keycloak.testsuite.util.SamlClient;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilderException;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.client.protocol.HttpClientContext;
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.xml.transform.dom.DOMSource;
import org.junit.Before;
import org.junit.Test;
-import org.w3c.dom.Document;
import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.util.Matchers.*;
import static org.keycloak.testsuite.util.SamlClient.Binding.*;
@@ -57,7 +50,8 @@ public class LogoutTest extends AbstractSamlTest {
private ClientRepresentation salesRep;
private ClientRepresentation sales2Rep;
- private SamlClient samlClient;
+ private final AtomicReference<NameIDType> nameIdRef = new AtomicReference<>();
+ private final AtomicReference<String> sessionIndexRef = new AtomicReference<>();
@Before
public void setup() {
@@ -71,7 +65,8 @@ public class LogoutTest extends AbstractSamlTest {
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "http://url")
.build());
- samlClient = new SamlClient(getAuthServerSamlEndpoint(REALM_NAME));
+ nameIdRef.set(null);
+ sessionIndexRef.set(null);
}
@Override
@@ -79,186 +74,192 @@ public class LogoutTest extends AbstractSamlTest {
return true;
}
- private Document prepareLogoutFromSalesAfterLoggingIntoTwoApps() throws ParsingException, IllegalArgumentException, UriBuilderException, ConfigurationException, ProcessingException {
- AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
- Document doc = SAML2Request.convert(loginRep);
- SAMLDocumentHolder resp = samlClient.login(bburkeUser, doc, null, POST, POST, false, true);
- assertThat(resp.getSamlObject(), isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
- ResponseType loginResp1 = (ResponseType) resp.getSamlObject();
-
- loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, REALM_NAME);
- doc = SAML2Request.convert(loginRep);
- resp = samlClient.subsequentLoginViaSSO(doc, null, POST, POST);
- assertThat(resp.getSamlObject(), isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
- ResponseType loginResp2 = (ResponseType) resp.getSamlObject();
-
- AssertionType firstAssertion = loginResp1.getAssertions().get(0).getAssertion();
- assertThat(firstAssertion.getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class));
- NameIDType nameId = (NameIDType) firstAssertion.getSubject().getSubType().getBaseID();
- AuthnStatementType firstAssertionStatement = (AuthnStatementType) firstAssertion.getStatements().iterator().next();
-
- return new SAML2LogoutRequestBuilder()
- .destination(getAuthServerSamlEndpoint(REALM_NAME).toString())
- .issuer(SAML_CLIENT_ID_SALES_POST)
- .sessionIndex(firstAssertionStatement.getSessionIndex())
- .userPrincipal(nameId.getValue(), nameId.getFormat().toString())
- .buildDocument();
+ private SamlClientBuilder prepareLogIntoTwoApps() {
+ return new SamlClientBuilder()
+ .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST).build()
+ .login().user(bburkeUser).build()
+ .processSamlResponse(POST).transformObject(so -> {
+ assertThat(so, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
+ ResponseType loginResp1 = (ResponseType) so;
+ final AssertionType firstAssertion = loginResp1.getAssertions().get(0).getAssertion();
+ assertThat(firstAssertion, org.hamcrest.Matchers.notNullValue());
+ assertThat(firstAssertion.getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class));
+
+ NameIDType nameId = (NameIDType) firstAssertion.getSubject().getSubType().getBaseID();
+ AuthnStatementType firstAssertionStatement = (AuthnStatementType) firstAssertion.getStatements().iterator().next();
+
+ nameIdRef.set(nameId);
+ sessionIndexRef.set(firstAssertionStatement.getSessionIndex());
+ return null; // Do not follow the redirect to the app from the returned response
+ }).build()
+
+ .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, POST).build()
+ .login().sso(true).build() // This is a formal step
+ .processSamlResponse(POST).transformObject(so -> {
+ assertThat(so, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
+ return null; // Do not follow the redirect to the app from the returned response
+ }).build();
}
@Test
- public void testLogoutInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+ public void testLogoutDifferentBrowser() {
+ // This is in fact the same as admin logging out a session from admin console.
+ // This always succeeds as it is essentially the same as backend logout which
+ // does not report errors to client but only to the server log
adminClient.realm(REALM_NAME)
.clients().get(sales2Rep.getId())
.update(ClientBuilder.edit(sales2Rep)
.frontchannelLogout(false)
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
+ .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
.build());
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+ SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+ .clearCookies()
+
+ .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
+ .nameId(nameIdRef::get)
+ .sessionIndex(sessionIndexRef::get)
+ .build()
- samlClient.logout(logoutDoc, null, POST, POST);
+ .getSamlResponse(POST);
+
+ assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
}
@Test
- public void testLogoutDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException {
- // This is in fact the same as admin logging out a session from admin console.
- // This always succeeds as it is essentially the same as backend logout which
- // does not report errors to client but only to the server log
+ public void testFrontchannelLogoutInSameBrowser() {
adminClient.realm(REALM_NAME)
.clients().get(sales2Rep.getId())
.update(ClientBuilder.edit(sales2Rep)
- .frontchannelLogout(false)
+ .frontchannelLogout(true)
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
.build());
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+ SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+ .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
+ .nameId(nameIdRef::get)
+ .sessionIndex(sessionIndexRef::get)
+ .build()
- samlClient.execute((client, context, strategy) -> {
- HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
- CloseableHttpResponse response = client.execute(post, HttpClientContext.create());
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- return response;
- });
+ .getSamlResponse(POST);
+
+ assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
}
@Test
- public void testFrontchannelLogoutInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+ public void testFrontchannelLogoutNoLogoutServiceUrlSetInSameBrowser() {
adminClient.realm(REALM_NAME)
.clients().get(sales2Rep.getId())
.update(ClientBuilder.edit(sales2Rep)
.frontchannelLogout(true)
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
+ .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "")
.build());
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+ SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+ .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
+ .nameId(nameIdRef::get)
+ .sessionIndex(sessionIndexRef::get)
+ .build()
+
+ .getSamlResponse(POST);
- samlClient.execute((client, context, strategy) -> {
- HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
- CloseableHttpResponse response = client.execute(post, context);
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- return response;
- });
+ assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
}
@Test
- public void testFrontchannelLogoutNoLogoutServiceUrlSetInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+ public void testFrontchannelLogoutDifferentBrowser() {
adminClient.realm(REALM_NAME)
.clients().get(sales2Rep.getId())
.update(ClientBuilder.edit(sales2Rep)
.frontchannelLogout(true)
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE)
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
+ .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
.build());
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+ SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+ .clearCookies()
+
+ .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
+ .nameId(nameIdRef::get)
+ .sessionIndex(sessionIndexRef::get)
+ .build()
+
+ .getSamlResponse(POST);
- samlClient.execute((client, context, strategy) -> {
- HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
- CloseableHttpResponse response = client.execute(post, context);
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- return response;
- });
+ assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
}
@Test
- public void testFrontchannelLogoutDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+ public void testFrontchannelLogoutWithRedirectUrlDifferentBrowser() {
adminClient.realm(REALM_NAME)
- .clients().get(sales2Rep.getId())
- .update(ClientBuilder.edit(sales2Rep)
+ .clients().get(salesRep.getId())
+ .update(ClientBuilder.edit(salesRep)
.frontchannelLogout(true)
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
+ .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url")
.build());
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
-
- samlClient.execute((client, context, strategy) -> {
- HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
- CloseableHttpResponse response = client.execute(post, HttpClientContext.create());
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- return response;
- });
- }
-
- @Test
- public void testFrontchannelLogoutWithRedirectUrlDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException {
adminClient.realm(REALM_NAME)
.clients().get(sales2Rep.getId())
.update(ClientBuilder.edit(sales2Rep)
.frontchannelLogout(true)
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE)
- .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url")
+ .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
+ .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "")
.build());
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+ SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+ .clearCookies()
- samlClient.execute((client, context, strategy) -> {
- HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
- CloseableHttpResponse response = client.execute(post, HttpClientContext.create());
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- return response;
- });
+ .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, REDIRECT)
+ .nameId(nameIdRef::get)
+ .sessionIndex(sessionIndexRef::get)
+ .build()
+
+ .getSamlResponse(REDIRECT);
+
+ assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
}
@Test
- public void testLogoutWithPostBindingUnsetRedirectBindingSet() throws ParsingException, ConfigurationException, ProcessingException {
+ public void testLogoutWithPostBindingUnsetRedirectBindingSet() {
// https://issues.jboss.org/browse/KEYCLOAK-4779
adminClient.realm(REALM_NAME)
.clients().get(sales2Rep.getId())
.update(ClientBuilder.edit(sales2Rep)
.frontchannelLogout(true)
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
- .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url")
+ .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url-to-sales-2")
.build());
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
-
- SAMLDocumentHolder resp = samlClient.getSamlResponse(REDIRECT, (client, context, strategy) -> {
- strategy.setRedirectable(false);
- HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
- return client.execute(post, context);
- });
-
- // Expect logout request for sales-post2
- assertThat(resp.getSamlObject(), isSamlLogoutRequest("http://url"));
- Document logoutRespDoc = new SAML2LogoutResponseBuilder()
- .destination(getAuthServerSamlEndpoint(REALM_NAME).toString())
- .issuer(SAML_CLIENT_ID_SALES_POST2)
- .logoutRequestID(((LogoutRequestType) resp.getSamlObject()).getID())
- .buildDocument();
-
- // Emulate successful logout response from sales-post2 logout
- resp = samlClient.getSamlResponse(POST, (client, context, strategy) -> {
- strategy.setRedirectable(false);
- HttpUriRequest post = POST.createSamlUnsignedResponse(getAuthServerSamlEndpoint(REALM_NAME), null, logoutRespDoc);
- return client.execute(post, context);
- });
+ SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+ .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
+ .nameId(nameIdRef::get)
+ .sessionIndex(sessionIndexRef::get)
+ .build()
+
+ .processSamlResponse(REDIRECT)
+ .transformDocument(doc -> {
+ // Expect logout request for sales-post2
+ SAML2Object so = (SAML2Object) new SAMLParser().parse(new DOMSource(doc));
+ assertThat(so, isSamlLogoutRequest("http://url-to-sales-2"));
+
+ // Emulate successful logout response from sales-post2 logout
+ return new SAML2LogoutResponseBuilder()
+ .destination(getAuthServerSamlEndpoint(REALM_NAME).toString())
+ .issuer(SAML_CLIENT_ID_SALES_POST2)
+ .logoutRequestID(((LogoutRequestType) so).getID())
+ .buildDocument();
+ })
+ .targetAttributeSamlResponse()
+ .targetUri(getAuthServerSamlEndpoint(REALM_NAME))
+ .build()
+
+ .getSamlResponse(POST);
// Expect final successful logout response from auth server signalling final successful logout
- assertThat(resp.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
+ assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
+ assertThat(((StatusResponseType) samlResponse.getSamlObject()).getDestination(), is("http://url"));
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java
index 3fcf0c3..bd30eea 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java
@@ -9,16 +9,15 @@ import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.IOUtil;
-import org.keycloak.testsuite.util.SamlClient;
-import java.net.URI;
+import org.keycloak.testsuite.util.SamlClient.Binding;
+import org.keycloak.testsuite.util.SamlClientBuilder;
import java.util.List;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.util.IOUtil.loadRealm;
-import static org.keycloak.testsuite.util.SamlClient.idpInitiatedLoginWithRequiredConsent;
/**
* @author mhajas
@@ -48,13 +47,17 @@ public class SamlConsentTest extends AbstractSamlTest {
.build());
log.debug("Log in using idp initiated login");
- String idpInitiatedLogin = getAuthServerRoot() + "realms/" + REALM_NAME + "/protocol/saml/clients/sales-post-enc";
- SAMLDocumentHolder documentHolder = idpInitiatedLoginWithRequiredConsent(bburkeUser, URI.create(idpInitiatedLogin), SamlClient.Binding.POST, false);
-
- assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString("<dsig:Signature")); // KEYCLOAK-4262
- assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), not(containsString("<samlp:LogoutResponse"))); // KEYCLOAK-4261
- assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString("<samlp:Response")); // KEYCLOAK-4261
- assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString("<samlp:Status")); // KEYCLOAK-4181
- assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString("<samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:RequestDenied\"")); // KEYCLOAK-4181
+ SAMLDocumentHolder documentHolder = new SamlClientBuilder()
+ .idpInitiatedLogin(getAuthServerSamlEndpoint(REALM_NAME), "sales-post-enc").build()
+ .login().user(bburkeUser).build()
+ .consentRequired().approveConsent(false).build()
+ .getSamlResponse(Binding.POST);
+
+ final String samlDocumentString = IOUtil.documentToString(documentHolder.getSamlDocument());
+ assertThat(samlDocumentString, containsString("<dsig:Signature")); // KEYCLOAK-4262
+ assertThat(samlDocumentString, not(containsString("<samlp:LogoutResponse"))); // KEYCLOAK-4261
+ assertThat(samlDocumentString, containsString("<samlp:Response")); // KEYCLOAK-4261
+ assertThat(samlDocumentString, containsString("<samlp:Status")); // KEYCLOAK-4181
+ assertThat(samlDocumentString, containsString("<samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:RequestDenied\"")); // KEYCLOAK-4181
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/LastSessionRefreshUnitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/LastSessionRefreshUnitTest.java
new file mode 100644
index 0000000..5b8c559
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/LastSessionRefreshUnitTest.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.session;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.infinispan.Cache;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.common.util.Time;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStore;
+import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStoreFactory;
+import org.keycloak.models.sessions.infinispan.changes.sessions.SessionData;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.Retry;
+import org.keycloak.testsuite.runonserver.RunOnServer;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LastSessionRefreshUnitTest extends AbstractKeycloakTest {
+
+ @Deployment
+ public static WebArchive deploy() {
+ return RunOnServerDeployment.create(UserResource.class)
+ .addPackages(true, "org.keycloak.testsuite");
+ }
+
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+
+ }
+
+
+ @Test
+ public void testLastSessionRefreshCounters() {
+ testingClient.server().run(new LastSessionRefreshServerCounterTest());
+ }
+
+ public static class LastSessionRefreshServerCounterTest extends LastSessionRefreshServerTest {
+
+
+ @Override
+ public void run(KeycloakSession session) {
+ LastSessionRefreshStore customStore = createStoreInstance(session, 1000000, 1000);
+ System.out.println("sss");
+
+ int lastSessionRefresh = Time.currentTime();
+
+ // Add 8 items. No message
+ for (int i=0 ; i<8 ; i++){
+ customStore.putLastSessionRefresh(session, "session-" + i, "master", lastSessionRefresh);
+ }
+ Assert.assertEquals(0, counter.get());
+
+ // Add 2 other items. Message sent now due the maxCount is 10
+ for (int i=8 ; i<10 ; i++){
+ customStore.putLastSessionRefresh(session, "session-" + i, "master", lastSessionRefresh);
+ }
+ Assert.assertEquals(1, counter.get());
+
+ // Add 5 items. No additional message
+ for (int i=10 ; i<15 ; i++){
+ customStore.putLastSessionRefresh(session, "session-" + i, "master", lastSessionRefresh);
+ }
+ Assert.assertEquals(1, counter.get());
+
+ // Add 20 items. 2 additional messages
+ for (int i=15 ; i<35 ; i++){
+ customStore.putLastSessionRefresh(session, "session-" + i, "master", lastSessionRefresh);
+ }
+ Assert.assertEquals(3, counter.get());
+
+ }
+
+ }
+
+
+ @Test
+ public void testLastSessionRefreshIntervals() {
+ testingClient.server().run(new LastSessionRefreshServerIntervalsTest());
+ }
+
+ public static class LastSessionRefreshServerIntervalsTest extends LastSessionRefreshServerTest {
+
+ @Override
+ public void run(KeycloakSession session) {
+ // Long timer interval. No message due the timer wasn't executed
+ LastSessionRefreshStore customStore1 = createStoreInstance(session, 100000, 10);
+ Time.setOffset(100);
+
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException ie) {
+ throw new RuntimeException();
+ }
+ Assert.assertEquals(0, counter.get());
+
+ // Short timer interval 10 ms. 1 message due the interval is executed and lastRun was in the past due to Time.setOffset
+ LastSessionRefreshStore customStore2 = createStoreInstance(session, 10, 10);
+ Time.setOffset(200);
+
+ Retry.execute(() -> {
+ Assert.assertEquals(1, counter.get());
+ }, 100, 10);
+
+ Assert.assertEquals(1, counter.get());
+
+ // Another sleep won't send message. lastRun was updated
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException ie) {
+ throw new RuntimeException();
+ }
+ Assert.assertEquals(1, counter.get());
+
+
+ Time.setOffset(0);
+ }
+
+ }
+
+
+ public static abstract class LastSessionRefreshServerTest implements RunOnServer {
+
+ AtomicInteger counter = new AtomicInteger();
+
+ LastSessionRefreshStore createStoreInstance(KeycloakSession session, long timerIntervalMs, int maxIntervalBetweenMessagesSeconds) {
+ LastSessionRefreshStoreFactory factory = new LastSessionRefreshStoreFactory() {
+
+ @Override
+ protected LastSessionRefreshStore createStoreInstance(int maxIntervalBetweenMessagesSeconds, int maxCount, String eventKey) {
+ return new LastSessionRefreshStore(maxIntervalBetweenMessagesSeconds, maxCount, eventKey) {
+
+ @Override
+ protected void sendMessage(KeycloakSession kcSession, Map<String, SessionData> refreshesToSend) {
+ counter.incrementAndGet();
+ }
+
+ };
+ }
+
+ };
+
+ Cache<String, SessionEntityWrapper> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+ return factory.createAndInit(session, cache, timerIntervalMs, maxIntervalBetweenMessagesSeconds, 10, false);
+ }
+
+ }
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserManager.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserManager.java
index ed797b5..6bd3789 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserManager.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserManager.java
@@ -61,6 +61,12 @@ public class UserManager {
userResource.update(user);
}
+ public void enabled(Boolean enabled) {
+ UserRepresentation user = userResource.toRepresentation();
+ user.setEnabled(enabled);
+ userResource.update(user);
+ }
+
private UserRepresentation initializeRequiredActions() {
UserRepresentation user = userResource.toRepresentation();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json
index e190129..aed0231 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json
@@ -8,6 +8,7 @@
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
+ "passwordPolicy": "hashIterations(1)",
"defaultRoles": [ "user" ],
"smtpServer": {
"from": "auto@keycloak.org",
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
index 58ef272..acf153c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
@@ -50,7 +50,7 @@
<container qualifier="auth-server-undertow" mode="suite" >
<configuration>
<property name="enabled">${auth.server.undertow} && ! ${auth.server.undertow.crossdc}</property>
- <property name="bindAddress">localhost</property>
+ <property name="bindAddress">0.0.0.0</property>
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
<property name="bindHttpPort">${auth.server.http.port}</property>
<property name="remoteMode">${undertow.remote}</property>
@@ -68,6 +68,8 @@
<property name="jbossArguments">
-Djboss.socket.binding.port-offset=${auth.server.port.offset}
-Djboss.bind.address=0.0.0.0
+ -Dauth.server.http.port=${auth.server.http.port}
+ -Dauth.server.https.port=${auth.server.https.port}
${adapter.test.props}
${migration.import.properties}
${auth.server.profile}
@@ -172,7 +174,7 @@
<group qualifier="auth-server-undertow-cross-dc">
<container qualifier="cache-server-cross-dc-1" mode="suite" >
<configuration>
- <property name="enabled">${auth.server.undertow.crossdc}</property>
+ <property name="enabled">${auth.server.undertow.crossdc} && ! ${cache.server.lifecycle.skip}</property>
<property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
<property name="jbossHome">${cache.server.home}</property>
<property name="serverConfig">clustered.xml</property>
@@ -195,7 +197,7 @@
<container qualifier="cache-server-cross-dc-2" mode="suite" >
<configuration>
- <property name="enabled">${auth.server.undertow.crossdc}</property>
+ <property name="enabled">${auth.server.undertow.crossdc} && ! ${cache.server.lifecycle.skip}</property>
<property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
<property name="jbossHome">${cache.server.home}</property>
<property name="setupCleanServerBaseDir">true</property>
@@ -240,6 +242,7 @@
<property name="dataCenter">0</property>
<property name="keycloakConfigPropertyOverrides">{
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.1",
+ "keycloak.connectionsInfinispan.siteName": "dc-0",
"keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0_1",
"keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
"keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
@@ -263,6 +266,7 @@
<property name="dataCenter">0</property>
<property name="keycloakConfigPropertyOverrides">{
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.1",
+ "keycloak.connectionsInfinispan.siteName": "dc-0",
"keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0_2-manual",
"keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
"keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
@@ -287,6 +291,7 @@
<property name="dataCenter">1</property>
<property name="keycloakConfigPropertyOverrides">{
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.2",
+ "keycloak.connectionsInfinispan.siteName": "dc-1",
"keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1_1",
"keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
"keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
@@ -310,6 +315,7 @@
<property name="dataCenter">1</property>
<property name="keycloakConfigPropertyOverrides">{
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.2",
+ "keycloak.connectionsInfinispan.siteName": "dc-1",
"keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1_2-manual",
"keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
"keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/client-with-authz-settings.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/client-with-authz-settings.json
new file mode 100644
index 0000000..ccc3ccc
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/client-with-authz-settings.json
@@ -0,0 +1,866 @@
+{
+ "clientId": "authz-client",
+ "enabled": true,
+ "publicClient": false,
+ "secret": "secret",
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": true,
+ "authorizationServicesEnabled": true,
+ "redirectUris": [
+ "http://localhost/authz-client/*"
+ ],
+ "webOrigins": [
+ "http://localhost"
+ ],
+ "authorizationSettings": {
+ "allowRemoteResourceManagement": true,
+ "policyEnforcementMode": "PERMISSIVE",
+ "resources": [
+ {
+ "name": "Default Resource",
+ "uri": "/*",
+ "type": "urn:authz-client:resources:default"
+ },
+ {
+ "name": "Resource 1",
+ "uri": "/protected/resource/1",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 2",
+ "uri": "/protected/resource/2",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 3",
+ "uri": "/protected/resource/3",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 4",
+ "uri": "/protected/resource/4",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 5",
+ "uri": "/protected/resource/5",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 6",
+ "uri": "/protected/resource/6",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 7",
+ "uri": "/protected/resource/7",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 8",
+ "uri": "/protected/resource/8",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 9",
+ "uri": "/protected/resource/9",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 10",
+ "uri": "/protected/resource/10",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 11",
+ "uri": "/protected/resource/11",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 12",
+ "uri": "/protected/resource/12",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 13",
+ "uri": "/protected/resource/13",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 14",
+ "uri": "/protected/resource/14",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 15",
+ "uri": "/protected/resource/15",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 16",
+ "uri": "/protected/resource/16",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 17",
+ "uri": "/protected/resource/17",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 18",
+ "uri": "/protected/resource/18",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 19",
+ "uri": "/protected/resource/19",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ },
+ {
+ "name": "Resource 20",
+ "uri": "/protected/resource/20",
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ }
+ ],
+ "policies": [
+ {
+ "name": "Default Policy",
+ "description": "A policy that grants access only for users within this realm",
+ "type": "js",
+ "logic": "POSITIVE",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n"
+ }
+ },
+ {
+ "name": "Resource 1 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 2 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 3 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 4 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 5 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 6 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 7 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 8 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 9 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 10 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 11 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 12 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 13 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 14 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 15 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 16 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 17 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 18 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 19 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Resource 20 Policy",
+ "type": "role",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]"
+ }
+ },
+ {
+ "name": "Default Permission",
+ "description": "A permission that applies to the default resource type",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "defaultResourceType": "urn:authz-client:resources:default",
+ "applyPolicies": "[\"Default Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 1 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 1\"]",
+ "applyPolicies": "[\"Resource 1 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 2 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 2\"]",
+ "applyPolicies": "[\"Resource 2 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 3 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 3\"]",
+ "applyPolicies": "[\"Resource 3 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 4 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 4\"]",
+ "applyPolicies": "[\"Resource 4 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 5 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 5\"]",
+ "applyPolicies": "[\"Resource 5 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 6 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 6\"]",
+ "applyPolicies": "[\"Resource 6 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 7 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 7\"]",
+ "applyPolicies": "[\"Resource 7 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 8 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 8\"]",
+ "applyPolicies": "[\"Resource 8 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 9 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 9\"]",
+ "applyPolicies": "[\"Resource 9 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 10 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 10\"]",
+ "applyPolicies": "[\"Resource 10 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 11 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 11\"]",
+ "applyPolicies": "[\"Resource 11 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 12 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 12\"]",
+ "applyPolicies": "[\"Resource 12 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 13 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 13\"]",
+ "applyPolicies": "[\"Resource 13 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 14 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 14\"]",
+ "applyPolicies": "[\"Resource 14 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 15 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 15\"]",
+ "applyPolicies": "[\"Resource 15 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 16 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 16\"]",
+ "applyPolicies": "[\"Resource 16 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 17 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 17\"]",
+ "applyPolicies": "[\"Resource 17 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 18 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 18\"]",
+ "applyPolicies": "[\"Resource 18 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 19 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 19\"]",
+ "applyPolicies": "[\"Resource 19 Policy\"]"
+ }
+ },
+ {
+ "name": "Resource 20 Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource 20\"]",
+ "applyPolicies": "[\"Resource 20 Policy\"]"
+ }
+ }
+ ],
+ "scopes": [
+ {
+ "name": "Scope B"
+ },
+ {
+ "name": "Scope A"
+ },
+ {
+ "name": "Scope D"
+ },
+ {
+ "name": "Scope C"
+ },
+ {
+ "name": "Scope E"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/docker-realm-public-key.pem b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/docker-realm-public-key.pem
new file mode 100644
index 0000000..a7493f1
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/docker-realm-public-key.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICsTCCAZkCBgFbaSTAdjANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFkb2Nr
+ZXItdGVzdC1yZWFsbTAeFw0xNzA0MTMyMTA2MDdaFw0yNzA0MTMyMTA3NDdaMBwx
+GjAYBgNVBAMMEWRvY2tlci10ZXN0LXJlYWxtMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAk2ZfvP3znNH5EbBd6ckiT7Eq7loqBCa5o6fdOajD2X8cjT7r
+oLG4GANhu075SUrCxfcx2A+P1kBnSsyPCc3dxMmCT7BUJsYScCF88q52GIskQc7E
++eBkuIjeVmPMECLq3xhY7YONqIl47n17dEYYmVo1uRqbrVSFdSX9EDqn9vRn/7uJ
+FLafdK9766Na2JMSZVKgnNsXRTtxxCjnU3LyMnNw5JdbnsfSPj1pgnOi+pTDPqlw
+fcAIaG72lmhWMXaStmwO1DYsBoUd4yEnv6/dtXQkAaDr6TthX7ITliaxXPrh+YMD
+AxnhV7X/PtbiFUpTaNBpSy3k87onYBiWrL44IQIDAQABMA0GCSqGSIb3DQEBCwUA
+A4IBAQB2u9hP3S1bP4+FBwOLPwI3p7WrWBlt2CgwTiyuXvV7u9GLiXqCDUWZd3dS
+ks9vU4Y4NdVyToY4q9YFJ3oAQXlfRw2Yi6e/0nSPpU25o52TWwREnRY98fjVy1eC
+5K2GRwSu79HZKeqA0Tg/ONvGOrlYO1KPbWZGg9NcwAGeILkNdfI82w0KZTpTy+f5
+ATtV30pFkDNT0gfayFmDQvw3EgcD/x0/vI3PlnHLLGprV/ZlBmFWo0vk8iUBwP1Y
+bTA0XqKasITFXJaPeZWzNMCjR1NxDqlIq095uX04E5XGS6XGJKS9PanvGXidk5xM
+gI7xwKE6jaxD9pspYPRgv66528Dc
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt
new file mode 100644
index 0000000..6b50a04
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIGBTCCA+2gAwIBAgIJALfo8UyCLlnkMA0GCSqGSIb3DQEBCwUAMIGYMQswCQYD
+VQQGEwJVUzEXMBUGA1UECAwOTm9ydGggQ2Fyb2xpbmExEDAOBgNVBAcMB1JhbGVp
+Z2gxFjAUBgNVBAoMDVJlZCBIYXQsIEluYy4xJzAlBgNVBAsMHklkZW50aXR5IGFu
+ZCBBY2Nlc3MgTWFuYWdlbWVudDEdMBsGA1UEAwwUcmVnaXN0cnkubG9jYWxkb21h
+aW4wHhcNMTcwNDIwMDMwNzMwWhcNMjAwMTE0MDMwNzMwWjCBmDELMAkGA1UEBhMC
+VVMxFzAVBgNVBAgMDk5vcnRoIENhcm9saW5hMRAwDgYDVQQHDAdSYWxlaWdoMRYw
+FAYDVQQKDA1SZWQgSGF0LCBJbmMuMScwJQYDVQQLDB5JZGVudGl0eSBhbmQgQWNj
+ZXNzIE1hbmFnZW1lbnQxHTAbBgNVBAMMFHJlZ2lzdHJ5LmxvY2FsZG9tYWluMIIC
+IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyIKYO7gYA9T8PpqTf2Lad81X
+cHzhiRYvvzUDgR4UD1NummWPnl2sPjdlQayM/TZ7p6gserdLjms336tvU/6GOIjv
+v10uvDsFVxafuASY1tQSlrFLwF2NwavVOWlPhdlYLvOUnT/zk7fWKRFy7WXp6hD5
+RAkI4+ywuhS6eiZy3wIv/04VjFGYAB1x3NfHVwSuo+cjz/UvI3sU1i0LR+aOSRoP
+9GM8OBpaTxRu/vEHd3k0A2FLP3sJYzkSD6A0p+nqbMfrPKRuZEjDYvBad4KemAl2
+5GRxNeZkJUk0CX2QK2cqr6xOa7598Nr+3ejv99Iiga5r2VlSSdsbV3U9j3RoZY48
+J0RvSgsVeeYqE93SUsVKhSoN4UIdhiVoDCvLtuIeqfQjehowent03OwDUiYw0TeV
+GqmcN54Ki6v+EWSNqY2h01wcbMuQw6PDQ/mn1pz7f/ZAt9T0fop6ml4Mg4nud9S9
+b/Y9+XfuJlPKwZIgQEtrpSfLveOBmWYRu9/rSX9YtHx+pyzbWDtwrF0O9Z/pO+T4
+qOMmfc2ltjzRMFKK6JZFhFVHQP0AKsxLChQrzoHr5k7Rmcn+iGtmqD4tWtzgEQvA
+umhNsm4nrR92hB97yxw3WC9gGvJlBIi/swrCxiKCJDklxCZtVCmqwMFx/bzXu3pH
+sKwYv3poURR9NZb7kDcCAwEAAaNQME4wHQYDVR0OBBYEFNhH71tQSivnjfCHd7pt
+3Qo50DCZMB8GA1UdIwQYMBaAFNhH71tQSivnjfCHd7pt3Qo50DCZMAwGA1UdEwQF
+MAMBAf8wDQYJKoZIhvcNAQELBQADggIBAGSCDF/l/ExabQ1DfoKoRCmVoslnK+M1
+0TuDtfss2zqF89BPLBNBKdfp7r1OV4fp465HMpd2ovUkuijLjrIf78+I4AFEv60s
+Z7NKMYEULpvBZ3RY7INr9CoNcWGvnfC/h782axjyI6ZW6I2v717FcciI6su0Eg+k
+kF6+c+cVLmhKLi7hnC9mlN0JMUcOt3cBuZ8NvCHwW6VFmv8hsxt8Z18JcY6aPZE8
+32XzdgcU/U9OAhv1iMEuoGAqQatCHAmA3FOpfI9LjVOxW0LZgHWKX7OEyDEZ+7Ed
+DbEpD73bmTp89lvFcT0UEAcWkRpD+VSozgYEzSeNmzKks2ngl37SlG2YQ23UzgYS
+alGcUEJFBmWr9pJUN+tDPzbtmlrEw9pA6xYZMTDgAQSRHGQK/5lISuzEIMR0nh3q
+Hyhmamlg+zkF415gYKUwh96NgalIc+Y9B4vnSpOv7b+ZFXoubBD2Wk5oi0Ziyog0
+J8YcbLQ8ZhINRvDyNv0iWHNachIzO1/N5G5H8hjibLkH+tpFBSs3uCiwTi+L/MlD
+Pqc0A6Slyi8TnJJDFCDaa3xU321dkvyhGmPeqiyIK+dpJO1FI3OU0rZeGGcyc+K6
+SnDRByp0HQt9W/8Aw+kXjUoI8LOYeR/7Ctd+Tqf11TDxmw9w9LSIEhiYeEJQCxTc
+Dk72PkeTi1zO
+-----END CERTIFICATE-----
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.key b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.key
new file mode 100644
index 0000000..22a3986
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKQIBAAKCAgEAyIKYO7gYA9T8PpqTf2Lad81XcHzhiRYvvzUDgR4UD1NummWP
+nl2sPjdlQayM/TZ7p6gserdLjms336tvU/6GOIjvv10uvDsFVxafuASY1tQSlrFL
+wF2NwavVOWlPhdlYLvOUnT/zk7fWKRFy7WXp6hD5RAkI4+ywuhS6eiZy3wIv/04V
+jFGYAB1x3NfHVwSuo+cjz/UvI3sU1i0LR+aOSRoP9GM8OBpaTxRu/vEHd3k0A2FL
+P3sJYzkSD6A0p+nqbMfrPKRuZEjDYvBad4KemAl25GRxNeZkJUk0CX2QK2cqr6xO
+a7598Nr+3ejv99Iiga5r2VlSSdsbV3U9j3RoZY48J0RvSgsVeeYqE93SUsVKhSoN
+4UIdhiVoDCvLtuIeqfQjehowent03OwDUiYw0TeVGqmcN54Ki6v+EWSNqY2h01wc
+bMuQw6PDQ/mn1pz7f/ZAt9T0fop6ml4Mg4nud9S9b/Y9+XfuJlPKwZIgQEtrpSfL
+veOBmWYRu9/rSX9YtHx+pyzbWDtwrF0O9Z/pO+T4qOMmfc2ltjzRMFKK6JZFhFVH
+QP0AKsxLChQrzoHr5k7Rmcn+iGtmqD4tWtzgEQvAumhNsm4nrR92hB97yxw3WC9g
+GvJlBIi/swrCxiKCJDklxCZtVCmqwMFx/bzXu3pHsKwYv3poURR9NZb7kDcCAwEA
+AQKCAgEAsPuM0dGZ6O/7QmsAXEVuHqbyUkj4bh9WP8jUcgiRnkF/c+rHTPrTyQru
+Znye6fZISWFI+XyGxYvgAp54osQbxxUfwWLHmL/j484FZtEv8xe33Klb+szZDiTV
+DVrmJXgFvVOlTvOe1TlEYHWVYvQ89yzKSIJNBZnrGCSpwJ3lcPCmWwyaOoPezeMv
+mMYhnq50VBn2Y13AoOnIJ5AUz/8yglXt1UIuajrgkcKwgnlPpOYnwgAEAmFglONQ
+DNjVAY2YLTJ9ccaV5hDP3anXwHtb70kTV19NCk11AfBObT4Wniju5acKhVHcKley
+9T7haXZinOLPMUcFOkmbJaRHlTMj3UgnF4k2iJJ7NyY3lAAIedlZ3EFNwpa68Roo
+WClNAJIV6KYRExOZfqeRyR09loTnynPgxkMR4N4oLJHCiTtReXW5Y1HAYbT+iVHC
+Ox1ob/INuZ1VoumDfn6bRqFdK8LldjBwVqRecSad/dg84BtjTB/po81aUpSRENEV
+aZP+jOT9kZbybACh8FdF8u7mxgL+x7Xidng3SKRJi5whQJNmQ62QkzTFMPVXCqlO
+ABsz2a/Zw7swyetg9uApoTTCeK1P0V/MrcEVTIGmcABfBYAVMBj1S2SH1xgAr20P
+IR3SOpPtiNYhIIOnfyQQ3qVudsaSOAJH26I7QLnMyBqOId0Js9ECggEBAOSrGSfT
+bm7OhGu1ZcTmlS17kjsUUYn1Uy30vV5e7uhpQGmr4rKVWYkNeZa5qtJossY3z+4H
+9fZAqJWH2Cr/4pqnfz4GqK+qE56fFdbyHzHKLZOXZGdp9fQzlLsEi9JVYgv+nAPR
+MHS7WeMTUlFc+P3pP6Btyhk/x7YfZnnlatFYlsNJVzUVdblrG6wSVZGpmxcNIeM2
+UeGG78aDBZQdKUO+xuh6MFW20lU165QC1JfGE+NRawqvgSD09F3MGkEwJuD8XEBg
+/rOwNUg8/ayQhd1EgRGQOiDgqfXSpsF101HPUSX/HDC41KG3gTKTc/Vw+ac5ID1r
+b3PKExEXCicDgCkCggEBAOB55eVsRZHBHeBjhqemH8SxWUfSCbx17cGbs7sw95Rs
+3wYci7ABC8wbvG5UDNPd3BI2IV5bJWYOlbVv+Y1FjNHamQjiSXgB3g6RzvaM0bVP
+1Rvn7EvQF87XIKEdo3uHtvpSVBDHYq/DtDyE9wwaNctxBgJwThVXVYINsp+leGsD
+uGVMAsUP01vMNdHJBk/ANPvYxUkDOCtlDDV8cyaFVJAq4/A1h4crv39S/6ZY/RWo
+LQpYnA47pfKZzxvtDQsnVTmolQ8x4yAX5bQrpKAt/hIJhzKdeCglgVr9cq/7sNOO
+kDLZzPLlFPRX1gOHTpDlucNxxlIjPh2h+3CCCPUzGV8CggEAYGmDgbczqKSKUJ96
++Tn/S93+GcrHVlOJbqbx8Qg10ugNsIA4ZPNzfMWhrls6GtzqA4kkskfI/LrmWaWd
+DwQ0luBoVc6Y8PfUrdyFaMtNO8Dy1nfObYvPl9bnrrKMAXLelBAV18YrmAwmKgfL
+fWKl2OivWwTvYRXzLmau3lZMY1fmuRADJO6XZEY0tKhGS9Qm/+EZmKMeguhR0HEN
+uRVSgK2/T+W0227p3+OMICvRVuy9FesOJsM4vpyJK8MSjsmums3MV5iNy1VQIdUV
+X9zPlCt9/9m/qH0RLARVKtxy7Ntsa4jUafaEMGseniRtj97CZC9B2KOjqj5ZK6t7
+LFfdgQKCAQEAtu6gC3dQupdGYba55aXb/c8Jkx34ET2JpF3e+o3NNYgDuFdK/wPb
+OVrhFIgqa/5BehXi26IruB/qoRG/rQEg4WPjkvnWJZZgAD+TChl4TOniIfu+9Yl/
+3XAzhxlAQUs4MoclOwdBxTsXhrpVGefCLyjMXPBosbuaU4IWL0QJ/ivp+aMYHr/m
+3shsk6nfGt7oTtU48WdOPw76BByHOr0tTM+nMfptmBpu1LQu4sFifmOvUN8lTfQO
+KMZvobJtDsnfCj34O4nMLjtLVqi6YE8a3lgldXoekZj+8cfZztCuKbnkiYw1GTzW
+9skd/4Ik5LBR0pTFqepOlJeM8QMHics6wQKCAQA+6RvPk2/b8OJArrFHkhNbfqpf
+Sa/BvRam8azo2MGgOZWVm/yAGHvoVgOaq2H1DrrDh6qBlzZULpwFD+XeuuzYrLs2
+mYr2LFZdeQtd95V7oASdM0OlFatzKPOoLrHwNc4ztwNz0sMrjTYxDG07mp/3Ixz7
+koUPinV636wZUmvwHiUTlD4E2db+fslDhBUc+HV/4MXihvMSA3D8Mum9SttMABYJ
+L0lBzexfVL8oyYvft/tGwV9LwrlFpzndnX6ZZvgJUqzBPx/+exuZjnTwD3N70SN+
+T0TwL0tsVE5clxVdv5xlm5WIW4kQKglRoJnVB1TnpFddRRu/QD8S+e/S6G4w
+-----END RSA PRIVATE KEY-----
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/docker-compose.yaml b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/docker-compose.yaml
new file mode 100644
index 0000000..53702a6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/docker-compose.yaml
@@ -0,0 +1,15 @@
+registry:
+ image: registry:2
+ ports:
+ - 127.0.0.1:5000:5000
+ environment:
+ REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
+ REGISTRY_HTTP_TLS_CERTIFICATE: /opt/certs/localhost.crt
+ REGISTRY_HTTP_TLS_KEY: /opt/certs/localhost.key
+ REGISTRY_AUTH_TOKEN_REALM: http://localhost:8080/auth/realms/docker-test-realm/protocol/docker-v2/auth
+ REGISTRY_AUTH_TOKEN_SERVICE: docker-test-client
+ REGISTRY_AUTH_TOKEN_ISSUER: http://localhost:8080/auth/realms/docker-test-realm
+ REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /opt/certs/localhost_trust_chain.pem
+ volumes:
+ - ./data:/data:z
+ - ./certs:/opt/certs:z
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker
new file mode 100644
index 0000000..433cbc5
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker
@@ -0,0 +1,45 @@
+# /etc/sysconfig/docker
+
+# Modify these options if you want to change the way the docker daemon runs
+OPTIONS='--selinux-enabled --log-driver=journald --signature-verification=false'
+if [ -z "${DOCKER_CERT_PATH}" ]; then
+ DOCKER_CERT_PATH=/etc/docker
+fi
+
+# If you want to add your own registry to be used for docker search and docker
+# pull use the ADD_REGISTRY option to list a set of registries, each prepended
+# with --add-registry flag. The first registry added will be the first registry
+# searched.
+# ADD_REGISTRY='--add-registry registry.access.redhat.com'
+
+# If you want to block registries from being used, uncomment the BLOCK_REGISTRY
+# option and give it a set of registries, each prepended with --block-registry
+# flag. For example adding docker.io will stop users from downloading images
+# from docker.io
+# BLOCK_REGISTRY='--block-registry'
+
+# If you have a registry secured with https but do not have proper certs
+# distributed, you can tell docker to not look for full authorization by
+# adding the registry to the INSECURE_REGISTRY line and uncommenting it.
+INSECURE_REGISTRY='--insecure-registry registry.localdomain:5000'
+
+# On an SELinux system, if you remove the --selinux-enabled option, you
+# also need to turn on the docker_transition_unconfined boolean.
+# setsebool -P docker_transition_unconfined 1
+
+# Location used for temporary files, such as those created by
+# docker load and build operations. Default is /var/lib/docker/tmp
+# Can be overriden by setting the following environment variable.
+# DOCKER_TMPDIR=/var/tmp
+
+# Controls the /etc/cron.daily/docker-logrotate cron job status.
+# To disable, uncomment the line below.
+# LOGROTATE=false
+#
+
+# docker-latest daemon can be used by starting the docker-latest unitfile.
+# To use docker-latest client, uncomment below lines
+#DOCKERBINARY=/usr/bin/docker-latest
+#DOCKERDBINARY=/usr/bin/dockerd-latest
+#DOCKER_CONTAINERD_BINARY=/usr/bin/docker-containerd-latest
+#DOCKER_CONTAINERD_SHIM_BINARY=/usr/bin/docker-containerd-shim-latest
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/registry-config.txt b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/registry-config.txt
new file mode 100644
index 0000000..fe1af61
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/registry-config.txt
@@ -0,0 +1,6 @@
+auth:
+ token:
+ realm: http://localhost:8080/auth/auth/realms/docker-test-realm/protocol/docker-v2/auth
+ service: docker-test-client
+ issuer: http://localhost:8080/auth/auth/realms/docker-test-realm
+
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/variable-override.txt b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/variable-override.txt
new file mode 100644
index 0000000..7fd8485
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/variable-override.txt
@@ -0,0 +1,4 @@
+-e REGISTRY_AUTH_TOKEN_REALM=http://localhost:8080/auth/auth/realms/docker-test-realm/protocol/docker-v2/auth \
+-e REGISTRY_AUTH_TOKEN_SERVICE: docker-test-client \
+-e REGISTRY_AUTH_TOKEN_ISSUER: http://localhost:8080/auth/auth/realms/docker-test-realm \
+
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/docker-test-realm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/docker-test-realm.json
new file mode 100644
index 0000000..9f9d2ff
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/docker-test-realm.json
@@ -0,0 +1,1315 @@
+{
+ "id" : "docker-test-realm",
+ "realm" : "docker-test-realm",
+ "notBefore" : 0,
+ "revokeRefreshToken" : false,
+ "accessTokenLifespan" : 300,
+ "accessTokenLifespanForImplicitFlow" : 900,
+ "ssoSessionIdleTimeout" : 1800,
+ "ssoSessionMaxLifespan" : 36000,
+ "offlineSessionIdleTimeout" : 2592000,
+ "accessCodeLifespan" : 60,
+ "accessCodeLifespanUserAction" : 300,
+ "accessCodeLifespanLogin" : 1800,
+ "enabled" : true,
+ "sslRequired" : "external",
+ "registrationAllowed" : false,
+ "registrationEmailAsUsername" : false,
+ "rememberMe" : false,
+ "verifyEmail" : false,
+ "loginWithEmailAllowed" : true,
+ "duplicateEmailsAllowed" : false,
+ "resetPasswordAllowed" : false,
+ "editUsernameAllowed" : false,
+ "bruteForceProtected" : false,
+ "maxFailureWaitSeconds" : 900,
+ "minimumQuickLoginWaitSeconds" : 60,
+ "waitIncrementSeconds" : 60,
+ "quickLoginCheckMilliSeconds" : 1000,
+ "maxDeltaTimeSeconds" : 43200,
+ "failureFactor" : 30,
+ "roles" : {
+ "realm" : [ {
+ "id" : "dbcbd18f-52cb-4e45-9372-7e2bbf255729",
+ "name" : "uma_authorization",
+ "description" : "${role_uma_authorization}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : false,
+ "containerId" : "docker-test-realm"
+ }, {
+ "id" : "834687f7-29ce-43a2-a5f7-55c965026827",
+ "name" : "offline_access",
+ "description" : "${role_offline-access}",
+ "scopeParamRequired" : true,
+ "composite" : false,
+ "clientRole" : false,
+ "containerId" : "docker-test-realm"
+ } ],
+ "client" : {
+ "realm-management" : [ {
+ "id" : "11956a41-328d-4cec-a98c-f77fe6accda3",
+ "name" : "create-client",
+ "description" : "${role_create-client}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "e65e7810-359b-429d-9389-c1cd041915fd",
+ "name" : "view-clients",
+ "description" : "${role_view-clients}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "43d747fc-76c3-4a06-a492-44dea5a07edb",
+ "name" : "manage-clients",
+ "description" : "${role_manage-clients}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "de324c4c-34ea-467b-b851-cca912d1cf60",
+ "name" : "view-authorization",
+ "description" : "${role_view-authorization}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "b0f25ef8-404b-4370-a981-ca155eae6b83",
+ "name" : "manage-identity-providers",
+ "description" : "${role_manage-identity-providers}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "c16eb517-5416-4b86-b86d-c312d3b98e09",
+ "name" : "impersonation",
+ "description" : "${role_impersonation}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "1526f875-2d04-453a-aa29-979f61d1013c",
+ "name" : "view-events",
+ "description" : "${role_view-events}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "7043cd10-a2b0-4568-8295-9840c9c2fa43",
+ "name" : "view-realm",
+ "description" : "${role_view-realm}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "23bb0cd9-2c0e-4510-96af-73f0ba1251df",
+ "name" : "manage-realm",
+ "description" : "${role_manage-realm}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "eff4c8dd-0c53-41ca-8013-336b9c19f55b",
+ "name" : "manage-authorization",
+ "description" : "${role_manage-authorization}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "41cead2f-ed3f-4add-8fd2-ceaf3e20daf5",
+ "name" : "realm-admin",
+ "description" : "${role_realm-admin}",
+ "scopeParamRequired" : false,
+ "composite" : true,
+ "composites" : {
+ "client" : {
+ "realm-management" : [ "create-client", "view-clients", "manage-clients", "view-authorization", "manage-identity-providers", "impersonation", "view-events", "view-realm", "manage-realm", "manage-authorization", "view-users", "manage-events", "manage-users", "view-identity-providers" ]
+ }
+ },
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "025e57f5-73a2-4382-b6e7-ea2f447f86a5",
+ "name" : "view-users",
+ "description" : "${role_view-users}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "86fce514-f5f4-4c7d-ae07-56caaeffe272",
+ "name" : "manage-events",
+ "description" : "${role_manage-events}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "8f49cc1a-a3f1-4185-982e-765617c1ac88",
+ "name" : "manage-users",
+ "description" : "${role_manage-users}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "e8d7cf8e-b970-4ada-a8b5-58b7d5fcc4e8",
+ "name" : "view-identity-providers",
+ "description" : "${role_view-identity-providers}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ } ],
+ "security-admin-console" : [ ],
+ "admin-cli" : [ ],
+ "broker" : [ {
+ "id" : "f0eb6730-f5ed-4216-a9db-d87fee982b08",
+ "name" : "read-token",
+ "description" : "${role_read-token}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "f85d993b-f251-4f9c-87f9-6586cb7bb830"
+ } ],
+ "account" : [ {
+ "id" : "8a34db5e-26fb-4be0-ba09-d4e92bc9dd88",
+ "name" : "view-profile",
+ "description" : "${role_view-profile}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9"
+ }, {
+ "id" : "5aef5567-004e-4a18-8ee4-b8a6d5fa0c85",
+ "name" : "manage-account",
+ "description" : "${role_manage-account}",
+ "scopeParamRequired" : false,
+ "composite" : true,
+ "composites" : {
+ "client" : {
+ "account" : [ "manage-account-links" ]
+ }
+ },
+ "clientRole" : true,
+ "containerId" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9"
+ }, {
+ "id" : "3bf09e38-5f0d-41c8-adc2-1dba1cf5d819",
+ "name" : "manage-account-links",
+ "description" : "${role_manage-account-links}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9"
+ } ]
+ }
+ },
+ "groups" : [ ],
+ "defaultRoles" : [ "offline_access", "uma_authorization" ],
+ "requiredCredentials" : [ "password" ],
+ "passwordPolicy" : "hashIterations(20000)",
+ "otpPolicyType" : "totp",
+ "otpPolicyAlgorithm" : "HmacSHA1",
+ "otpPolicyInitialCounter" : 0,
+ "otpPolicyDigits" : 6,
+ "otpPolicyLookAheadWindow" : 1,
+ "otpPolicyPeriod" : 30,
+ "users" : [ {
+ "id" : "a413b2e2-5cff-43e4-ac6e-ab307e8c0652",
+ "createdTimestamp" : 1492117705870,
+ "username" : "user1",
+ "enabled" : true,
+ "totp" : false,
+ "emailVerified" : false,
+ "firstName" : "User",
+ "lastName" : "One",
+ "email" : "user1@redhat.com",
+ "credentials" : [ {
+ "type" : "password",
+ "hashedSaltedValue" : "A1B2lKKJ2npPjSoFo653q2H8Wu/CNoAVD9pYUnAJwMb0AJzAfXGkdX6eHSUEyUK1cDGVfn6iX/JRNo5XyoSH2w==",
+ "salt" : "5X0JI44mCfleW8qR08II1A==",
+ "hashIterations" : 20000,
+ "counter" : 0,
+ "algorithm" : "pbkdf2",
+ "digits" : 0,
+ "period" : 0,
+ "createdDate" : 1492117716198,
+ "config" : { }
+ } ],
+ "disableableCredentialTypes" : [ "password" ],
+ "requiredActions" : [ ],
+ "realmRoles" : [ "uma_authorization", "offline_access" ],
+ "clientRoles" : {
+ "account" : [ "view-profile", "manage-account" ]
+ },
+ "groups" : [ ]
+ } ],
+ "clientScopeMappings" : {
+ "realm-management" : [ {
+ "client" : "admin-cli",
+ "roles" : [ "realm-admin" ]
+ }, {
+ "client" : "security-admin-console",
+ "roles" : [ "realm-admin" ]
+ } ]
+ },
+ "clients" : [ {
+ "id" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9",
+ "clientId" : "account",
+ "name" : "${client_account}",
+ "baseUrl" : "/auth/realms/docker-test-realm/account",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "e4f21dc6-959f-4248-8e04-4fb606d9ceaf",
+ "defaultRoles" : [ "view-profile", "manage-account" ],
+ "redirectUris" : [ "/auth/realms/docker-test-realm/account/*" ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : false,
+ "frontchannelLogout" : false,
+ "attributes" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "protocolMappers" : [ {
+ "id" : "d0e8f6a9-9442-443e-af03-7d31545af866",
+ "name" : "family name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${familyName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "lastName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "family_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "2afbd4f6-e9bc-45d1-92ee-1c4dc9c099d5",
+ "name" : "email",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${email}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "email",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "email",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "1bd8d67f-3aac-42cc-8dba-e676a2b41bb1",
+ "name" : "docker-v2-allow-all-mapper",
+ "protocol" : "docker-v2",
+ "protocolMapper" : "docker-v2-allow-all-mapper",
+ "consentRequired" : false,
+ "config" : { }
+ }, {
+ "id" : "d7df006b-686a-41a8-958b-2525b9c48ff2",
+ "name" : "given name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${givenName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "firstName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "given_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "93bee57d-79e3-42fb-87da-71c05963aa49",
+ "name" : "role list",
+ "protocol" : "saml",
+ "protocolMapper" : "saml-role-list-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "single" : "false",
+ "attribute.nameformat" : "Basic",
+ "attribute.name" : "Role"
+ }
+ }, {
+ "id" : "297ecd2f-4440-48aa-82aa-74901588f7c1",
+ "name" : "full name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-full-name-mapper",
+ "consentRequired" : true,
+ "consentText" : "${fullName}",
+ "config" : {
+ "id.token.claim" : "true",
+ "access.token.claim" : "true"
+ }
+ }, {
+ "id" : "ac4d45a0-c127-4ba3-b243-49cc570a9871",
+ "name" : "username",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${username}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "username",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "preferred_username",
+ "jsonType.label" : "String"
+ }
+ } ],
+ "useTemplateConfig" : false,
+ "useTemplateScope" : false,
+ "useTemplateMappers" : false
+ }, {
+ "id" : "e0105ad8-27c3-471d-99c3-244762847563",
+ "clientId" : "admin-cli",
+ "name" : "${client_admin-cli}",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "72ff8162-b891-4ba3-9501-68e2e34d7cf0",
+ "redirectUris" : [ ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : false,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : true,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : true,
+ "frontchannelLogout" : false,
+ "attributes" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "protocolMappers" : [ {
+ "id" : "c61ba5ee-a8e1-409c-9898-cb8b9697eb26",
+ "name" : "docker-v2-allow-all-mapper",
+ "protocol" : "docker-v2",
+ "protocolMapper" : "docker-v2-allow-all-mapper",
+ "consentRequired" : false,
+ "config" : { }
+ }, {
+ "id" : "879e8a4f-e4e9-402d-b867-59171fbcb370",
+ "name" : "family name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${familyName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "lastName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "family_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "ee335ebe-a3bd-426a-9622-268ad583fe67",
+ "name" : "role list",
+ "protocol" : "saml",
+ "protocolMapper" : "saml-role-list-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "single" : "false",
+ "attribute.nameformat" : "Basic",
+ "attribute.name" : "Role"
+ }
+ }, {
+ "id" : "628083f3-62f0-454a-bc35-80728893513b",
+ "name" : "full name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-full-name-mapper",
+ "consentRequired" : true,
+ "consentText" : "${fullName}",
+ "config" : {
+ "id.token.claim" : "true",
+ "access.token.claim" : "true"
+ }
+ }, {
+ "id" : "48efdb06-c88b-478f-9009-65bac264de00",
+ "name" : "username",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${username}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "username",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "preferred_username",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "5683455d-bcaf-41ca-8b0e-da15dfd48753",
+ "name" : "email",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${email}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "email",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "email",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "31d6698d-10f0-4fd9-b7f3-c4bc23b507dc",
+ "name" : "given name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${givenName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "firstName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "given_name",
+ "jsonType.label" : "String"
+ }
+ } ],
+ "useTemplateConfig" : false,
+ "useTemplateScope" : false,
+ "useTemplateMappers" : false
+ }, {
+ "id" : "f85d993b-f251-4f9c-87f9-6586cb7bb830",
+ "clientId" : "broker",
+ "name" : "${client_broker}",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "1fbd3ca1-203f-4074-b1d5-b0c6c2739ea4",
+ "redirectUris" : [ ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : false,
+ "frontchannelLogout" : false,
+ "attributes" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "protocolMappers" : [ {
+ "id" : "9b754f6b-0a03-4db5-80f9-3c4f656e0828",
+ "name" : "docker-v2-allow-all-mapper",
+ "protocol" : "docker-v2",
+ "protocolMapper" : "docker-v2-allow-all-mapper",
+ "consentRequired" : false,
+ "config" : { }
+ }, {
+ "id" : "384701c9-c08a-483f-8f44-b288c8694fe3",
+ "name" : "username",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${username}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "username",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "preferred_username",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "c2e767e6-7744-457b-8dea-e6f170a5122c",
+ "name" : "given name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${givenName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "firstName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "given_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "796cc4cd-b7a5-4255-bf8b-3b99db7532ee",
+ "name" : "role list",
+ "protocol" : "saml",
+ "protocolMapper" : "saml-role-list-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "single" : "false",
+ "attribute.nameformat" : "Basic",
+ "attribute.name" : "Role"
+ }
+ }, {
+ "id" : "528ba572-1438-4afc-88c7-02f5e511d433",
+ "name" : "email",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${email}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "email",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "email",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "a14f7e92-23ea-444f-8bb8-f2dfb1f255dc",
+ "name" : "full name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-full-name-mapper",
+ "consentRequired" : true,
+ "consentText" : "${fullName}",
+ "config" : {
+ "id.token.claim" : "true",
+ "access.token.claim" : "true"
+ }
+ }, {
+ "id" : "724e61f0-b490-46b1-b063-2ee122e4ac7a",
+ "name" : "family name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${familyName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "lastName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "family_name",
+ "jsonType.label" : "String"
+ }
+ } ],
+ "useTemplateConfig" : false,
+ "useTemplateScope" : false,
+ "useTemplateMappers" : false
+ }, {
+ "id" : "2d61e404-7444-4fad-8386-06b811b5f7c1",
+ "clientId" : "realm-management",
+ "name" : "${client_realm-management}",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "403c5eae-8c79-4cfc-ba00-4bb2bfbaaf92",
+ "redirectUris" : [ ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : true,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : false,
+ "frontchannelLogout" : false,
+ "attributes" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "protocolMappers" : [ {
+ "id" : "098aeaab-76f1-4742-8522-27e8c178e596",
+ "name" : "family name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${familyName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "lastName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "family_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "fbc2f08d-d6a0-49ad-9b61-601eec42d46f",
+ "name" : "full name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-full-name-mapper",
+ "consentRequired" : true,
+ "consentText" : "${fullName}",
+ "config" : {
+ "id.token.claim" : "true",
+ "access.token.claim" : "true"
+ }
+ }, {
+ "id" : "b39438d3-a149-4e0f-a3a1-87c441d05123",
+ "name" : "docker-v2-allow-all-mapper",
+ "protocol" : "docker-v2",
+ "protocolMapper" : "docker-v2-allow-all-mapper",
+ "consentRequired" : false,
+ "config" : { }
+ }, {
+ "id" : "06706c9d-1f71-4cc8-afca-daea4e9fe9e8",
+ "name" : "email",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${email}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "email",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "email",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "bcf14207-1f8e-4e53-8d2b-59939e82f8c4",
+ "name" : "username",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${username}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "username",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "preferred_username",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "91e78da7-b049-41a5-9a22-1f833755c41b",
+ "name" : "given name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${givenName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "firstName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "given_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "3949f934-b86b-4e70-bcc4-52db0288d55b",
+ "name" : "role list",
+ "protocol" : "saml",
+ "protocolMapper" : "saml-role-list-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "single" : "false",
+ "attribute.nameformat" : "Basic",
+ "attribute.name" : "Role"
+ }
+ } ],
+ "useTemplateConfig" : false,
+ "useTemplateScope" : false,
+ "useTemplateMappers" : false
+ }, {
+ "id" : "7d4ec353-1cf7-43a1-af4d-218fd9dd37ed",
+ "clientId" : "security-admin-console",
+ "name" : "${client_security-admin-console}",
+ "baseUrl" : "/auth/admin/docker-test-realm/console/index.html",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "a0e6ebf9-58fa-472c-a853-64c16c2f8ad8",
+ "redirectUris" : [ "/auth/admin/docker-test-realm/console/*" ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : true,
+ "frontchannelLogout" : false,
+ "attributes" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "protocolMappers" : [ {
+ "id" : "c501a7bc-171b-4ce6-8d91-3f69ae32591d",
+ "name" : "given name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${givenName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "firstName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "given_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "bce6f7a9-b86d-4f5f-a262-f01e235b5622",
+ "name" : "locale",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "consentText" : "${locale}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "locale",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "locale",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "9d28d5da-53f2-49f9-b0c0-ae3a51f5ac92",
+ "name" : "role list",
+ "protocol" : "saml",
+ "protocolMapper" : "saml-role-list-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "single" : "false",
+ "attribute.nameformat" : "Basic",
+ "attribute.name" : "Role"
+ }
+ }, {
+ "id" : "00183de0-af80-47c5-807f-a62366b2e1b6",
+ "name" : "email",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${email}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "email",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "email",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "31eccf32-3e16-44f2-b727-27c5cb2e9554",
+ "name" : "family name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${familyName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "lastName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "family_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "c26c0dc9-4cba-42f0-80e4-1f2363084b95",
+ "name" : "docker-v2-allow-all-mapper",
+ "protocol" : "docker-v2",
+ "protocolMapper" : "docker-v2-allow-all-mapper",
+ "consentRequired" : false,
+ "config" : { }
+ }, {
+ "id" : "db4d11d2-e243-4df7-811f-e4622b49950b",
+ "name" : "username",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${username}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "username",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "preferred_username",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "e6d398a7-dbec-480f-93c4-8a9d1bfbad24",
+ "name" : "full name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-full-name-mapper",
+ "consentRequired" : true,
+ "consentText" : "${fullName}",
+ "config" : {
+ "id.token.claim" : "true",
+ "access.token.claim" : "true"
+ }
+ } ],
+ "useTemplateConfig" : false,
+ "useTemplateScope" : false,
+ "useTemplateMappers" : false
+ } ],
+ "clientTemplates" : [ ],
+ "browserSecurityHeaders" : {
+ "xContentTypeOptions" : "nosniff",
+ "xRobotsTag" : "none",
+ "xFrameOptions" : "SAMEORIGIN",
+ "contentSecurityPolicy" : "frame-src 'self'"
+ },
+ "smtpServer" : { },
+ "eventsEnabled" : false,
+ "eventsListeners" : [ "jboss-logging" ],
+ "enabledEventTypes" : [ ],
+ "adminEventsEnabled" : false,
+ "adminEventsDetailsEnabled" : false,
+ "components" : {
+ "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ {
+ "id" : "7f9cbf76-3ecb-49ed-850b-f2fce4ecc87f",
+ "name" : "Trusted Hosts",
+ "providerId" : "trusted-hosts",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : {
+ "host-sending-registration-request-must-match" : [ "true" ],
+ "client-uris-must-match" : [ "true" ]
+ }
+ }, {
+ "id" : "ea2db337-b9d9-463b-abea-0c5dadb5b5f0",
+ "name" : "Consent Required",
+ "providerId" : "consent-required",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : { }
+ }, {
+ "id" : "2d6e7a94-d73c-4f54-b9ea-64f563f5f8fa",
+ "name" : "Full Scope Disabled",
+ "providerId" : "scope",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : { }
+ }, {
+ "id" : "16f6705e-f671-4fde-ba7d-6254e404b503",
+ "name" : "Max Clients Limit",
+ "providerId" : "max-clients",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : {
+ "max-clients" : [ "200" ]
+ }
+ }, {
+ "id" : "e4baf3d7-e7af-48d0-890d-11304927be69",
+ "name" : "Allowed Protocol Mapper Types",
+ "providerId" : "allowed-protocol-mappers",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : {
+ "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper" ],
+ "consent-required-for-all-mappers" : [ "true" ]
+ }
+ }, {
+ "id" : "c27ecc77-c0c3-462e-b803-33432c9a7813",
+ "name" : "Allowed Client Templates",
+ "providerId" : "allowed-client-templates",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : { }
+ }, {
+ "id" : "18bdc70c-5475-4ae4-8606-d52a6397a125",
+ "name" : "Allowed Protocol Mapper Types",
+ "providerId" : "allowed-protocol-mappers",
+ "subType" : "authenticated",
+ "subComponents" : { },
+ "config" : {
+ "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper" ],
+ "consent-required-for-all-mappers" : [ "true" ]
+ }
+ }, {
+ "id" : "95fd260b-36e9-4df5-aa6b-6c3b8138c766",
+ "name" : "Allowed Client Templates",
+ "providerId" : "allowed-client-templates",
+ "subType" : "authenticated",
+ "subComponents" : { },
+ "config" : { }
+ } ],
+ "org.keycloak.keys.KeyProvider" : [ {
+ "id" : "9dc7e4c1-5bc2-4756-9486-fb64a06582ad",
+ "name" : "rsa-generated",
+ "providerId" : "rsa-generated",
+ "subComponents" : { },
+ "config" : {
+ "privateKey" : [ "MIIEowIBAAKCAQEAk2ZfvP3znNH5EbBd6ckiT7Eq7loqBCa5o6fdOajD2X8cjT7roLG4GANhu075SUrCxfcx2A+P1kBnSsyPCc3dxMmCT7BUJsYScCF88q52GIskQc7E+eBkuIjeVmPMECLq3xhY7YONqIl47n17dEYYmVo1uRqbrVSFdSX9EDqn9vRn/7uJFLafdK9766Na2JMSZVKgnNsXRTtxxCjnU3LyMnNw5JdbnsfSPj1pgnOi+pTDPqlwfcAIaG72lmhWMXaStmwO1DYsBoUd4yEnv6/dtXQkAaDr6TthX7ITliaxXPrh+YMDAxnhV7X/PtbiFUpTaNBpSy3k87onYBiWrL44IQIDAQABAoIBAQCLigz0Q41OVlDt+ALQAYMj4lr8DgtcprRzQ8Tggu31hqom5Pv3woa+5OSuh9LjGY1OD/f1zLWkZI/kdcarx2I8m29rtUfU9QobcPhyXcqa7Y5DZlV/IHj5YUjqi8txMz0aOlhlcXa3qHz9eXlX18wN0SKuu4vJCQzWnEH4DS9ZTwXAp4uZUkOIUHIkACcRPBGBVHCNvwneLA7tPi5E1TK2fvlgyHOvbsomBh385WKrO6HFBmjV9XsMx3QU1EjRaXSpELdIDUR9Z8rgVg08nZ8z3LZ9UNHHdiAXoCm5oqqf8zP5gL6U79vybvjerCpx2AX60UkhpuHeUmZQQMcylLLhAoGBAP4xdt/gkBsC+9faAw3o9VW/6RsdW7ussptnt50Ymi/mlE8qHNe0oSbkGAhqdqCjAV0+cgygn2krOM+OUF/Lq87kBgRE0fAqaarEAryT/DrmvroNrp3Lnif9/kAcEWo8WhpIPgspqzVy7byAFR29/sdbVby2C37OeFYpw0ad/UVdAoGBAJRylgu59wM5ekrmJqNd326J+RLg76abF9TpW3Ka5CY12NgI60ZxRFBfncZKJCTovmoZgE89RHdz7n4ghxVg8D9ThPY7Kh4flAq8SIqAqmb2b7hkfyEMOgGpdwQq1T7uIcIefwYivLpb62C8cSK7leLXJ/wMza5bo8m5fD3t+a2VAoGAJZxqC2wtxmFlpCWU6Bz9GAgCVMm+RgGil8375Bm8zrOeZCxGAkCuy5NaXvxpuxEDZamUtHuburLzf/p9t/7p1/3zSfRo39FWuzavdPmsi4aS1/KoUJ7NMvupABFnHkH5zwO7cmli9NChjo+hEDqJlTPVdsu03bltIsqhIzTDQd0CgYAQ8owCxrZWnedCScg7emoZupK+/wMdKDOuUP3ptZk6a4dYEpyZrDC6ZFAk5S3/MLscbdDiOwJoCMo/iAMkA68p66UQX2zNh5llKF23wjyyCIx0prSE11p/+hLmXOV/i7w65zRlRO368KeMobbg2j2gaiPceLG6qCeozg5LG7IXiQKBgALwLpGKaIixsIaAD1Bzd5cLaKdPGXPyaJwG5xqog58XGVcHklGQRnaN/B3vlrHBgI/NGZNt83bWamCTVlN+A0q9AnMxGHXZHzL21lx6bNiZXX+3DVDm88m+ODPebZXxSZQRNjBrw1KotqUyyhzkbIjfE8752ofb4T+veViHkjW2" ],
+ "certificate" : [ "MIICsTCCAZkCBgFbaSTAdjANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFkb2NrZXItdGVzdC1yZWFsbTAeFw0xNzA0MTMyMTA2MDdaFw0yNzA0MTMyMTA3NDdaMBwxGjAYBgNVBAMMEWRvY2tlci10ZXN0LXJlYWxtMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk2ZfvP3znNH5EbBd6ckiT7Eq7loqBCa5o6fdOajD2X8cjT7roLG4GANhu075SUrCxfcx2A+P1kBnSsyPCc3dxMmCT7BUJsYScCF88q52GIskQc7E+eBkuIjeVmPMECLq3xhY7YONqIl47n17dEYYmVo1uRqbrVSFdSX9EDqn9vRn/7uJFLafdK9766Na2JMSZVKgnNsXRTtxxCjnU3LyMnNw5JdbnsfSPj1pgnOi+pTDPqlwfcAIaG72lmhWMXaStmwO1DYsBoUd4yEnv6/dtXQkAaDr6TthX7ITliaxXPrh+YMDAxnhV7X/PtbiFUpTaNBpSy3k87onYBiWrL44IQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB2u9hP3S1bP4+FBwOLPwI3p7WrWBlt2CgwTiyuXvV7u9GLiXqCDUWZd3dSks9vU4Y4NdVyToY4q9YFJ3oAQXlfRw2Yi6e/0nSPpU25o52TWwREnRY98fjVy1eC5K2GRwSu79HZKeqA0Tg/ONvGOrlYO1KPbWZGg9NcwAGeILkNdfI82w0KZTpTy+f5ATtV30pFkDNT0gfayFmDQvw3EgcD/x0/vI3PlnHLLGprV/ZlBmFWo0vk8iUBwP1YbTA0XqKasITFXJaPeZWzNMCjR1NxDqlIq095uX04E5XGS6XGJKS9PanvGXidk5xMgI7xwKE6jaxD9pspYPRgv66528Dc" ],
+ "priority" : [ "100" ]
+ }
+ }, {
+ "id" : "ae58bc1e-c60e-4889-986d-ea5648ea5989",
+ "name" : "hmac-generated",
+ "providerId" : "hmac-generated",
+ "subComponents" : { },
+ "config" : {
+ "kid" : [ "5a0c54c4-fb3d-4b2c-8e1a-9bebb6251b6f" ],
+ "secret" : [ "-5XJ1f5410LDE1XIvQsvAuwwm4CdEyd6Rco0E3EsxG4" ],
+ "priority" : [ "100" ]
+ }
+ } ]
+ },
+ "internationalizationEnabled" : false,
+ "supportedLocales" : [ ],
+ "authenticationFlows" : [ {
+ "id" : "6a3d3800-bea6-4fc4-958f-65365d23c33b",
+ "alias" : "Handle Existing Account",
+ "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "idp-confirm-link",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "idp-email-verification",
+ "requirement" : "ALTERNATIVE",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "requirement" : "ALTERNATIVE",
+ "priority" : 30,
+ "flowAlias" : "Verify Existing Account by Re-authentication",
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : true
+ } ]
+ }, {
+ "id" : "41de318f-6434-443a-bcf0-6632568f32b0",
+ "alias" : "Verify Existing Account by Re-authentication",
+ "description" : "Reauthentication of existing account",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "idp-username-password-form",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "auth-otp-form",
+ "requirement" : "OPTIONAL",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "8b2f90df-5a09-49b6-b978-acbb74a60670",
+ "alias" : "browser",
+ "description" : "browser based authentication",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "auth-cookie",
+ "requirement" : "ALTERNATIVE",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "auth-spnego",
+ "requirement" : "DISABLED",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "identity-provider-redirector",
+ "requirement" : "ALTERNATIVE",
+ "priority" : 25,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "requirement" : "ALTERNATIVE",
+ "priority" : 30,
+ "flowAlias" : "forms",
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : true
+ } ]
+ }, {
+ "id" : "6d0cba98-a1d9-4ca4-a877-ffe0d2c7f667",
+ "alias" : "clients",
+ "description" : "Base authentication for clients",
+ "providerId" : "client-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "client-secret",
+ "requirement" : "ALTERNATIVE",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "client-jwt",
+ "requirement" : "ALTERNATIVE",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "8c752045-bd44-48fc-ae36-816625897545",
+ "alias" : "direct grant",
+ "description" : "OpenID Connect Resource Owner Grant",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "direct-grant-validate-username",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "direct-grant-validate-password",
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "direct-grant-validate-otp",
+ "requirement" : "OPTIONAL",
+ "priority" : 30,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "7c8e6906-6b5f-4766-b80d-f23b56595992",
+ "alias" : "docker-basic-auth-flow",
+ "description" : "",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : false,
+ "authenticationExecutions" : [ {
+ "authenticator" : "docker-http-basic-authenticator",
+ "requirement" : "REQUIRED",
+ "priority" : 0,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "a41036cf-e368-46e0-9cf3-a96908c53609",
+ "alias" : "first broker login",
+ "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticatorConfig" : "review profile config",
+ "authenticator" : "idp-review-profile",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticatorConfig" : "create unique user config",
+ "authenticator" : "idp-create-user-if-unique",
+ "requirement" : "ALTERNATIVE",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "requirement" : "ALTERNATIVE",
+ "priority" : 30,
+ "flowAlias" : "Handle Existing Account",
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : true
+ } ]
+ }, {
+ "id" : "49c349cc-f11e-461c-98e2-546327175ca4",
+ "alias" : "forms",
+ "description" : "Username, password, otp and other auth forms.",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "auth-username-password-form",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "auth-otp-form",
+ "requirement" : "OPTIONAL",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "2445867e-f9eb-46cc-8f68-c15d6cf962e4",
+ "alias" : "registration",
+ "description" : "registration flow",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "registration-page-form",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "flowAlias" : "registration form",
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : true
+ } ]
+ }, {
+ "id" : "83a735c2-cf61-49fa-879b-e9b0ed5bb9e9",
+ "alias" : "registration form",
+ "description" : "registration form",
+ "providerId" : "form-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "registration-user-creation",
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "registration-profile-action",
+ "requirement" : "REQUIRED",
+ "priority" : 40,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "registration-password-action",
+ "requirement" : "REQUIRED",
+ "priority" : 50,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "registration-recaptcha-action",
+ "requirement" : "DISABLED",
+ "priority" : 60,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "32acb7cb-af8f-42b2-bd34-9ff534d87121",
+ "alias" : "reset credentials",
+ "description" : "Reset credentials for a user if they forgot their password or something",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "reset-credentials-choose-user",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "reset-credential-email",
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "reset-password",
+ "requirement" : "REQUIRED",
+ "priority" : 30,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "reset-otp",
+ "requirement" : "OPTIONAL",
+ "priority" : 40,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "1c67b912-70f4-4182-b055-08c3d6bb23c8",
+ "alias" : "saml ecp",
+ "description" : "SAML ECP Profile Authentication Flow",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "http-basic-authenticator",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ } ],
+ "authenticatorConfig" : [ {
+ "id" : "30fd72e5-eb98-4ae5-a695-c959ec626ac6",
+ "alias" : "create unique user config",
+ "config" : {
+ "require.password.update.after.registration" : "false"
+ }
+ }, {
+ "id" : "e0ea82a7-98d7-4ffb-8444-8d240a94d83b",
+ "alias" : "review profile config",
+ "config" : {
+ "update.profile.on.first.login" : "missing"
+ }
+ } ],
+ "requiredActions" : [ {
+ "alias" : "CONFIGURE_TOTP",
+ "name" : "Configure OTP",
+ "providerId" : "CONFIGURE_TOTP",
+ "enabled" : true,
+ "defaultAction" : false,
+ "config" : { }
+ }, {
+ "alias" : "UPDATE_PASSWORD",
+ "name" : "Update Password",
+ "providerId" : "UPDATE_PASSWORD",
+ "enabled" : true,
+ "defaultAction" : false,
+ "config" : { }
+ }, {
+ "alias" : "UPDATE_PROFILE",
+ "name" : "Update Profile",
+ "providerId" : "UPDATE_PROFILE",
+ "enabled" : true,
+ "defaultAction" : false,
+ "config" : { }
+ }, {
+ "alias" : "VERIFY_EMAIL",
+ "name" : "Verify Email",
+ "providerId" : "VERIFY_EMAIL",
+ "enabled" : true,
+ "defaultAction" : false,
+ "config" : { }
+ }, {
+ "alias" : "terms_and_conditions",
+ "name" : "Terms and Conditions",
+ "providerId" : "terms_and_conditions",
+ "enabled" : false,
+ "defaultAction" : false,
+ "config" : { }
+ } ],
+ "browserFlow" : "docker-basic-auth-flow",
+ "registrationFlow" : "registration",
+ "directGrantFlow" : "direct grant",
+ "resetCredentialsFlow" : "reset credentials",
+ "clientAuthenticationFlow" : "clients",
+ "attributes" : {
+ "_browser_header.xFrameOptions" : "SAMEORIGIN",
+ "failureFactor" : "30",
+ "quickLoginCheckMilliSeconds" : "1000",
+ "maxDeltaTimeSeconds" : "43200",
+ "_browser_header.xContentTypeOptions" : "nosniff",
+ "_browser_header.xRobotsTag" : "none",
+ "bruteForceProtected" : "false",
+ "maxFailureWaitSeconds" : "900",
+ "_browser_header.contentSecurityPolicy" : "frame-src 'self'",
+ "minimumQuickLoginWaitSeconds" : "60",
+ "waitIncrementSeconds" : "60"
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties
index 167c611..8f74373 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties
@@ -18,24 +18,22 @@
log4j.rootLogger=info, keycloak
log4j.appender.keycloak=org.apache.log4j.ConsoleAppender
-log4j.appender.keycloak.layout=org.apache.log4j.PatternLayout
-log4j.appender.keycloak.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n
-
-log4j.appender.testsuite=org.apache.log4j.ConsoleAppender
-log4j.appender.testsuite.layout=org.apache.log4j.PatternLayout
-log4j.appender.testsuite.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%C{1}] %m%n
+log4j.appender.keycloak.layout=org.apache.log4j.EnhancedPatternLayout
+keycloak.testsuite.logging.pattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n
+log4j.appender.keycloak.layout.ConversionPattern=${keycloak.testsuite.logging.pattern}
# Logging with "info" when running test from IDE, but disabled when running test with "mvn" . Both cases can be overriden by use system property "keycloak.logging.level" (eg. -Dkeycloak.logging.level=debug )
-keycloak.logging.level=info
-log4j.logger.org.keycloak=${keycloak.logging.level}
+log4j.logger.org.keycloak=${keycloak.logging.level:info}
log4j.logger.org.jboss.resteasy.resteasy_jaxrs.i18n=off
#log4j.logger.org.keycloak.keys.DefaultKeyManager=trace
#log4j.logger.org.keycloak.services.managers.AuthenticationManager=trace
-log4j.logger.org.keycloak.testsuite=debug, testsuite
-log4j.additivity.org.keycloak.testsuite=false
+keycloak.testsuite.logging.level=debug
+log4j.logger.org.keycloak.testsuite=${keycloak.testsuite.logging.level}
+
+log4j.logger.org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancer=info
# Enable to view events
# log4j.logger.org.keycloak.events=debug
@@ -54,6 +52,13 @@ log4j.logger.org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory=de
# log4j.logger.org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory=debug
# log4j.logger.org.keycloak.migration.MigrationModelManager=debug
+keycloak.infinispan.logging.level=info
+log4j.logger.org.keycloak.cluster.infinispan=${keycloak.infinispan.logging.level}
+log4j.logger.org.keycloak.connections.infinispan=${keycloak.infinispan.logging.level}
+log4j.logger.org.keycloak.keys.infinispan=${keycloak.infinispan.logging.level}
+log4j.logger.org.keycloak.models.cache.infinispan=${keycloak.infinispan.logging.level}
+log4j.logger.org.keycloak.models.sessions.infinispan=${keycloak.infinispan.logging.level}
+
# Enable to view kerberos/spnego logging
# log4j.logger.org.keycloak.broker.kerberos=trace
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
index 7a0e6b6..22d5ebc 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
@@ -108,7 +108,8 @@
"connectionsInfinispan": {
"default": {
"jgroupsUdpMcastAddr": "${keycloak.connectionsInfinispan.jgroupsUdpMcastAddr:234.56.78.90}",
- "nodeName": "${keycloak.connectionsInfinispan.nodeName,jboss.node.name:defaultNodeName}",
+ "nodeName": "${keycloak.connectionsInfinispan.nodeName,jboss.node.name:}",
+ "siteName": "${keycloak.connectionsInfinispan.siteName,jboss.site.name:}",
"clustered": "${keycloak.connectionsInfinispan.clustered:false}",
"async": "${keycloak.connectionsInfinispan.async:false}",
"sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}",
@@ -118,13 +119,6 @@
"remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
}
},
-
- "stickySessionEncoder": {
- "infinispan": {
- "nodeName": "${keycloak.stickySessionEncoder.nodeName,jboss.node.name:defaultNodeName}"
- }
- },
-
"truststore": {
"file": {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
index f4b118e..2ce6b39 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
@@ -148,6 +148,17 @@
"secret": "password"
},
{
+ "clientId": "root-url-client",
+ "enabled": true,
+ "rootUrl": "http://localhost:8180/foo/bar",
+ "adminUrl": "http://localhost:8180/foo/bar",
+ "baseUrl": "/baz",
+ "redirectUris": [
+ "http://localhost:8180/foo/bar/*"
+ ],
+ "secret": "password"
+ },
+ {
"clientId" : "test-app-scope",
"enabled": true,
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/pom.xml
index 6d9c234..ebb0293 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-as7</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/pom.xml
index 201c085..6e06b83 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-eap</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml
index 9faeaf9..16ae5fd 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-eap6</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6-fuse/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6-fuse/pom.xml
index 82f387b..9f5a988 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6-fuse/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6-fuse/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-eap6-fuse</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/pom.xml
index 091aba2..45d6cd1 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-jboss</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/pom.xml
index 7c0fa8a..7ef888e 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-jboss-relative</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-relative-eap</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/pom.xml
index 0333a30..306ee67 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<packaging>pom</packaging>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/pom.xml
index f943f8b..e838764 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-jboss-relative</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-relative-wildfly</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/pom.xml
index 7e44ddd..922bb86 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-remote</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml
index afa7e8b..3a6e545 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-wildfly</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml
index 486da62..62c011c 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml
@@ -24,10 +24,10 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
- <artifactId>integration-arquillian-tests-adapters-wildfly</artifactId>
+ <artifactId>integration-arquillian-tests-adapters-wildfly10</artifactId>
<name>Adapter Tests - JBoss - Wildfly 10</name>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/pom.xml
index 299b015..ea362d4 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-wildfly8</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/pom.xml
index 454e364..af851f2 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-jboss</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-wildfly9</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse61/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse61/pom.xml
index f332d23..a206f01 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse61/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse61/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-karaf</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-fuse61</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse62/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse62/pom.xml
index 561eb4c..62e8607 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse62/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse62/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-karaf</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-fuse62</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse63/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse63/pom.xml
index 0a438fe..bd0e11e 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse63/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse63/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-karaf</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-fuse63</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/karaf3/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/karaf3/pom.xml
index 3af5010..f86b72c 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/karaf/karaf3/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/karaf3/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-karaf</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-karaf3</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml
index fdd9523..433e642 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-karaf</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
index 70533a9..6cfc621 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-other</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters</artifactId>
@@ -98,6 +98,8 @@
<module>jboss</module>
<module>karaf</module>
<module>tomcat</module>
+ <module>was</module>
+ <module>wls</module>
</modules>
<profiles>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/tomcat/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/tomcat/pom.xml
index fd0b3a2..d5fd8c8 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/tomcat/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/tomcat/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-tomcat</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat7/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat7/pom.xml
index a8e55b8..5029854 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat7/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat7/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-tomcat</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-tomcat7</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat8/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat8/pom.xml
index 6ef1449..32574f8 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat8/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat8/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-tomcat</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-tomcat8</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat9/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat9/pom.xml
index 5268feb..c487634 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat9/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat9/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-adapters-tomcat</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-adapters-tomcat9</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/was/common/xslt/arquillian.xsl b/testsuite/integration-arquillian/tests/other/adapters/was/common/xslt/arquillian.xsl
new file mode 100644
index 0000000..420a0fb
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/was/common/xslt/arquillian.xsl
@@ -0,0 +1,53 @@
+<!--
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:xalan="http://xml.apache.org/xalan"
+ xmlns:a="http://jboss.org/schema/arquillian"
+ version="2.0"
+ exclude-result-prefixes="xalan a">
+
+ <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" xalan:indent-amount="4" standalone="no"/>
+ <xsl:strip-space elements="*"/>
+
+ <xsl:template match="/a:arquillian">
+ <xsl:copy>
+ <xsl:apply-templates select="node()|@*"/>
+
+ <container qualifier="app-server-was" mode="manual">
+ <configuration>
+ <property name="enabled">true</property>
+ <property name="remoteServerAddress">localhost</property>
+ <property name="remoteServerSoapPort">8880</property>
+ <property name="securityEnabled">false</property>
+ <property name="username">admin</property>
+ <property name="adapterImplClass">org.jboss.arquillian.container.was.remote_8_5.WebSphereRemoteContainer</property>
+ </configuration>
+ </container>
+
+ </xsl:copy>
+ </xsl:template>
+
+
+ <xsl:template match="@*|node()">
+ <xsl:copy>
+ <xsl:apply-templates select="@*|node()" />
+ </xsl:copy>
+ </xsl:template>
+
+
+</xsl:stylesheet>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/adapters/was/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/was/pom.xml
new file mode 100644
index 0000000..c5b96a2
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/was/pom.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0"?>
+<!--
+~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+~ and other contributors as indicated by the @author tags.
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak.testsuite</groupId>
+ <artifactId>integration-arquillian-tests-adapters</artifactId>
+ <version>3.3.0.CR1-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>integration-arquillian-tests-adapters-was</artifactId>
+
+ <packaging>pom</packaging>
+
+ <name>Adapter Tests - WAS</name>
+
+ <profiles>
+ <profile>
+ <id>app-server-was</id>
+ <modules>
+ <module>was8</module>
+ </modules>
+ </profile>
+ </profiles>
+
+</project>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/adapters/was/README.md b/testsuite/integration-arquillian/tests/other/adapters/was/README.md
new file mode 100644
index 0000000..ae7afce
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/was/README.md
@@ -0,0 +1,19 @@
+# Keycloak Arquillian WebSphere AS Integration Testsuite
+
+- arquillian-was-remote-8.5-custom container is used for deploying artifacts to running WebSphere server
+- arquillian-was-remote-8.5-custom is based on arquillian-was-remote-8.5 and solves some ibm dependency issues
+- arquillian-was-remote-8.5-custom can be downloaded from this [repo](https://repository.jboss.org/nexus/content/repositories/jboss_releases_staging_profile-11801)
+- more info about arquillian-was-remote-8.5-custom:
+ - There is the [artifact](https://github.com/vramik/arquillian-container-was/blob/custom/was-remote-8.5/pom.xml#L17)
+ - This is a [profile](https://github.com/vramik/arquillian-container-was/blob/custom/pom.xml#L108-L114) to activate
+ - To build `ws-dependencies` module it is required to specify `lib_location` property where directory `lib` is located. The `lib` has to contain `com.ibm.ws.admin.client_8.5.0.jar` and `com.ibm.ws.orb_8.5.0.jar` which are part of WebSphere AS installation
+ - see [pom.xml](https://github.com/vramik/arquillian-container-was/blob/custom/ws-dependencies/pom.xml) for more details
+ - note: to solve classpath conflicts the package javax/ws from within `com.ibm.ws.admin.client_8.5.0.jar` has to be removed
+
+## How to run tests
+
+1. start IBM WebSphere container with ibmjdk8 (tests expects that app-server runs on port 8280)
+2. add the [repository](https://repository.jboss.org/nexus/content/repositories/jboss_releases_staging_profile-11801) to settings.xml
+3. mvn -f keycloak/pom.xml -Pdistribution -DskipTests clean install
+4. mvn -f keycloak/testsuite/integration-arquillian/pom.xml -Pauth-server-wildfly -DskipTests clean install
+5. mvn -f keycloak/testsuite/integration-arquillian/tests/other/adapters/was/pom.xml -Pauth-server-wildfly,app-server-was clean install
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/adapters/was/was8/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/was/was8/pom.xml
new file mode 100644
index 0000000..ad138d3
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/was/was8/pom.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+<!--
+~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+~ and other contributors as indicated by the @author tags.
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak.testsuite</groupId>
+ <artifactId>integration-arquillian-tests-adapters-was</artifactId>
+ <version>3.3.0.CR1-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>integration-arquillian-tests-adapters-was8</artifactId>
+
+ <name>Adapter Tests - WAS8</name>
+
+ <properties>
+ <common.resources>${project.parent.basedir}/common</common.resources>
+ <app.server>was</app.server>
+ <app.server.type>remote</app.server.type>
+ <app.server.skip.unpack>true</app.server.skip.unpack>
+ </properties>
+
+ <dependencies>
+ <!--check module's README.md to learn more about the dependency-->
+ <dependency>
+ <groupId>org.jboss.arquillian.container</groupId>
+ <artifactId>arquillian-was-remote-8.5-custom</artifactId>
+ <version>1.0.0.Final</version>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/adapters/was/was8/src/test/java/org/keycloak/testsuite/adapter/WASSAMLFilterAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/was/was8/src/test/java/org/keycloak/testsuite/adapter/WASSAMLFilterAdapterTest.java
new file mode 100644
index 0000000..3c1fb19
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/was/was8/src/test/java/org/keycloak/testsuite/adapter/WASSAMLFilterAdapterTest.java
@@ -0,0 +1,9 @@
+package org.keycloak.testsuite.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractSAMLFilterServletAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+@AppServerContainer("app-server-was")
+public class WASSAMLFilterAdapterTest extends AbstractSAMLFilterServletAdapterTest {
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/wls/common/xslt/arquillian.xsl b/testsuite/integration-arquillian/tests/other/adapters/wls/common/xslt/arquillian.xsl
new file mode 100644
index 0000000..d34cc0c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/wls/common/xslt/arquillian.xsl
@@ -0,0 +1,54 @@
+<!--
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:xalan="http://xml.apache.org/xalan"
+ xmlns:a="http://jboss.org/schema/arquillian"
+ version="2.0"
+ exclude-result-prefixes="xalan a">
+
+ <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" xalan:indent-amount="4" standalone="no"/>
+ <xsl:strip-space elements="*"/>
+
+ <xsl:template match="/a:arquillian">
+ <xsl:copy>
+ <xsl:apply-templates select="node()|@*"/>
+
+ <container qualifier="app-server-wls" mode="manual">
+ <configuration>
+ <property name="enabled">true</property>
+ <property name="adapterImplClass">org.jboss.arquillian.container.wls.remote_12_1_2.WebLogicContainer</property>
+ <property name="adminUrl">t3://localhost:8280/</property>
+ <property name="adminUserName">weblogic</property>
+ <property name="adminPassword">weblogic1</property>
+ <property name="target">AdminServer</property>
+ <property name="wlHome">/home/jenkins/Oracle/Middleware/Oracle_Home/wlserver</property>
+ </configuration>
+ </container>
+
+ </xsl:copy>
+ </xsl:template>
+
+
+ <xsl:template match="@*|node()">
+ <xsl:copy>
+ <xsl:apply-templates select="@*|node()" />
+ </xsl:copy>
+ </xsl:template>
+
+
+</xsl:stylesheet>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/adapters/wls/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/wls/pom.xml
new file mode 100644
index 0000000..85e33bc
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/wls/pom.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0"?>
+<!--
+~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+~ and other contributors as indicated by the @author tags.
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak.testsuite</groupId>
+ <artifactId>integration-arquillian-tests-adapters</artifactId>
+ <version>3.3.0.CR1-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>integration-arquillian-tests-adapters-wls</artifactId>
+
+ <packaging>pom</packaging>
+
+ <name>Adapter Tests - WLS</name>
+
+ <profiles>
+ <profile>
+ <id>app-server-wls</id>
+ <modules>
+ <module>wls12</module>
+ </modules>
+ </profile>
+ </profiles>
+
+</project>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/pom.xml
new file mode 100644
index 0000000..afade15
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/pom.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<!--
+~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+~ and other contributors as indicated by the @author tags.
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak.testsuite</groupId>
+ <artifactId>integration-arquillian-tests-adapters-wls</artifactId>
+ <version>3.3.0.CR1-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>integration-arquillian-tests-adapters-wls12</artifactId>
+
+ <name>Adapter Tests - WLS12</name>
+
+ <properties>
+ <common.resources>${project.parent.basedir}/common</common.resources>
+ <app.server>wls</app.server>
+ <app.server.type>remote</app.server.type>
+ <app.server.skip.unpack>true</app.server.skip.unpack>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.jboss.arquillian.container</groupId>
+ <artifactId>arquillian-wls-remote-12.1.x</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/src/test/java/org/keycloak/testsuite/adapter/WLSSAMLFilterAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/src/test/java/org/keycloak/testsuite/adapter/WLSSAMLFilterAdapterTest.java
new file mode 100644
index 0000000..ad2a92f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/src/test/java/org/keycloak/testsuite/adapter/WLSSAMLFilterAdapterTest.java
@@ -0,0 +1,9 @@
+package org.keycloak.testsuite.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractSAMLFilterServletAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+@AppServerContainer("app-server-wls")
+public class WLSSAMLFilterAdapterTest extends AbstractSAMLFilterServletAdapterTest {
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/clean-start/pom.xml b/testsuite/integration-arquillian/tests/other/clean-start/pom.xml
index 23d2b61..72eee78 100644
--- a/testsuite/integration-arquillian/tests/other/clean-start/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/clean-start/pom.xml
@@ -23,7 +23,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-other</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-smoke-clean-start</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/console/pom.xml b/testsuite/integration-arquillian/tests/other/console/pom.xml
index 482d482..003e24c 100644
--- a/testsuite/integration-arquillian/tests/other/console/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/console/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-other</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-console</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java
index 7e4c29c..09f5f1c 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java
@@ -85,4 +85,60 @@ public class RequiredActions extends Authentication {
public void setUpdateProfileDefaultAction(boolean value) {
setRequiredActionDefaultValue(UPDATE_PROFILE, value);
}
+
+ private boolean getRequiredActionValue(String id) {
+ WaitUtils.waitUntilElement(requiredActionTable).is().present();
+
+ WebElement checkbox = requiredActionTable.findElement(By.id(id));
+
+ return checkbox.isSelected();
+ }
+
+ private boolean getRequiredActionEnabledValue(String id) {
+ return getRequiredActionValue(id + ENABLED);
+ }
+
+ private boolean getRequiredActionDefaultValue(String id) {
+ return getRequiredActionValue(id + DEFAULT);
+ }
+
+ public boolean getTermsAndConditionEnabled() {
+ return getRequiredActionEnabledValue(TERMS_AND_CONDITIONS);
+ }
+
+ public boolean getTermsAndConditionDefaultAction() {
+ return getRequiredActionDefaultValue(TERMS_AND_CONDITIONS);
+ }
+
+ public boolean getVerifyEmailEnabled() {
+ return getRequiredActionEnabledValue(VERIFY_EMAIL);
+ }
+
+ public boolean getVerifyEmailDefaultAction() {
+ return getRequiredActionDefaultValue(VERIFY_EMAIL);
+ }
+
+ public boolean getUpdatePasswordEnabled() {
+ return getRequiredActionEnabledValue(UPDATE_PASSWORD);
+ }
+
+ public boolean getUpdatePasswordDefaultAction() {
+ return getRequiredActionDefaultValue(UPDATE_PASSWORD);
+ }
+
+ public boolean getConfigureTotpEnabled() {
+ return getRequiredActionEnabledValue(CONFIGURE_TOTP);
+ }
+
+ public boolean getConfigureTotpDefaultAction() {
+ return getRequiredActionDefaultValue(CONFIGURE_TOTP);
+ }
+
+ public boolean getUpdateProfileEnabled() {
+ return getRequiredActionEnabledValue(UPDATE_PROFILE);
+ }
+
+ public boolean getUpdateProfileDefaultAction() {
+ return getRequiredActionDefaultValue(UPDATE_PROFILE);
+ }
}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java
index a6c9527..de9c13c 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java
@@ -25,7 +25,9 @@ import org.keycloak.representations.idm.authorization.ResourcePermissionRepresen
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
import org.keycloak.testsuite.console.page.clients.authorization.policy.PolicyTypeUI;
import org.keycloak.testsuite.page.Form;
+import org.keycloak.testsuite.util.URLUtils;
import org.keycloak.testsuite.util.WaitUtils;
+import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.ui.Select;
@@ -58,11 +60,9 @@ public class Permissions extends Form {
if ("resource".equals(type)) {
resourcePermission.form().populate((ResourcePermissionRepresentation) expected);
- resourcePermission.form().save();
return (P) resourcePermission;
} else if ("scope".equals(type)) {
scopePermission.form().populate((ScopePermissionRepresentation) expected);
- scopePermission.form().save();
return (P) scopePermission;
}
@@ -73,7 +73,7 @@ public class Permissions extends Form {
for (WebElement row : permissions().rows()) {
PolicyRepresentation actual = permissions().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
String type = representation.getType();
@@ -92,7 +92,7 @@ public class Permissions extends Form {
for (WebElement row : permissions().rows()) {
PolicyRepresentation actual = permissions().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
String type = actual.getType();
if ("resource".equals(type)) {
@@ -109,7 +109,7 @@ public class Permissions extends Form {
for (WebElement row : permissions().rows()) {
PolicyRepresentation actual = permissions().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
String type = actual.getType();
@@ -124,4 +124,15 @@ public class Permissions extends Form {
}
}
}
+
+ public void deleteFromList(String name) {
+ for (WebElement row : permissions().rows()) {
+ PolicyRepresentation actual = permissions().toRepresentation(row);
+ if (actual.getName().equalsIgnoreCase(name)) {
+ row.findElements(tagName("td")).get(4).click();
+ driver.findElement(By.xpath(".//button[text()='Delete']")).click();
+ return;
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java
index cf39523..e31ac2a 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java
@@ -18,6 +18,7 @@ package org.keycloak.testsuite.console.page.clients.authorization.permission;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2;
import org.keycloak.testsuite.console.page.fragment.OnOffSwitch;
import org.keycloak.testsuite.page.Form;
@@ -48,8 +49,8 @@ public class ResourcePermissionForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
@FindBy(id = "s2id_policies")
private MultipleStringSelect2 policySelect;
@@ -78,7 +79,7 @@ public class ResourcePermissionForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public ResourcePermissionRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java
index f16cd5c..deb7f06 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java
@@ -21,6 +21,7 @@ import java.util.function.Function;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2;
import org.keycloak.testsuite.console.page.fragment.SingleStringSelect2;
import org.keycloak.testsuite.page.Form;
@@ -45,8 +46,8 @@ public class ScopePermissionForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
@FindBy(id = "s2id_policies")
private MultipleStringSelect2 policySelect;
@@ -81,7 +82,7 @@ public class ScopePermissionForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public ScopePermissionRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java
index 5e7170d..12c4289 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java
@@ -20,6 +20,7 @@ import java.util.Set;
import org.keycloak.representations.idm.authorization.AggregatePolicyRepresentation;
import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.WebElement;
@@ -46,8 +47,8 @@ public class AggregatePolicyForm extends Form {
@FindBy(id = "s2id_policies")
private MultipleStringSelect2 policySelect;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(AggregatePolicyRepresentation expected) {
setInputValue(name, expected.getName());
@@ -83,7 +84,7 @@ public class AggregatePolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public AggregatePolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java
index 9095a32..cedacee 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java
@@ -25,6 +25,7 @@ import java.util.stream.Collectors;
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.By;
@@ -52,8 +53,8 @@ public class ClientPolicyForm extends Form {
@FindBy(id = "s2id_clients")
private ClientSelect clientsInput;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(ClientPolicyRepresentation expected) {
setInputValue(name, expected.getName());
@@ -67,7 +68,7 @@ public class ClientPolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public ClientPolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java
index e83585b..9c1c1ea 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java
@@ -18,6 +18,7 @@ package org.keycloak.testsuite.console.page.clients.authorization.policy;
import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebElement;
@@ -41,8 +42,8 @@ public class JSPolicyForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(JSPolicyRepresentation expected) {
setInputValue(name, expected.getName());
@@ -58,7 +59,7 @@ public class JSPolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public JSPolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
index 7be563e..a42e12e 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
@@ -30,7 +30,9 @@ import org.keycloak.representations.idm.authorization.RulePolicyRepresentation;
import org.keycloak.representations.idm.authorization.TimePolicyRepresentation;
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
import org.keycloak.testsuite.page.Form;
+import org.keycloak.testsuite.util.URLUtils;
import org.keycloak.testsuite.util.WaitUtils;
+import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.ui.Select;
@@ -81,31 +83,24 @@ public class Policies extends Form {
if ("role".equals(type)) {
rolePolicy.form().populate((RolePolicyRepresentation) expected);
- rolePolicy.form().save();
return (P) rolePolicy;
} else if ("user".equals(type)) {
userPolicy.form().populate((UserPolicyRepresentation) expected);
- userPolicy.form().save();
return (P) userPolicy;
} else if ("aggregate".equals(type)) {
aggregatePolicy.form().populate((AggregatePolicyRepresentation) expected);
- aggregatePolicy.form().save();
return (P) aggregatePolicy;
} else if ("js".equals(type)) {
jsPolicy.form().populate((JSPolicyRepresentation) expected);
- jsPolicy.form().save();
return (P) jsPolicy;
} else if ("time".equals(type)) {
timePolicy.form().populate((TimePolicyRepresentation) expected);
- timePolicy.form().save();
return (P) timePolicy;
} else if ("rules".equals(type)) {
rulePolicy.form().populate((RulePolicyRepresentation) expected);
- rulePolicy.form().save();
return (P) rulePolicy;
} else if ("client".equals(type)) {
clientPolicy.form().populate((ClientPolicyRepresentation) expected);
- clientPolicy.form().save();
return (P) clientPolicy;
} else if ("group".equals(type)) {
groupPolicy.form().populate((GroupPolicyRepresentation) expected);
@@ -120,7 +115,7 @@ public class Policies extends Form {
for (WebElement row : policies().rows()) {
PolicyRepresentation actual = policies().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
String type = representation.getType();
@@ -151,8 +146,7 @@ public class Policies extends Form {
for (WebElement row : policies().rows()) {
PolicyRepresentation actual = policies().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
- WaitUtils.waitForPageToLoad(driver);
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
String type = actual.getType();
if ("role".equals(type)) {
return (P) rolePolicy;
@@ -180,8 +174,7 @@ public class Policies extends Form {
for (WebElement row : policies().rows()) {
PolicyRepresentation actual = policies().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
- WaitUtils.waitForPageToLoad(driver);
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
String type = actual.getType();
@@ -207,4 +200,15 @@ public class Policies extends Form {
}
}
}
+
+ public void deleteFromList(String name) {
+ for (WebElement row : policies().rows()) {
+ PolicyRepresentation actual = policies().toRepresentation(row);
+ if (actual.getName().equalsIgnoreCase(name)) {
+ row.findElements(tagName("td")).get(4).click();
+ driver.findElement(By.xpath(".//button[text()='Delete']")).click();
+ return;
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java
index 8b6f114..f917678 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
import org.keycloak.representations.idm.authorization.Logic;
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
import org.keycloak.testsuite.console.page.fragment.AbstractMultipleSelect2;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
@@ -60,8 +61,8 @@ public class RolePolicyForm extends Form {
@FindBy(id = "s2id_clientRoles")
private ClientRoleSelect clientRoleSelect;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(RolePolicyRepresentation expected) {
setInputValue(name, expected.getName());
@@ -115,7 +116,7 @@ public class RolePolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public RolePolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java
index 17b4d46..0ba43f1 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java
@@ -18,6 +18,7 @@ package org.keycloak.testsuite.console.page.clients.authorization.policy;
import org.keycloak.representations.idm.authorization.Logic;
import org.keycloak.representations.idm.authorization.RulePolicyRepresentation;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.page.Form;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.WebElement;
@@ -62,8 +63,8 @@ public class RulePolicyForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
@FindBy(id = "resolveModule")
private WebElement resolveModuleButton;
@@ -92,7 +93,7 @@ public class RulePolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public RulePolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java
index 5c31f33..47be24d 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java
@@ -18,6 +18,7 @@ package org.keycloak.testsuite.console.page.clients.authorization.policy;
import org.keycloak.representations.idm.authorization.TimePolicyRepresentation;
import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -77,8 +78,8 @@ public class TimePolicyForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(TimePolicyRepresentation expected) {
setInputValue(name, expected.getName());
@@ -102,7 +103,7 @@ public class TimePolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public TimePolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java
index e403d1b..ec24ace 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java
@@ -25,6 +25,7 @@ import java.util.stream.Collectors;
import org.keycloak.representations.idm.authorization.Logic;
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.By;
@@ -52,8 +53,8 @@ public class UserPolicyForm extends Form {
@FindBy(id = "s2id_users")
private UserSelect usersInput;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(UserPolicyRepresentation expected) {
setInputValue(name, expected.getName());
@@ -67,7 +68,7 @@ public class UserPolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public UserPolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java
index 8f4a66f..c4d2b2b 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java
@@ -23,6 +23,7 @@ import java.util.Set;
import org.jboss.arquillian.graphene.fragment.Root;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.page.Form;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.By;
@@ -54,8 +55,8 @@ public class ResourceForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
@FindBy(id = "s2id_scopes")
private ScopesInput scopesInput;
@@ -94,7 +95,7 @@ public class ResourceForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public ResourceRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java
index 280af3f..199be95 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java
@@ -21,7 +21,9 @@ import static org.openqa.selenium.By.tagName;
import org.jboss.arquillian.graphene.page.Page;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.testsuite.page.Form;
+import org.keycloak.testsuite.util.URLUtils;
import org.keycloak.testsuite.util.WaitUtils;
+import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -52,7 +54,7 @@ public class Resources extends Form {
for (WebElement row : resources().rows()) {
ResourceRepresentation actual = resources().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
resource.form().populate(representation);
return;
@@ -64,7 +66,7 @@ public class Resources extends Form {
for (WebElement row : resources().rows()) {
ResourceRepresentation actual = resources().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
resource.form().delete();
return;
@@ -72,11 +74,22 @@ public class Resources extends Form {
}
}
+ public void deleteFromList(String name) {
+ for (WebElement row : resources().rows()) {
+ ResourceRepresentation actual = resources().toRepresentation(row);
+ if (actual.getName().equalsIgnoreCase(name)) {
+ row.findElements(tagName("td")).get(6).click();
+ driver.findElement(By.xpath(".//button[text()='Delete']")).click();
+ return;
+ }
+ }
+ }
+
public Resource name(String name) {
for (WebElement row : resources().rows()) {
ResourceRepresentation actual = resources().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
return resource;
}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java
index ed01a2b..29ec514 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java
@@ -17,6 +17,7 @@
package org.keycloak.testsuite.console.page.clients.authorization.scope;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -35,8 +36,8 @@ public class ScopeForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(ScopeRepresentation expected) {
setInputValue(name, expected.getName());
@@ -46,6 +47,6 @@ public class ScopeForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java
index a59869c..3974e35 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java
@@ -21,6 +21,8 @@ import static org.openqa.selenium.By.tagName;
import org.jboss.arquillian.graphene.page.Page;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.testsuite.page.Form;
+import org.keycloak.testsuite.util.URLUtils;
+import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -51,7 +53,7 @@ public class Scopes extends Form {
for (WebElement row : scopes().rows()) {
ScopeRepresentation actual = scopes().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
scope.form().populate(representation);
}
}
@@ -61,9 +63,19 @@ public class Scopes extends Form {
for (WebElement row : scopes().rows()) {
ScopeRepresentation actual = scopes().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
scope.form().delete();
}
}
}
+
+ public void deleteFromList(String name) {
+ for (WebElement row : scopes().rows()) {
+ ScopeRepresentation actual = scopes().toRepresentation(row);
+ if (actual.getName().equalsIgnoreCase(name)) {
+ row.findElements(tagName("td")).get(3).click();
+ driver.findElement(By.xpath(".//button[text()='Delete']")).click();
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/users/Users.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/users/Users.java
index 7e4f417..4232289 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/users/Users.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/users/Users.java
@@ -53,7 +53,7 @@ public class Users extends AdminConsoleRealm {
public static final String IMPERSONATE = "Impersonate";
public static final String DELETE = "Delete";
- @FindBy(xpath = "//div[./h1[text()='Users']]/table")
+ @FindBy(id = "user-table")
private UsersTable table;
public UsersTable table() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java
index b8217f8..b879e18 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java
@@ -21,8 +21,11 @@ import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.auth.page.login.Registration;
import org.keycloak.testsuite.console.AbstractConsoleTest;
+import org.keycloak.testsuite.console.page.AdminConsoleRealm;
import org.keycloak.testsuite.console.page.authentication.RequiredActions;
import org.keycloak.testsuite.console.page.realm.LoginSettings;
import org.openqa.selenium.By;
@@ -72,6 +75,52 @@ public class RequiredActionsTest extends AbstractConsoleTest {
}
@Test
+ public void defaultCheckboxUncheckableWhenEnabledIsFalse() {
+ requiredActionsPage.setTermsAndConditionEnabled(false);
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionEnabled());
+ requiredActionsPage.setTermsAndConditionDefaultAction(true);
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction());
+ }
+
+ @Test
+ public void defaultCheckboxUncheckedWhenEnabledBecomesFalse() {
+ requiredActionsPage.setTermsAndConditionEnabled(true);
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled());
+ requiredActionsPage.setTermsAndConditionDefaultAction(true);
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionDefaultAction());
+ requiredActionsPage.setTermsAndConditionEnabled(false);
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction());
+ assertAlertSuccess();
+ }
+
+ @Test
+ public void defaultCheckboxKeepsValueWhenEnabledIsToggled() {
+ requiredActionsPage.setTermsAndConditionEnabled(true);
+ requiredActionsPage.setTermsAndConditionDefaultAction(false);
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction());
+ requiredActionsPage.setTermsAndConditionEnabled(false);
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction());
+ requiredActionsPage.setTermsAndConditionEnabled(true);
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction());
+
+ requiredActionsPage.setTermsAndConditionDefaultAction(true);
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionDefaultAction());
+ requiredActionsPage.setTermsAndConditionEnabled(false);
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction());
+ requiredActionsPage.setTermsAndConditionEnabled(true);
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionDefaultAction());
+
+ assertAlertSuccess();
+ }
+
+ @Test
public void configureTotpDefaultActionTest() {
requiredActionsPage.setConfigureTotpDefaultAction(true);
assertAlertSuccess();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/AggregatePolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/AggregatePolicyManagementTest.java
index f1bba0d..be2a984 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/AggregatePolicyManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/AggregatePolicyManagementTest.java
@@ -122,6 +122,22 @@ public class AggregatePolicyManagementTest extends AbstractAuthorizationSettings
assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
}
+ @Test
+ public void testDeleteFromList() throws InterruptedException {
+ authorizationPage.navigateTo();
+ AggregatePolicyRepresentation expected = new AggregatePolicyRepresentation();
+
+ expected.setName("Test Delete Aggregate Policy");
+ expected.setDescription("description");
+ expected.addPolicy("Policy C");
+
+ expected = createPolicy(expected);
+ authorizationPage.navigateTo();
+ authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName());
+ authorizationPage.navigateTo();
+ assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
+ }
+
private AggregatePolicyRepresentation createPolicy(AggregatePolicyRepresentation expected) {
AggregatePolicy policy = authorizationPage.authorizationTabs().policies().create(expected);
assertAlertSuccess();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ClientPolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ClientPolicyManagementTest.java
index 2c95b83..04e9826 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ClientPolicyManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ClientPolicyManagementTest.java
@@ -76,7 +76,7 @@ public class ClientPolicyManagementTest extends AbstractAuthorizationSettingsTes
}
@Test
- public void testDeletePolicy() throws InterruptedException {
+ public void testDelete() throws InterruptedException {
authorizationPage.navigateTo();
ClientPolicyRepresentation expected = new ClientPolicyRepresentation();
@@ -92,6 +92,22 @@ public class ClientPolicyManagementTest extends AbstractAuthorizationSettingsTes
assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
}
+ @Test
+ public void testDeleteFromList() throws InterruptedException {
+ authorizationPage.navigateTo();
+ ClientPolicyRepresentation expected = new ClientPolicyRepresentation();
+
+ expected.setName("Test Client Policy");
+ expected.setDescription("description");
+ expected.addClient("client c");
+
+ expected = createPolicy(expected);
+ authorizationPage.navigateTo();
+ authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName());
+ authorizationPage.navigateTo();
+ assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
+ }
+
private ClientPolicyRepresentation createPolicy(ClientPolicyRepresentation expected) {
ClientPolicy policy = authorizationPage.authorizationTabs().policies().create(expected);
assertAlertSuccess();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java
index e8b05bf..91c86f9 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java
@@ -135,6 +135,25 @@ public class GroupPolicyManagementTest extends AbstractAuthorizationSettingsTest
assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
}
+ @Test
+ public void testDeleteFromList() throws InterruptedException {
+ authorizationPage.navigateTo();
+ GroupPolicyRepresentation expected = new GroupPolicyRepresentation();
+
+ expected.setName("Test Delete Group Policy");
+ expected.setDescription("description");
+ expected.setGroupsClaim("groups");
+ expected.addGroupPath("/Group A", true);
+ expected.addGroupPath("/Group A/Group B/Group D");
+ expected.addGroupPath("Group F");
+
+ expected = createPolicy(expected);
+ authorizationPage.navigateTo();
+ authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName());
+ authorizationPage.navigateTo();
+ assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
+ }
+
private GroupPolicyRepresentation createPolicy(GroupPolicyRepresentation expected) {
GroupPolicy policy = authorizationPage.authorizationTabs().policies().create(expected);
assertAlertSuccess();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/JSPolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/JSPolicyManagementTest.java
index 0b9113c..6da809c 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/JSPolicyManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/JSPolicyManagementTest.java
@@ -74,6 +74,22 @@ public class JSPolicyManagementTest extends AbstractAuthorizationSettingsTest {
assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
}
+ @Test
+ public void testDeleteFromList() throws InterruptedException {
+ authorizationPage.navigateTo();
+ JSPolicyRepresentation expected = new JSPolicyRepresentation();
+
+ expected.setName("Test JS Policy");
+ expected.setDescription("description");
+ expected.setCode("$evaluation.deny();");
+
+ expected = createPolicy(expected);
+ authorizationPage.navigateTo();
+ authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName());
+ authorizationPage.navigateTo();
+ assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
+ }
+
private JSPolicyRepresentation createPolicy(JSPolicyRepresentation expected) {
JSPolicy policy = authorizationPage.authorizationTabs().policies().create(expected);
assertAlertSuccess();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java
index 75a479a..3d29c03 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java
@@ -72,7 +72,7 @@ public class ResourceManagementTest extends AbstractAuthorizationSettingsTest {
}
@Test
- public void testDelete() {
+ public void testDeleteFromDetails() {
ResourceRepresentation expected = createResource();
authorizationPage.navigateTo();
authorizationPage.authorizationTabs().resources().delete(expected.getName());
@@ -80,6 +80,15 @@ public class ResourceManagementTest extends AbstractAuthorizationSettingsTest {
assertNull(authorizationPage.authorizationTabs().resources().resources().findByName(expected.getName()));
}
+ @Test
+ public void testDeleteFromList() {
+ ResourceRepresentation expected = createResource();
+ authorizationPage.navigateTo();
+ authorizationPage.authorizationTabs().resources().deleteFromList(expected.getName());
+ authorizationPage.navigateTo();
+ assertNull(authorizationPage.authorizationTabs().resources().resources().findByName(expected.getName()));
+ }
+
private ResourceRepresentation createResource() {
ResourceRepresentation expected = new ResourceRepresentation();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourcePermissionManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourcePermissionManagementTest.java
index 4ff011a..f6a967e 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourcePermissionManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourcePermissionManagementTest.java
@@ -165,6 +165,23 @@ public class ResourcePermissionManagementTest extends AbstractAuthorizationSetti
assertNull(authorizationPage.authorizationTabs().permissions().permissions().findByName(expected.getName()));
}
+ @Test
+ public void testDeleteFromList() throws InterruptedException {
+ authorizationPage.navigateTo();
+ ResourcePermissionRepresentation expected = new ResourcePermissionRepresentation();
+
+ expected.setName("Test Delete Resource Permission");
+ expected.setDescription("description");
+ expected.addResource("Resource B");
+ expected.addPolicy("Policy C");
+
+ expected = createPermission(expected);
+ authorizationPage.navigateTo();
+ authorizationPage.authorizationTabs().permissions().deleteFromList(expected.getName());
+ authorizationPage.navigateTo();
+ assertNull(authorizationPage.authorizationTabs().permissions().permissions().findByName(expected.getName()));
+ }
+
private ResourcePermissionRepresentation createPermission(ResourcePermissionRepresentation expected) {
ResourcePermission policy = authorizationPage.authorizationTabs().permissions().create(expected);
assertAlertSuccess();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RolePolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RolePolicyManagementTest.java
index 44e4f70..e8794cc 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RolePolicyManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RolePolicyManagementTest.java
@@ -208,6 +208,24 @@ public class RolePolicyManagementTest extends AbstractAuthorizationSettingsTest
assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
}
+ @Test
+ public void testDeleteFromList() throws InterruptedException {
+ authorizationPage.navigateTo();
+ RolePolicyRepresentation expected = new RolePolicyRepresentation();
+
+ expected.setName("Test Delete Role Policy");
+ expected.setDescription("description");
+ expected.addRole("Realm Role A");
+ expected.addRole("Realm Role B");
+ expected.addRole("Realm Role C");
+
+ expected = createPolicy(expected);
+ authorizationPage.navigateTo();
+ authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName());
+ authorizationPage.navigateTo();
+ assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
+ }
+
private RolePolicyRepresentation createPolicy(RolePolicyRepresentation expected) {
RolePolicy policy = authorizationPage.authorizationTabs().policies().create(expected);
assertAlertSuccess();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RulePolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RulePolicyManagementTest.java
index 09fb47a..a1fbb60 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RulePolicyManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RulePolicyManagementTest.java
@@ -71,6 +71,18 @@ public class RulePolicyManagementTest extends AbstractAuthorizationSettingsTest
assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
}
+ @Test
+ public void testDeleteFromList() throws InterruptedException {
+ authorizationPage.navigateTo();
+ RulePolicyRepresentation expected =createDefaultRepresentation("Delete Rule Policy");
+
+ expected = createPolicy(expected);
+ authorizationPage.navigateTo();
+ authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName());
+ authorizationPage.navigateTo();
+ assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
+ }
+
private RulePolicyRepresentation createDefaultRepresentation(String name) {
RulePolicyRepresentation expected = new RulePolicyRepresentation();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java
index 84a5c42..9bd5738 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java
@@ -49,6 +49,15 @@ public class ScopeManagementTest extends AbstractAuthorizationSettingsTest {
assertNull(authorizationPage.authorizationTabs().scopes().scopes().findByName(expected.getName()));
}
+ @Test
+ public void testDeleteFromList() {
+ ScopeRepresentation expected = createScope();
+ authorizationPage.navigateTo();
+ authorizationPage.authorizationTabs().scopes().deleteFromList(expected.getName());
+ authorizationPage.navigateTo();
+ assertNull(authorizationPage.authorizationTabs().scopes().scopes().findByName(expected.getName()));
+ }
+
private ScopeRepresentation createScope() {
ScopeRepresentation expected = new ScopeRepresentation();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopePermissionManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopePermissionManagementTest.java
index 3dfd0c8..e755335 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopePermissionManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopePermissionManagementTest.java
@@ -166,6 +166,23 @@ public class ScopePermissionManagementTest extends AbstractAuthorizationSettings
assertNull(authorizationPage.authorizationTabs().permissions().permissions().findByName(expected.getName()));
}
+ @Test
+ public void testDeleteFromList() throws InterruptedException {
+ authorizationPage.navigateTo();
+ ScopePermissionRepresentation expected = new ScopePermissionRepresentation();
+
+ expected.setName("Test Delete Scope Permission");
+ expected.setDescription("description");
+ expected.addScope("Scope C");
+ expected.addPolicy("Policy C");
+
+ expected = createPermission(expected);
+ authorizationPage.navigateTo();
+ authorizationPage.authorizationTabs().permissions().deleteFromList(expected.getName());
+ authorizationPage.navigateTo();
+ assertNull(authorizationPage.authorizationTabs().permissions().permissions().findByName(expected.getName()));
+ }
+
private ScopePermissionRepresentation createPermission(ScopePermissionRepresentation expected) {
ScopePermission policy = authorizationPage.authorizationTabs().permissions().create(expected);
assertAlertSuccess();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/TimePolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/TimePolicyManagementTest.java
index 6242c77..ed0165d 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/TimePolicyManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/TimePolicyManagementTest.java
@@ -109,6 +109,33 @@ public class TimePolicyManagementTest extends AbstractAuthorizationSettingsTest
assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
}
+ @Test
+ public void testDeleteFromList() throws InterruptedException {
+ authorizationPage.navigateTo();
+ TimePolicyRepresentation expected = new TimePolicyRepresentation();
+
+ expected.setName("Test Time Policy");
+ expected.setDescription("description");
+ expected.setNotBefore("2017-01-01 00:00:00");
+ expected.setNotBefore("2018-01-01 00:00:00");
+ expected.setDayMonth("1");
+ expected.setDayMonthEnd("2");
+ expected.setMonth("3");
+ expected.setMonthEnd("4");
+ expected.setYear("5");
+ expected.setYearEnd("6");
+ expected.setHour("7");
+ expected.setHourEnd("8");
+ expected.setMinute("9");
+ expected.setMinuteEnd("10");
+
+ expected = createPolicy(expected);
+ authorizationPage.navigateTo();
+ authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName());
+ authorizationPage.navigateTo();
+ assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
+ }
+
private TimePolicyRepresentation createPolicy(TimePolicyRepresentation expected) {
TimePolicy policy = authorizationPage.authorizationTabs().policies().create(expected);
assertAlertSuccess();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/UserPolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/UserPolicyManagementTest.java
index ed19bc5..7e8c483 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/UserPolicyManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/UserPolicyManagementTest.java
@@ -76,7 +76,7 @@ public class UserPolicyManagementTest extends AbstractAuthorizationSettingsTest
}
@Test
- public void testDeletePolicy() throws InterruptedException {
+ public void testDelete() throws InterruptedException {
authorizationPage.navigateTo();
UserPolicyRepresentation expected = new UserPolicyRepresentation();
@@ -92,6 +92,22 @@ public class UserPolicyManagementTest extends AbstractAuthorizationSettingsTest
assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
}
+ @Test
+ public void testDeleteFromList() throws InterruptedException {
+ authorizationPage.navigateTo();
+ UserPolicyRepresentation expected = new UserPolicyRepresentation();
+
+ expected.setName("Test User Policy");
+ expected.setDescription("description");
+ expected.addUser("user c");
+
+ expected = createPolicy(expected);
+ authorizationPage.navigateTo();
+ authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName());
+ authorizationPage.navigateTo();
+ assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
+ }
+
private UserPolicyRepresentation createPolicy(UserPolicyRepresentation expected) {
UserPolicy policy = authorizationPage.authorizationTabs().policies().create(expected);
assertAlertSuccess();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java
index 0d8e6b2..eaa45de 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java
@@ -23,6 +23,7 @@ package org.keycloak.testsuite.console.clients;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
@@ -92,7 +93,6 @@ public class ClientMappersOIDCTest extends AbstractClientTest {
assertEquals("oidc-hardcoded-role-mapper", found.getProtocolMapper());
Map<String, String> config = found.getConfig();
- assertEquals(1, config.size());
assertEquals("offline_access", config.get("role"));
//edit
@@ -164,8 +164,6 @@ public class ClientMappersOIDCTest extends AbstractClientTest {
assertEquals("oidc-usersessionmodel-note-mapper", found.getProtocolMapper());
Map<String, String> config = found.getConfig();
- assertNull(config.get("id.token.claim"));
- assertNull(config.get("access.token.claim"));
assertEquals("claim name", config.get("claim.name"));
assertEquals("session note", config.get("user.session.note"));
assertEquals("int", config.get("jsonType.label"));
diff --git a/testsuite/integration-arquillian/tests/other/jpa-performance/pom.xml b/testsuite/integration-arquillian/tests/other/jpa-performance/pom.xml
index 83fda9b..75e4bc7 100644
--- a/testsuite/integration-arquillian/tests/other/jpa-performance/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/jpa-performance/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-other</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-jpa-performance</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/mod_auth_mellon/pom.xml b/testsuite/integration-arquillian/tests/other/mod_auth_mellon/pom.xml
index 933b653..16e5394 100644
--- a/testsuite/integration-arquillian/tests/other/mod_auth_mellon/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/mod_auth_mellon/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-other</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-other-mod_auth_mellon</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/nodejs_adapter/pom.xml b/testsuite/integration-arquillian/tests/other/nodejs_adapter/pom.xml
index 9270e51..828006c 100644
--- a/testsuite/integration-arquillian/tests/other/nodejs_adapter/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/nodejs_adapter/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-other</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-nodejs-adapter</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/pom.xml b/testsuite/integration-arquillian/tests/other/pom.xml
index dce7878..f406421 100644
--- a/testsuite/integration-arquillian/tests/other/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-other</artifactId>
@@ -39,6 +39,7 @@
<modules>
<module>adapters</module>
<module>sssd</module>
+ <module>springboot-tests</module>
</modules>
<build>
diff --git a/testsuite/integration-arquillian/tests/other/server-config-migration/pom.xml b/testsuite/integration-arquillian/tests/other/server-config-migration/pom.xml
index 4e57844..428d254 100644
--- a/testsuite/integration-arquillian/tests/other/server-config-migration/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/server-config-migration/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-other</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml b/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml
new file mode 100644
index 0000000..90a997f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>integration-arquillian-tests-other</artifactId>
+ <groupId>org.keycloak.testsuite</groupId>
+ <version>3.3.0.CR1-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>integration-arquillian-tests-springboot</artifactId>
+
+ <properties>
+ <exclude.springboot>**/springboot/**/*Test.java</exclude.springboot>
+
+ <adapter.container>tomcat</adapter.container>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-test-helper</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+
+ <plugin>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <excludes>
+ <exclude>${exclude.springboot}</exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>test-springboot</id>
+ <properties>
+ <exclude.springboot>-</exclude.springboot>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.bazaarvoice.maven.plugins</groupId>
+ <artifactId>process-exec-maven-plugin</artifactId>
+ <version>0.7</version>
+ <executions>
+ <execution>
+ <id>spring-boot-application-process</id>
+ <phase>generate-test-resources</phase>
+ <goals>
+ <goal>start</goal>
+ </goals>
+ <configuration>
+ <name>springboot</name>
+ <workingDir>../../../../test-apps/spring-boot-adapter</workingDir>
+ <arguments>
+ <argument>mvn</argument>
+ <argument>spring-boot:run</argument>
+ <argument>-Dkeycloak.version=${project.version}</argument>
+ <argument>-Pspring-boot-adapter-${adapter.container}</argument>
+ </arguments>
+ </configuration>
+ </execution>
+
+ <execution>
+ <id>kill-processes</id>
+ <phase>post-integration-test</phase>
+ <goals>
+ <goal>stop-all</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+
+
+</project>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringAdminPage.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringAdminPage.java
new file mode 100644
index 0000000..8ce5e75
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringAdminPage.java
@@ -0,0 +1,22 @@
+package org.keycloak.testsuite.springboot;
+
+import org.keycloak.testsuite.pages.AbstractPage;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+public class SpringAdminPage extends AbstractPage {
+
+ @FindBy(className = "test")
+ private WebElement testDiv;
+
+
+ @Override
+ public boolean isCurrent() {
+ return driver.getTitle().equalsIgnoreCase("springboot admin page");
+ }
+
+ @Override
+ public void open() throws Exception {
+
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringApplicationPage.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringApplicationPage.java
new file mode 100644
index 0000000..9442cd3
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringApplicationPage.java
@@ -0,0 +1,40 @@
+package org.keycloak.testsuite.springboot;
+
+import org.keycloak.testsuite.pages.AbstractPage;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+public class SpringApplicationPage extends AbstractPage {
+
+ @FindBy(className = "test")
+ private WebElement testDiv;
+
+ @FindBy(className = "adminlink")
+ private WebElement adminLink;
+
+ private String title;
+
+ public SpringApplicationPage() {
+ super();
+
+ title = "springboot test page";
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ @Override
+ public boolean isCurrent() {
+ return driver.getTitle().equalsIgnoreCase(title);
+ }
+
+ @Override
+ public void open() throws Exception {
+
+ }
+
+ public void goAdmin() {
+ adminLink.click();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/TokenPage.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/TokenPage.java
new file mode 100644
index 0000000..7fc7961
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/TokenPage.java
@@ -0,0 +1,19 @@
+package org.keycloak.testsuite.springboot;
+
+import java.net.URL;
+
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.keycloak.testsuite.adapter.page.AbstractShowTokensPage;
+
+public class TokenPage extends AbstractShowTokensPage {
+
+ @Override
+ public boolean isCurrent() {
+ return driver.getTitle().equalsIgnoreCase("tokens from spring boot");
+ }
+
+ @Override
+ public URL getInjectedUrl() {
+ return null;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java
new file mode 100644
index 0000000..5b15077
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java
@@ -0,0 +1,217 @@
+package org.keycloak.testsuite.springboot;
+
+import static org.keycloak.testsuite.admin.ApiUtil.assignRealmRoles;
+import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
+import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.ws.rs.core.UriBuilder;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.logging.Logger;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.RoleResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.arquillian.SuiteContext;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.util.WaitUtils;
+import org.keycloak.util.TokenUtil;
+import org.openqa.selenium.By;
+
+public abstract class AbstractSpringBootTest extends AbstractKeycloakTest {
+
+ protected static final String REALM_ID = "cd8ee421-5100-41ba-95dd-b27c8e5cf042";
+
+ protected static final String REALM_NAME = "test";
+
+ protected static final String CLIENT_ID = "spring-boot-app";
+ protected static final String SECRET = "e3789ac5-bde6-4957-a7b0-612823dac101";
+
+ protected static final String APPLICATION_URL = "http://localhost:8280";
+ protected static final String BASE_URL = APPLICATION_URL + "/admin";
+
+ protected static final String USER_LOGIN = "testuser";
+ protected static final String USER_EMAIL = "user@email.test";
+ protected static final String USER_PASSWORD = "user-password";
+
+ protected static final String USER_LOGIN_2 = "testuser2";
+ protected static final String USER_EMAIL_2 = "user2@email.test";
+ protected static final String USER_PASSWORD_2 = "user2-password";
+
+ protected static final String CORRECT_ROLE = "admin";
+ protected static final String INCORRECT_ROLE = "wrong-admin";
+
+ protected static final String REALM_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5" +
+ "mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi7" +
+ "9NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB";
+
+ protected static final String REALM_PRIVATE_KEY = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3Bj" +
+ "LGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vj" +
+ "O2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jY" +
+ "lQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn" +
+ "9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEK" +
+ "Xalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2w" +
+ "Vl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJ" +
+ "AY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZ" +
+ "N39fOYAlo+nTixgeW7X8Y=";
+
+ @Page
+ protected LoginPage loginPage;
+
+ @Page
+ protected SpringApplicationPage applicationPage;
+
+ @Page
+ protected SpringAdminPage adminPage;
+
+ @Page
+ protected TokenPage tokenPage;
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ RealmRepresentation realm = new RealmRepresentation();
+
+ realm.setId(REALM_ID);
+ realm.setRealm(REALM_NAME);
+ realm.setEnabled(true);
+
+ realm.setPublicKey(REALM_PUBLIC_KEY);
+ realm.setPrivateKey(REALM_PRIVATE_KEY);
+
+ realm.setClients(Collections.singletonList(createClient()));
+
+ List<String> eventListeners = new ArrayList<>();
+ eventListeners.add("jboss-logging");
+ eventListeners.add("event-queue");
+ realm.setEventsListeners(eventListeners);
+
+ testRealms.add(realm);
+ }
+
+ private ClientRepresentation createClient() {
+ ClientRepresentation clientRepresentation = new ClientRepresentation();
+
+ clientRepresentation.setId(CLIENT_ID);
+ clientRepresentation.setSecret(SECRET);
+
+ clientRepresentation.setBaseUrl(BASE_URL);
+ clientRepresentation.setRedirectUris(Collections.singletonList(BASE_URL + "/*"));
+ clientRepresentation.setAdminUrl(BASE_URL);
+
+ return clientRepresentation;
+ }
+
+ private void addUser(String login, String email, String password, String... roles) {
+ UserRepresentation userRepresentation = new UserRepresentation();
+
+ userRepresentation.setUsername(login);
+ userRepresentation.setEmail(email);
+ userRepresentation.setEmailVerified(true);
+ userRepresentation.setEnabled(true);
+
+ RealmResource realmResource = adminClient.realm(REALM_NAME);
+ String userId = createUserWithAdminClient(realmResource, userRepresentation);
+
+ resetUserPassword(realmResource.users().get(userId), password, false);
+
+ for (String role : roles)
+ assignRealmRoles(realmResource, userId, role);
+ }
+
+ private String getAuthRoot(SuiteContext suiteContext) {
+ return suiteContext.getAuthServerInfo().getContextRoot().toString();
+ }
+
+ private String encodeUrl(String url) {
+ String result;
+ try {
+ result = URLEncoder.encode(url, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ result = url;
+ }
+
+ return result;
+ }
+
+ protected String logoutPage(String redirectUrl) {
+ return getAuthRoot(suiteContext)
+ + "/auth/realms/" + REALM_NAME
+ + "/protocol/" + "openid-connect"
+ + "/logout?redirect_uri=" + encodeUrl(redirectUrl);
+ }
+
+ protected void setAdapterAndServerTimeOffset(int timeOffset, String url) {
+ setTimeOffset(timeOffset);
+
+ String timeOffsetUri = UriBuilder.fromUri(url)
+ .queryParam("timeOffset", timeOffset)
+ .build().toString();
+
+ driver.navigate().to(timeOffsetUri);
+ WaitUtils.waitUntilElement(By.tagName("body")).is().visible();
+ }
+
+ protected String getCorrectUserId() {
+ return adminClient.realms().realm(REALM_NAME).users().search(USER_LOGIN)
+ .get(0).getId();
+ }
+
+ @Before
+ public void createRoles() {
+ RealmResource realm = realmsResouce().realm(REALM_NAME);
+
+ RoleRepresentation correct = new RoleRepresentation(CORRECT_ROLE, CORRECT_ROLE, false);
+ realm.roles().create(correct);
+
+ RoleRepresentation incorrect = new RoleRepresentation(INCORRECT_ROLE, INCORRECT_ROLE, false);
+ realm.roles().create(incorrect);
+ }
+
+ @Before
+ public void addUsers() {
+ addUser(USER_LOGIN, USER_EMAIL, USER_PASSWORD, CORRECT_ROLE);
+ addUser(USER_LOGIN_2, USER_EMAIL_2, USER_PASSWORD_2, INCORRECT_ROLE);
+ }
+
+ @After
+ public void cleanupUsers() {
+ RealmResource providerRealm = adminClient.realm(REALM_NAME);
+ UserRepresentation userRep = ApiUtil.findUserByUsername(providerRealm, USER_LOGIN);
+ if (userRep != null) {
+ providerRealm.users().get(userRep.getId()).remove();
+ }
+
+ RealmResource childRealm = adminClient.realm(REALM_NAME);
+ userRep = ApiUtil.findUserByUsername(childRealm, USER_LOGIN_2);
+ if (userRep != null) {
+ childRealm.users().get(userRep.getId()).remove();
+ }
+ }
+
+ @After
+ public void cleanupRoles() {
+ RealmResource realm = realmsResouce().realm(REALM_NAME);
+
+ RoleResource correctRole = realm.roles().get(CORRECT_ROLE);
+ correctRole.remove();
+
+ RoleResource incorrectRole = realm.roles().get(INCORRECT_ROLE);
+ incorrectRole.remove();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java
new file mode 100644
index 0000000..6aea719
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java
@@ -0,0 +1,61 @@
+package org.keycloak.testsuite.springboot;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class BasicSpringBootTest extends AbstractSpringBootTest {
+ @Test
+ public void testCorrectUser() {
+ driver.navigate().to(APPLICATION_URL + "/index.html");
+
+ Assert.assertTrue("Must be on application page", applicationPage.isCurrent());
+
+ applicationPage.goAdmin();
+
+ Assert.assertTrue("Must be on login page", loginPage.isCurrent());
+
+ loginPage.login(USER_LOGIN, USER_PASSWORD);
+
+ Assert.assertTrue("Must be on admin page", adminPage.isCurrent());
+ Assert.assertTrue("Admin page must contain correct div",
+ driver.getPageSource().contains("You are now admin"));
+
+ driver.navigate().to(logoutPage(BASE_URL));
+
+ Assert.assertTrue("Must be on login page", loginPage.isCurrent());
+
+ }
+
+ @Test
+ public void testIncorrectUser() {
+ driver.navigate().to(APPLICATION_URL + "/index.html");
+
+ Assert.assertTrue("Must be on application page", applicationPage.isCurrent());
+
+ applicationPage.goAdmin();
+
+ Assert.assertTrue("Must be on login page", loginPage.isCurrent());
+
+ loginPage.login(USER_LOGIN_2, USER_PASSWORD_2);
+
+ Assert.assertTrue("Must return 403 because of incorrect role",
+ driver.getPageSource().contains("There was an unexpected error (type=Forbidden, status=403)")
+ || driver.getPageSource().contains("\"status\":403,\"error\":\"Forbidden\""));
+ }
+
+ @Test
+ public void testIncorrectCredentials() {
+ driver.navigate().to(APPLICATION_URL + "/index.html");
+
+ Assert.assertTrue("Must be on application page", applicationPage.isCurrent());
+
+ applicationPage.goAdmin();
+
+ Assert.assertTrue("Must be on login page", loginPage.isCurrent());
+
+ loginPage.login(USER_LOGIN, USER_PASSWORD_2);
+
+ Assert.assertEquals("Error message about password",
+ "Invalid username or password.", loginPage.getError());
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java
new file mode 100644
index 0000000..5ac950f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java
@@ -0,0 +1,154 @@
+package org.keycloak.testsuite.springboot;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventType;
+import org.keycloak.services.Urls;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.pages.AccountApplicationsPage;
+import org.keycloak.testsuite.pages.OAuthGrantPage;
+import org.keycloak.testsuite.util.ClientManager;
+import org.keycloak.testsuite.util.WaitUtils;
+import org.keycloak.util.TokenUtil;
+import org.openqa.selenium.By;
+
+import javax.ws.rs.core.UriBuilder;
+import java.util.List;
+
+import static org.keycloak.testsuite.util.WaitUtils.pause;
+
+public class OfflineTokenSpringBootTest extends AbstractSpringBootTest {
+ private static final String SERVLET_URI = APPLICATION_URL + "/admin/TokenServlet";
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ @Page
+ private AccountApplicationsPage accountAppPage;
+
+ @Page
+ private OAuthGrantPage oauthGrantPage;
+
+ @Test
+ public void testTokens() {
+ String servletUri = UriBuilder.fromUri(SERVLET_URI)
+ .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS)
+ .build().toString();
+ driver.navigate().to(servletUri);
+
+ Assert.assertTrue("Must be on login page", loginPage.isCurrent());
+ loginPage.login(USER_LOGIN, USER_PASSWORD);
+
+ WaitUtils.waitUntilElement(By.tagName("body")).is().visible();
+
+ Assert.assertTrue(tokenPage.isCurrent());
+
+ Assert.assertEquals(tokenPage.getRefreshToken().getType(), TokenUtil.TOKEN_TYPE_OFFLINE);
+ Assert.assertEquals(tokenPage.getRefreshToken().getExpiration(), 0);
+
+ String accessTokenId = tokenPage.getAccessToken().getId();
+ String refreshTokenId = tokenPage.getRefreshToken().getId();
+
+ setAdapterAndServerTimeOffset(9999, SERVLET_URI);
+
+ driver.navigate().to(SERVLET_URI);
+ Assert.assertTrue("Must be on tokens page", tokenPage.isCurrent());
+ Assert.assertNotEquals(tokenPage.getRefreshToken().getId(), refreshTokenId);
+ Assert.assertNotEquals(tokenPage.getAccessToken().getId(), accessTokenId);
+
+ setAdapterAndServerTimeOffset(0, SERVLET_URI);
+
+ driver.navigate().to(logoutPage(SERVLET_URI));
+ Assert.assertTrue("Must be on login page", loginPage.isCurrent());
+ }
+
+ @Test
+ public void testRevoke() {
+ // Login to servlet first with offline token
+ String servletUri = UriBuilder.fromUri(SERVLET_URI)
+ .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS)
+ .build().toString();
+ driver.navigate().to(servletUri);
+ WaitUtils.waitUntilElement(By.tagName("body")).is().visible();
+
+ loginPage.login(USER_LOGIN, USER_PASSWORD);
+ Assert.assertTrue("Must be on token page", tokenPage.isCurrent());
+
+ Assert.assertEquals(tokenPage.getRefreshToken().getType(), TokenUtil.TOKEN_TYPE_OFFLINE);
+
+ // Assert refresh works with increased time
+ setAdapterAndServerTimeOffset(9999, SERVLET_URI);
+ driver.navigate().to(SERVLET_URI);
+ Assert.assertTrue("Must be on token page", tokenPage.isCurrent());
+ setAdapterAndServerTimeOffset(0, SERVLET_URI);
+
+ events.clear();
+
+ // Go to account service and revoke grant
+ accountAppPage.open();
+
+ List<String> additionalGrants = accountAppPage.getApplications().get(CLIENT_ID).getAdditionalGrants();
+ Assert.assertEquals(additionalGrants.size(), 1);
+ Assert.assertEquals(additionalGrants.get(0), "Offline Token");
+ accountAppPage.revokeGrant(CLIENT_ID);
+ pause(500);
+ Assert.assertEquals(accountAppPage.getApplications().get(CLIENT_ID).getAdditionalGrants().size(), 0);
+
+ events.expect(EventType.REVOKE_GRANT).realm(REALM_ID).user(getCorrectUserId())
+ .client("account").detail(Details.REVOKED_CLIENT, CLIENT_ID).assertEvent();
+
+ // Assert refresh doesn't work now (increase time one more time)
+ setAdapterAndServerTimeOffset(9999, SERVLET_URI);
+ driver.navigate().to(SERVLET_URI);
+ loginPage.assertCurrent();
+ setAdapterAndServerTimeOffset(0, SERVLET_URI);
+ }
+
+ @Test
+ public void testConsent() {
+ ClientManager.realm(adminClient.realm(REALM_NAME)).clientId(CLIENT_ID).consentRequired(true);
+
+ // Assert grant page doesn't have 'Offline Access' role when offline token is not requested
+ driver.navigate().to(SERVLET_URI);
+ loginPage.login(USER_LOGIN, USER_PASSWORD);
+ oauthGrantPage.assertCurrent();
+ WaitUtils.waitUntilElement(By.xpath("//body")).text().not().contains("Offline access");
+ oauthGrantPage.cancel();
+
+ // Assert grant page has 'Offline Access' role now
+ String servletUri = UriBuilder.fromUri(SERVLET_URI)
+ .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS)
+ .build().toString();
+ driver.navigate().to(servletUri);
+ WaitUtils.waitUntilElement(By.tagName("body")).is().visible();
+
+ loginPage.login(USER_LOGIN, USER_PASSWORD);
+ oauthGrantPage.assertCurrent();
+ WaitUtils.waitUntilElement(By.xpath("//body")).text().contains("Offline access");
+
+ oauthGrantPage.accept();
+
+ Assert.assertTrue("Must be on token page", tokenPage.isCurrent());
+ Assert.assertEquals(tokenPage.getRefreshToken().getType(), TokenUtil.TOKEN_TYPE_OFFLINE);
+
+ String accountAppPageUrl =
+ Urls.accountApplicationsPage(getAuthServerRoot(), REALM_NAME).toString();
+ driver.navigate().to(accountAppPageUrl);
+ AccountApplicationsPage.AppEntry offlineClient = accountAppPage.getApplications().get(CLIENT_ID);
+ Assert.assertTrue(offlineClient.getRolesGranted().contains("Offline access"));
+ Assert.assertTrue(offlineClient.getAdditionalGrants().contains("Offline Token"));
+
+ //This was necessary to be introduced, otherwise other testcases will fail
+ driver.navigate().to(logoutPage(SERVLET_URI));
+ loginPage.assertCurrent();
+
+ events.clear();
+
+ // Revert change
+ ClientManager.realm(adminClient.realm(REALM_NAME)).clientId(CLIENT_ID).consentRequired(false);
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/sssd/pom.xml b/testsuite/integration-arquillian/tests/other/sssd/pom.xml
index 93c7339..e3f0b78 100644
--- a/testsuite/integration-arquillian/tests/other/sssd/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/sssd/pom.xml
@@ -5,7 +5,7 @@
<parent>
<artifactId>integration-arquillian-tests-other</artifactId>
<groupId>org.keycloak.testsuite</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
testsuite/integration-arquillian/tests/pom.xml 21(+11 -10)
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 24e67c9..324f726 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<packaging>pom</packaging>
@@ -43,6 +43,7 @@
<auth.server.undertow>true</auth.server.undertow>
<auth.server.undertow.crossdc>false</auth.server.undertow.crossdc>
<auth.server.crossdc>false</auth.server.crossdc>
+ <cache.server.lifecycle.skip>false</cache.server.lifecycle.skip>
<auth.server.container>auth-server-${auth.server}</auth.server.container>
<auth.server.home>${containers.home}/${auth.server.container}</auth.server.home>
@@ -85,6 +86,7 @@
<keycloak.connectionsInfinispan.remoteStorePort>12232</keycloak.connectionsInfinispan.remoteStorePort>
<keycloak.connectionsInfinispan.remoteStorePort.2>13232</keycloak.connectionsInfinispan.remoteStorePort.2>
<keycloak.connectionsJpa.url.crossdc>jdbc:h2:mem:test-dc-shared</keycloak.connectionsJpa.url.crossdc>
+ <keycloak.testsuite.logging.pattern>%d{HH:mm:ss,SSS} %-5p [%c] %m%n</keycloak.testsuite.logging.pattern>
<adapter.test.props/>
<migration.import.properties/>
@@ -271,6 +273,7 @@
<!--cache server properties-->
<auth.server.crossdc>${auth.server.crossdc}</auth.server.crossdc>
<auth.server.undertow.crossdc>${auth.server.undertow.crossdc}</auth.server.undertow.crossdc>
+ <cache.server.lifecycle.skip>${cache.server.lifecycle.skip}</cache.server.lifecycle.skip>
<cache.server>${cache.server}</cache.server>
<cache.server.port.offset>${cache.server.port.offset}</cache.server.port.offset>
@@ -284,13 +287,14 @@
<keycloak.connectionsInfinispan.remoteStorePort>${keycloak.connectionsInfinispan.remoteStorePort}</keycloak.connectionsInfinispan.remoteStorePort>
<keycloak.connectionsInfinispan.remoteStorePort.2>${keycloak.connectionsInfinispan.remoteStorePort.2}</keycloak.connectionsInfinispan.remoteStorePort.2>
<keycloak.connectionsInfinispan.remoteStoreServer>${keycloak.connectionsInfinispan.remoteStoreServer}</keycloak.connectionsInfinispan.remoteStoreServer>
+ <keycloak.testsuite.logging.pattern>${keycloak.testsuite.logging.pattern}</keycloak.testsuite.logging.pattern>
<keycloak.connectionsJpa.url.crossdc>${keycloak.connectionsJpa.url.crossdc}</keycloak.connectionsJpa.url.crossdc>
</systemPropertyVariables>
<properties>
<property>
<name>listener</name>
- <value>org.keycloak.testsuite.util.TestEventsLogger,org.keycloak.testsuite.util.junit.AggregateResultsReporter,org.keycloak.testsuite.util.NonIDERunListener</value>
+ <value>org.keycloak.testsuite.util.TestEventsLogger,org.keycloak.testsuite.util.NonIDERunListener</value>
</property>
</properties>
</configuration>
@@ -386,6 +390,7 @@
<auth.server.crossdc>true</auth.server.crossdc>
<cache.server.jboss>true</cache.server.jboss>
<cache.server.config.dir>${cache.server.home}/standalone/configuration</cache.server.config.dir>
+ <keycloak.testsuite.logging.pattern>%d{HH:mm:ss,SSS} [%t] %-5p [%c{1.}] %m%n</keycloak.testsuite.logging.pattern>
</properties>
<dependencies>
<dependency>
@@ -460,6 +465,7 @@
<auth.server.crossdc>true</auth.server.crossdc>
<cache.server.jboss>true</cache.server.jboss>
<cache.server.config.dir>${cache.server.home}/standalone/configuration</cache.server.config.dir>
+ <keycloak.testsuite.logging.pattern>%d{HH:mm:ss,SSS} [%t] %-5p [%c{1.}] %m%n</keycloak.testsuite.logging.pattern>
</properties>
<dependencies>
<dependency>
@@ -540,7 +546,7 @@
<!--
profile that enables/disables specified feature, for more details see
- https://keycloak.gitbooks.io/server-installation-and-configuration/content/topics/profiles.html
+ https://keycloak.gitbooks.io/documentation/content/server_installation/topics/profiles.html
-->
<profile>
<id>auth-server-enable-disable-feature</id>
@@ -584,6 +590,8 @@
<auth.server.backend2.home>${containers.home}/auth-server-${auth.server}-backend2</auth.server.backend2.home>
<auth.server.config.dir>${auth.server.backend1.home}/standalone/configuration</auth.server.config.dir>
+
+ <keycloak.testsuite.logging.pattern>%d{HH:mm:ss,SSS} [%t] %-5p [%c{1.}] %m%n</keycloak.testsuite.logging.pattern>
</properties>
<build>
<plugins>
@@ -944,13 +952,6 @@
</file>
</activation>
<dependencies>
-
- <dependency>
- <groupId>org.keycloak.testsuite</groupId>
- <artifactId>integration-arquillian-test-utils</artifactId>
- <version>${project.version}</version>
- </dependency>
-
<!-- TEST DEPENDENCIES -->
<dependency>
<groupId>junit</groupId>
testsuite/jetty/jetty81/pom.xml 2(+1 -1)
diff --git a/testsuite/jetty/jetty81/pom.xml b/testsuite/jetty/jetty81/pom.xml
index 21dd809..41d7549 100755
--- a/testsuite/jetty/jetty81/pom.xml
+++ b/testsuite/jetty/jetty81/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-testsuite-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
testsuite/jetty/jetty91/pom.xml 2(+1 -1)
diff --git a/testsuite/jetty/jetty91/pom.xml b/testsuite/jetty/jetty91/pom.xml
index 2c96759..6d4fe3e 100755
--- a/testsuite/jetty/jetty91/pom.xml
+++ b/testsuite/jetty/jetty91/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-testsuite-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
testsuite/jetty/jetty92/pom.xml 2(+1 -1)
diff --git a/testsuite/jetty/jetty92/pom.xml b/testsuite/jetty/jetty92/pom.xml
index c6eb51c..4886f7f 100755
--- a/testsuite/jetty/jetty92/pom.xml
+++ b/testsuite/jetty/jetty92/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-testsuite-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
testsuite/jetty/jetty93/pom.xml 2(+1 -1)
diff --git a/testsuite/jetty/jetty93/pom.xml b/testsuite/jetty/jetty93/pom.xml
index 5412b51..ca619ee 100644
--- a/testsuite/jetty/jetty93/pom.xml
+++ b/testsuite/jetty/jetty93/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-testsuite-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
testsuite/jetty/jetty94/pom.xml 2(+1 -1)
diff --git a/testsuite/jetty/jetty94/pom.xml b/testsuite/jetty/jetty94/pom.xml
index 65dec4a..87fbee5 100644
--- a/testsuite/jetty/jetty94/pom.xml
+++ b/testsuite/jetty/jetty94/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-testsuite-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
testsuite/jetty/pom.xml 2(+1 -1)
diff --git a/testsuite/jetty/pom.xml b/testsuite/jetty/pom.xml
index 75860cb..ed23e79 100755
--- a/testsuite/jetty/pom.xml
+++ b/testsuite/jetty/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-testsuite-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Keycloak SAML Jetty Testsuite Integration</name>
testsuite/pom.xml 3(+2 -1)
diff --git a/testsuite/pom.xml b/testsuite/pom.xml
index a123732..0adb305 100755
--- a/testsuite/pom.xml
+++ b/testsuite/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -53,6 +53,7 @@
<module>integration</module>
<module>tomcat8</module>
<module>integration-arquillian</module>
+ <module>utils</module>
</modules>
<profiles>
testsuite/proxy/pom.xml 2(+1 -1)
diff --git a/testsuite/proxy/pom.xml b/testsuite/proxy/pom.xml
index c120b11..0d0314f 100755
--- a/testsuite/proxy/pom.xml
+++ b/testsuite/proxy/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-testsuite-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
testsuite/tomcat6/pom.xml 2(+1 -1)
diff --git a/testsuite/tomcat6/pom.xml b/testsuite/tomcat6/pom.xml
index f2d3c77..7500803 100755
--- a/testsuite/tomcat6/pom.xml
+++ b/testsuite/tomcat6/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-testsuite-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
testsuite/tomcat7/pom.xml 2(+1 -1)
diff --git a/testsuite/tomcat7/pom.xml b/testsuite/tomcat7/pom.xml
index b984b03..2109bac 100755
--- a/testsuite/tomcat7/pom.xml
+++ b/testsuite/tomcat7/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-testsuite-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
testsuite/tomcat8/pom.xml 2(+1 -1)
diff --git a/testsuite/tomcat8/pom.xml b/testsuite/tomcat8/pom.xml
index d467195..700c461 100755
--- a/testsuite/tomcat8/pom.xml
+++ b/testsuite/tomcat8/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-testsuite-pom</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
testsuite/utils/pom.xml 36(+36 -0)
diff --git a/testsuite/utils/pom.xml b/testsuite/utils/pom.xml
new file mode 100755
index 0000000..0bbf053
--- /dev/null
+++ b/testsuite/utils/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0"?>
+<!--
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-testsuite-pom</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>3.3.0.CR1-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-testsuite-utils</artifactId>
+ <name>Keycloak TestSuite Utils</name>
+ <description />
+
+ <properties>
+ <maven.compiler.target>1.8</maven.compiler.target>
+ <maven.compiler.source>1.8</maven.compiler.source>
+ </properties>
+</project>
diff --git a/testsuite/utils/src/main/java/org/keycloak/testsuite/LogTrimmer.java b/testsuite/utils/src/main/java/org/keycloak/testsuite/LogTrimmer.java
new file mode 100644
index 0000000..d53bf5b
--- /dev/null
+++ b/testsuite/utils/src/main/java/org/keycloak/testsuite/LogTrimmer.java
@@ -0,0 +1,44 @@
+package org.keycloak.testsuite;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * Created by st on 03/07/17.
+ */
+public class LogTrimmer {
+
+ public static void main(String[] args) throws IOException {
+ BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
+ String testRunning = null;
+ StringBuilder sb = new StringBuilder();
+ for(String l = br.readLine(); l != null; l = br.readLine()) {
+ if (testRunning == null) {
+ if (l.startsWith("Running")) {
+ testRunning = l.split(" ")[1];
+ System.out.println(l);
+ } else {
+ System.out.println("-- " + l);
+ }
+ } else {
+ if (l.contains("Tests run:")) {
+ if (!(l.contains("Failures: 0") && l.contains("Errors: 0"))) {
+ System.out.println("--------- " + testRunning + " output start ---------");
+ System.out.println(sb.toString());
+ System.out.println("--------- " + testRunning + " output end ---------");
+ }
+ System.out.println(l);
+
+
+ testRunning = null;
+ sb = new StringBuilder();
+ } else {
+ sb.append(testRunning.substring(testRunning.lastIndexOf('.') + 1) + " ++ " + l);
+ sb.append("\n");
+ }
+ }
+ }
+ }
+
+}
themes/pom.xml 45(+44 -1)
diff --git a/themes/pom.xml b/themes/pom.xml
index e2a5de9..4274262 100755
--- a/themes/pom.xml
+++ b/themes/pom.xml
@@ -4,7 +4,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -50,5 +50,48 @@
</build>
</profile>
</profiles>
+
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-clean-plugin</artifactId>
+ <configuration>
+ <filesets>
+ <fileset>
+ <directory>src/main/resources/theme/keycloak/common/resources/node_modules</directory>
+ </fileset>
+ </filesets>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>com.github.eirslett</groupId>
+ <artifactId>frontend-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>install node and yarn</id>
+ <goals>
+ <goal>install-node-and-yarn</goal>
+ </goals>
+ <phase>generate-resources</phase>
+ </execution>
+ <execution>
+ <id>yarn install</id>
+ <goals>
+ <goal>yarn</goal>
+ </goals>
+ <configuration>
+ <arguments>install --production=false --frozen-lockfile</arguments>
+ </configuration>
+ </execution>
+ </executions>
+ <configuration>
+ <nodeVersion>v6.11.1</nodeVersion>
+ <yarnVersion>v0.27.5</yarnVersion>
+ <workingDirectory>src/main/resources/theme/keycloak/common/resources</workingDirectory>
+ <installDirectory>target</installDirectory>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
</project>
diff --git a/themes/src/main/resources/theme/base/account/applications.ftl b/themes/src/main/resources/theme/base/account/applications.ftl
index bca5102..45a253a 100755
--- a/themes/src/main/resources/theme/base/account/applications.ftl
+++ b/themes/src/main/resources/theme/base/account/applications.ftl
@@ -27,9 +27,9 @@
<#list applications.applications as application>
<tr>
<td>
- <#if application.client.baseUrl??><a href="${application.client.baseUrl}"></#if>
+ <#if application.effectiveUrl?has_content><a href="${application.effectiveUrl}"></#if>
<#if application.client.name??>${advancedMsg(application.client.name)}<#else>${application.client.clientId}</#if>
- <#if application.client.baseUrl??></a></#if>
+ <#if application.effectiveUrl?has_content></a></#if>
</td>
<td>
diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
index b3f4829..47dbda1 100755
--- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -163,4 +163,6 @@ locale_ja=\u65E5\u672C\u8A9E
locale_no=Norsk
locale_lt=Lietuvi\u0173
locale_pt-BR=Portugu\u00EAs (Brasil)
-locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439
\ No newline at end of file
+locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439
+locale_zh-CN=\u4e2d\u6587\u7b80\u4f53
+locale_sv=Svenska
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/account/messages/messages_zh_CN.properties b/themes/src/main/resources/theme/base/account/messages/messages_zh_CN.properties
new file mode 100644
index 0000000..4691002
--- /dev/null
+++ b/themes/src/main/resources/theme/base/account/messages/messages_zh_CN.properties
@@ -0,0 +1,164 @@
+doSave=保存
+doCancel=取消
+doLogOutAllSessions=登出所有会话
+doRemove=删除
+doAdd=添加
+doSignOut=登出
+
+editAccountHtmlTitle=编辑账户
+federatedIdentitiesHtmlTitle=链接的身份
+accountLogHtmlTitle=账户日志
+changePasswordHtmlTitle=更改密码
+sessionsHtmlTitle=会话
+accountManagementTitle=Keycloak账户管理
+authenticatorTitle=认证者
+applicationsHtmlTitle=应用
+
+authenticatorCode=一次性认证码
+email=电子邮件
+firstName=名
+givenName=姓
+fullName=全名
+lastName=姓
+familyName=姓
+password=密码
+passwordConfirm=确认
+passwordNew=新密码
+username=用户名
+address=地址
+street=街道
+locality=城市住所
+region=省,自治区,直辖市
+postal_code=邮政编码
+country=国家
+emailVerified=验证过的Email
+gssDelegationCredential=GSS Delegation Credential
+
+role_admin=管理员
+role_realm-admin=域管理员
+role_create-realm=创建域
+role_view-realm=查看域
+role_view-users=查看用户
+role_view-applications=查看应用
+role_view-clients=查看客户
+role_view-events=查看事件
+role_view-identity-providers=查看身份提供者
+role_manage-realm=管理域
+role_manage-users=管理用户
+role_manage-applications=管理应用
+role_manage-identity-providers=管理身份提供者
+role_manage-clients=管理客户
+role_manage-events=管理事件
+role_view-profile=查看用户信息
+role_manage-account=管理账户
+role_read-token=读取 token
+role_offline-access=离线访问
+role_uma_authorization=获取授权
+client_account=账户
+client_security-admin-console=安全管理终端
+client_admin-cli=管理命令行
+client_realm-management=域管理
+client_broker=代理
+
+
+requiredFields=必填项
+allFieldsRequired=所有项必填
+
+backToApplication=« 回到应用
+backTo=回到 {0}
+
+date=日期
+event=事件
+ip=IP
+client=客户端
+clients=客户端
+details=详情
+started=开始
+lastAccess=最后一次访问
+expires=过期时间
+applications=应用
+
+account=账户
+federatedIdentity=关联身份
+authenticator=认证方
+sessions=会话
+log=日志
+
+application=应用
+availablePermissions=可用权限
+grantedPermissions=授予权限
+grantedPersonalInfo=授权的个人信息
+additionalGrants=可授予的权限
+action=操作
+inResource=in
+fullAccess=所有权限
+offlineToken=离线 token
+revoke=收回授权
+
+configureAuthenticators=配置的认证者
+mobile=手机
+totpStep1=在你的设备上安装 <a href="https://fedorahosted.org/freeotp/" target="_blank">FreeOTP</a> 或者 Google Authenticator.两个应用可以从 <a href="https://play.google.com">Google Play</a> 和 Apple App Store下载。
+totpStep2=打开应用扫描二维码输入验证码
+totpStep3=输入应用提供的一次性验证码单击保存
+
+missingUsernameMessage=请指定用户名
+missingFirstNameMessage=请指定名
+invalidEmailMessage=无效的电子邮箱地址
+missingLastNameMessage=请指定姓
+missingEmailMessage=请指定邮件地址
+missingPasswordMessage=请输入密码
+notMatchPasswordMessage=密码不匹配
+
+missingTotpMessage=请指定认证者代码
+invalidPasswordExistingMessage=无效的旧密码
+invalidPasswordConfirmMessage=确认密码不相符
+invalidTotpMessage=无效的认证码
+
+usernameExistsMessage=用户名已经存在
+emailExistsMessage=电子邮箱已经存在
+
+readOnlyUserMessage=无法修改账户,因为它是只读的。
+readOnlyPasswordMessage=不可以更该账户因为它是只读的。
+
+successTotpMessage=手机认证者配置完毕
+successTotpRemovedMessage=手机认证者已删除
+
+successGrantRevokedMessage=授权成功回收
+
+accountUpdatedMessage=您的账户已经更新
+accountPasswordUpdatedMessage=您的密码已经修改
+
+missingIdentityProviderMessage=身份提供者未指定
+invalidFederatedIdentityActionMessage=无效或者缺少操作
+identityProviderNotFoundMessage=指定的身份提供者未找到
+federatedIdentityLinkNotActiveMessage=这个身份不再使用了。
+federatedIdentityRemovingLastProviderMessage=你不可以移除最后一个身份提供者因为你没有设置密码
+identityProviderRedirectErrorMessage=尝试重定向到身份提供商失败
+identityProviderRemovedMessage=身份提供商成功删除
+identityProviderAlreadyLinkedMessage=链接的身份 {0} 已经连接到已有用户。
+staleCodeAccountMessage=页面过期。请再试一次。
+consentDenied=不同意
+
+accountDisabledMessage=账户已经关闭,请联系管理员
+
+accountTemporarilyDisabledMessage=账户暂时关闭,请联系管理员或稍后再试。
+invalidPasswordMinLengthMessage=无效的密码:最短长度 {0}.
+invalidPasswordMinLowerCaseCharsMessage=无效的密码: 至少包含 {0} 小写字母。
+invalidPasswordMinDigitsMessage=无效的密码: 至少包含 {0} 数字。
+invalidPasswordMinUpperCaseCharsMessage=无效的密码: 至少包含 {0} 大写字母
+invalidPasswordMinSpecialCharsMessage=无效的密码: 至少包含 {0} 个特殊字符
+invalidPasswordNotUsernameMessage=无效的密码: 不能与用户名相同
+invalidPasswordRegexPatternMessage=无效的密码: 无法与正则表达式匹配
+invalidPasswordHistoryMessage=无效的密码: 不能与之前的{0} 个旧密码相同
+locale_ca=Català
+locale_de=Deutsch
+locale_en=English
+locale_es=Español
+locale_fr=Français
+locale_it=Italian
+locale_ja=日本語
+locale_no=Norsk
+locale_lt=Lietuvių
+locale_pt-BR=Português (Brasil)
+locale_ru=Русский
+locale_zh-CN=中文简体
diff --git a/themes/src/main/resources/theme/base/account/template.ftl b/themes/src/main/resources/theme/base/account/template.ftl
index c117cff..bc59407 100644
--- a/themes/src/main/resources/theme/base/account/template.ftl
+++ b/themes/src/main/resources/theme/base/account/template.ftl
@@ -43,8 +43,8 @@
</div>
<li>
</#if>
- <#if referrer?has_content && referrer.url?has_content><li><a href="${referrer.url}" id="referrer">${msg("backTo",referrer.name)}</a></li></#if>
- <li><a href="${url.logoutUrl}">${msg("doSignOut")}</a></li>
+ <#if referrer?has_content && referrer.url?has_content><li><a href="${referrer.url?html}" id="referrer">${msg("backTo",referrer.name?html)}</a></li></#if>
+ <li><a href="${url.logoutUrl?html}">${msg("doSignOut")}</a></li>
</ul>
</div>
</div>
diff --git a/themes/src/main/resources/theme/base/account/theme.properties b/themes/src/main/resources/theme/base/account/theme.properties
new file mode 100644
index 0000000..b9c3990
--- /dev/null
+++ b/themes/src/main/resources/theme/base/account/theme.properties
@@ -0,0 +1 @@
+locales=ca,de,en,es,fr,it,ja,lt,no,pt-BR,ru,zh-CN
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/index.ftl b/themes/src/main/resources/theme/base/admin/index.ftl
index 7717904..aebc488 100755
--- a/themes/src/main/resources/theme/base/admin/index.ftl
+++ b/themes/src/main/resources/theme/base/admin/index.ftl
@@ -20,29 +20,46 @@
var consoleBaseUrl = '${consoleBaseUrl}';
var resourceUrl = '${resourceUrl}';
var masterRealm = '${masterRealm}';
+ var resourceVersion = '${resourceVersion}';
</script>
- <script src="${resourceUrl}/lib/jquery/jquery-1.10.2.js" type="text/javascript"></script>
- <script src="${resourceUrl}/lib/select2-3.4.1/select2.min.js" type="text/javascript"></script>
-
- <script src="${resourceUrl}/lib/angular/angular.js"></script>
- <script src="${resourceUrl}/lib/angular/angular-resource.js"></script>
- <script src="${resourceUrl}/lib/angular/angular-route.js"></script>
- <script src="${resourceUrl}/lib/angular/angular-cookies.js"></script>
- <script src="${resourceUrl}/lib/angular/angular-sanitize.js"></script>
- <script src="${resourceUrl}/lib/angular/angular-translate.js"></script>
- <script src="${resourceUrl}/lib/angular/angular-translate-loader-url.js"></script>
- <script src="${resourceUrl}/lib/angular/treeview/angular.treeview.js"></script>
+ <!-- Minimized versions (for those that have one) -->
+ <script src="${resourceUrl}/node_modules/jquery/dist/jquery.min.js" type="text/javascript"></script>
+ <script src="${resourceUrl}/node_modules/select2/select2.js" type="text/javascript"></script>
+ <script src="${resourceUrl}/node_modules/angular/angular.min.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-resource/angular-resource.min.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-route/angular-route.min.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-cookies/angular-cookies.min.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-sanitize/angular-sanitize.min.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-translate/dist/angular-translate.min.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-translate-loader-url/angular-translate-loader-url.min.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-ui-select2/src/select2.js" type="text/javascript"></script>
+ <script src="${resourceUrl}/node_modules/autofill-event/autofill-event.js"></script>
+
+
+ <!-- Unminimized versions
+ <script src="${resourceUrl}/node_modules/jquery/dist/jquery.js" type="text/javascript"></script>
+ <script src="${resourceUrl}/node_modules/select2/select2.js" type="text/javascript"></script>
+ <script src="${resourceUrl}/node_modules/angular/angular.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-resource/angular-resource.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-route/angular-route.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-cookies/angular-cookies.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-sanitize/angular-sanitize.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-translate/dist/angular-translate.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-translate-loader-url/angular-translate-loader-url.js"></script>
+ <script src="${resourceUrl}/node_modules/angular-ui-select2/src/select2.js" type="text/javascript"></script>
+ <script src="${resourceUrl}/node_modules/autofill-event/autofill-event.js"></script>
+ -->
+
+ <!-- Libraries not managed by yarn -->
<script src="${resourceUrl}/lib/angular/ui-bootstrap-tpls-0.11.0.js"></script>
-
- <script src="${resourceUrl}/lib/angular/select2.js" type="text/javascript"></script>
+ <script src="${resourceUrl}/lib/angular/treeview/angular.treeview.js"></script>
<script src="${resourceUrl}/lib/fileupload/angular-file-upload.min.js"></script>
<script src="${resourceUrl}/lib/filesaver/FileSaver.js"></script>
<script src="${resourceUrl}/lib/ui-ace/min/ace.js"></script>
<script src="${resourceUrl}/lib/ui-ace/ui-ace.min.js"></script>
- <script src="${resourceUrl}/lib/autofill-event/autofill-event-1.0.0.js"></script>
- <script src="${authUrl}/js/${resourceVersion}/keycloak.js" type="text/javascript"></script>
+ <script src="${authUrl}/js/keycloak.js?version=${resourceVersion}" type="text/javascript"></script>
<script src="${resourceUrl}/js/app.js" type="text/javascript"></script>
<script src="${resourceUrl}/js/controllers/realm.js" type="text/javascript"></script>
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 28a985c..f261105 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -574,6 +574,17 @@ key=Key
stackoverflow.key.tooltip=The Key obtained from Stack Overflow client registration.
openshift.base-url=Base Url
openshift.base-url.tooltip=Base Url to Openshift Online API
+gitlab-application-id=Application Id
+gitlab-application-secret=Application Secret
+gitlab.application-id.tooltip=Application Id for the application you created in your GitLab Applications account menu
+gitlab.application-secret.tooltip=Secret for the application that you created in your GitLab Applications account menu
+gitlab.default-scopes.tooltip=Scopes to ask for on login. Will always ask for openid. Additionally adds api if you do not specify anything.
+
+bitbucket-consumer-key=Consumer Key
+bitbucket-consumer-secret=Consumer Secret
+bitbucket.key.tooltip=Bitbucket OAuth Consumer Key
+bitbucket.secret.tooltip=Bitbucket OAuth Consumer Secret
+bitbucket.default-scopes.tooltip=Scopes to ask for on login. If you do not specify anything, scope defaults to 'email'.
# User federation
sync-ldap-roles-to-keycloak=Sync LDAP Roles To Keycloak
@@ -833,6 +844,8 @@ reset-credentials=Reset Credentials
reset-credentials.tooltip=Select the flow you want to use when the user has forgotten their credentials.
client-authentication=Client Authentication
client-authentication.tooltip=Select the flow you want to use for authentication of clients.
+docker-auth=Docker Authentication
+docker-auth.tooptip=Select the flow you want to use for authenticatoin against a docker client.
new=New
copy=Copy
add-execution=Add execution
@@ -1327,6 +1340,8 @@ manage-permissions-group.tooltip=Fine grain permssions for admins that want to m
manage-authz-group-scope-description=Policies that decide if an admin can manage this group
view-authz-group-scope-description=Policies that decide if an admin can view this group
view-members-authz-group-scope-description=Policies that decide if an admin can manage the members of this group
+exchange-to-authz-client-scope-description=Policies that decide which clients are allowed exchange tokens for a token that is targeted to this client.
+exchange-from-authz-client-scope-description=Policies that decide which clients are allowed to exchange tokens that were generated for this client.
manage-authz-client-scope-description=Policies that decide if an admin can manage this client
configure-authz-client-scope-description=Reduced management permissions for admin. Cannot set scope, template, or protocol mappers.
view-authz-client-scope-description=Policies that decide if an admin can view this client
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_zh_CN.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_zh_CN.properties
new file mode 100644
index 0000000..8d2dd08
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_zh_CN.properties
@@ -0,0 +1,1227 @@
+consoleTitle=Keycloak管理界面
+
+# Common messages
+enabled=开启
+name=名称
+displayName=显示名称
+displayNameHtml=HTML 显示名称
+save=保存
+cancel=取消
+onText=开
+offText=关
+client=客户端
+clients=客户端
+clear=清除
+selectOne=选择一个...
+
+true=是
+false=否
+
+endpoints=服务路径
+
+# Realm settings
+realm-detail.enabled.tooltip=只有当域启用时,用户和客户程序才能访问
+realm-detail.oidc-endpoints.tooltip=显示openID connect服务路径的配置
+registrationAllowed=用户注册
+registrationAllowed.tooltip=开启/关闭注册页面,注册页面的链接也会显示在登录页面
+registrationEmailAsUsername=Email当做用户名
+registrationEmailAsUsername.tooltip=当开启时注册表单的用户名域会被隐藏而且Email会作为新用户的用户名
+editUsernameAllowed=编辑用户名
+editUsernameAllowed.tooltip=如果开启,用户名域是可以编辑的。否则用户名域是只读的。
+resetPasswordAllowed=忘记密码
+resetPasswordAllowed.tooltip=当用户忘记他们的密码时,在登录界面显示一个连接给用户点击。
+rememberMe=记住我
+rememberMe.tooltip=显示一个选择框来让用户在重启浏览器时仍然处于登录状态,直到会话过期。
+verifyEmail=验证email
+verifyEmail.tooltip=要求用户在首次登录时验证他们的邮箱。
+sslRequired=需要SSL
+sslRequired.option.all=所有请求
+sslRequired.option.external=外部请求
+sslRequired.option.none=无
+sslRequired.tooltip=是否需要HTTPS?‘无’代表对于任何客户端IP地址都不需要HTTPS,‘外部请求’代表localhost和私有ip地址可以不通过https访问,‘所有请求’代表所有IP地址都需要通过https访问。
+publicKey=公有秘钥
+privateKey=私有秘钥
+gen-new-keys=生成新秘钥
+certificate=证书
+host=主机
+smtp-host=SMTP 主机
+port=端口号
+smtp-port=SMTP 端口号(默认25)
+from=来自
+sender-email-addr=邮件发送者email地址
+enable-ssl=启用 SSL
+enable-start-tls=启用 StartTLS
+enable-auth=启用认证
+username=用户名
+login-username=登录用户名
+password=密码
+login-password=登录密码
+login-theme=登录主题
+login-theme.tooltip=为登录、授权、注册、忘记密码界面选择页面主题
+account-theme=账户主题
+account-theme.tooltip=为用户管理界面选择主题
+admin-console-theme=管理员控制台主题
+select-theme-admin-console=为管理员控制台选择主题
+email-theme=邮件主题
+select-theme-email=为服务器发送的邮件选择主题
+i18n-enabled=启用国际化
+supported-locales=支持的语言
+supported-locales.placeholder=输入一个locale并按回车
+default-locale=默认语言
+realm-cache-clear=域缓存
+realm-cache-clear.tooltip=从域缓存中清理所有条目(这会清理所有域的条目)
+user-cache-clear=用户缓存
+user-cache-clear.tooltip=清理用户缓存的所有条目(这会清理所有域中的条目)
+revoke-refresh-token=收回 Refresh Token
+revoke-refresh-token.tooltip=如果开启 refresh tokens只能使用一次,否则refresh token不会被收回并且可以使用多次
+sso-session-idle=SSO会话空闲时间
+seconds=秒
+minutes=分
+hours=小时
+days=天
+sso-session-max=SSO会话最长时间
+sso-session-idle.tooltip=设置会话在过期之前可以空闲的时间长度,当会话过期时 Token 和浏览器会话都会被设置为无效。
+sso-session-max.tooltip=会话的最大时间长度,当会话过期时 Token 和浏览器会话都会被设置为无效。
+offline-session-idle=离线会话的空闲时间
+offline-session-idle.tooltip=离线会话允许的空闲时间。你需要使用离线Token在这段时间内至少刷新一次否则会话就会过期
+access-token-lifespan=Access Token 有效期
+access-token-lifespan.tooltip=access token最长有效时间,这个值推荐要比SSO超时要短一些。
+access-token-lifespan-for-implicit-flow =隐式流的访问令牌生命周期
+access-token-lifespan-for-implicit-flow.tooltip =在OpenID连接隐式流期间发出的访问令牌到期之前的最长时间。建议该值小于SSO超时。没有可能在隐式流期间刷新令牌,这就是为什么有单独的超时不同于“访问令牌寿命”。
+client-login-timeout =客户端登录超时
+client-login-timeout.tooltip =客户端必须完成访问令牌协议的最大时间。这通常是1分钟。
+login-timeout =登录超时
+login-timeout.tooltip =用户必须完成登录的最长时间。这建议比较长。 30分钟以上。
+login-action-timeout =登录操作超时
+login-action-timeout.tooltip =用户必须完成登录相关操作(如更新密码或配置totp)的最长时间。这建议比较长。 5分钟以上。
+headers =标题
+brute-force-detection=强力检测
+x-frame-options = X-Frame-Options
+x-frame-options-tooltip =默认值阻止通过非源iframe包含页面(单击标签了解更多信息)
+content-sec-policy = Content-Security-Policy
+content-sec-policy-tooltip =默认值阻止通过非源iframe包含网页(点击标签了解更多信息)
+content-type-options = X-Content-Type-Options
+content-type-options-tooltip =默认值阻止Internet Explorer和Google Chrome从已声明的内容类型中嗅探响应(点击标签了解更多信息)
+max-login-failures =最大登录失败
+max-login-failures.tooltip =触发等待之前的失败次数。
+wait-increment =等待增量
+wait-increment.tooltip =当满足故障阈值时,用户应该锁定多长时间?
+quick-login-check-millis =快速登录检查Milli秒
+quick-login-check-millis.tooltip =如果故障同时发生太快,则锁定用户。
+min-quick-login-wait =最小快速登录等待
+min-quick-login-wait.tooltip =快速登录失败后等待多长时间。
+max-wait = Max Wait
+max-wait.tooltip =用户将被锁定的最长时间。
+failure-reset-time =故障复位时间
+failure-reset-time.tooltip =何时将故障计数复位?
+realm-tab-login=登录
+realm-tab-keys=秘钥
+realm-tab-email=Email
+realm-tab-themes=主题
+realm-tab-cache=缓存
+realm-tab-tokens=Tokens
+realm-tab-client-registration=客户端注册
+realm-tab-security-defenses=安全防护
+realm-tab-general=通用
+add-realm=添加域
+
+#Session settings
+realm-sessions=域会话
+revocation=回收
+logout-all=登出所有
+active-sessions=活跃的会话
+sessions=会话
+not-before=不早于
+not-before.tooltip=回收早于日期授予的token
+set-to-now=设置到现在
+push=推送
+push.tooltip=对于每个拥有管理员权限的用户,通知他们新的回收策略
+#Protocol Mapper
+usermodel.prop.label=属性
+usermodel.prop.tooltip=UserModel 接口中属性方法的名字. 例如, 'email' 会引用UserModel.getEmail() 方法.
+usermodel.attr.label=用户属性
+usermodel.attr.tooltip=在UserModel.attribute映射中定义的存储的用户属性名。
+userSession.modelNote.label=用户会话标记
+userSession.modelNote.tooltip=用户会话标记在 UserSessionModel.note映射中的属性名。
+multivalued.label=多值的
+multivalued.tooltip=表示此值是否支持多值.如果为真,所有值会设置为已知。如果为假,只有第一个值是已知。
+selectRole.label=选择角色
+selectRole.tooltip=在左边文本框输入角色或点击这个按钮浏览并选择您想要的角色。
+tokenClaimName.label=Token申请名
+tokenClaimName.tooltip=token中加入的申请者名. 这可以是个完整的分级信息例如 'address.street'. 这种情况下,会生成一个复杂的json回复
+jsonType.label=申请 JSON 的类型
+jsonType.tooltip=用来展现申请的JSON 类型 long, int, boolean, 和 String 是有效值
+includeInIdToken.label =添加到ID令牌
+includeInIdToken.tooltip =是否应将声明添加到ID令牌?
+includeInAccessToken.label =添加到访问令牌
+includeInAccessToken.tooltip =是否应该将声明添加到访问令牌?
+includeInUserInfo.label =添加到userinfo
+includeInUserInfo.tooltip =是否应该将声明添加到userinfo?
+usermodel.clientRoleMapping.clientId.label =客户端ID
+usermodel.clientRoleMapping.clientId.tooltip =角色映射的客户端ID
+usermodel.clientRoleMapping.rolePrefix.label =客户端角色前缀
+usermodel.clientRoleMapping.rolePrefix.tooltip =每个客户端角色的前缀(可选)。
+usermodel.realmRoleMapping.rolePrefix.label = Realm角色前缀
+usermodel.realmRoleMapping.rolePrefix.tooltip =每个领域角色的前缀(可选)。
+sectorIdentifierUri.label =扇区标识符URI
+sectorIdentifierUri.tooltip =使用成对子值和支持的提供程序动态客户端注册应使用sector_identifier_uri参数。它为一组在共同管理控制下的网站提供了一种独立于各个域名的具有一致的成对子值的方法。它还为客户端更改redirect_uri域而不必重新注册其所有用户提供了一种方法。
+pairwiseSubAlgorithmSalt.label = Salt
+pairwiseSubAlgorithmSalt.tooltip =计算成对主体标识符时使用的盐。如果留空,将产生盐。
+
+
+
+# client details
+clients.tooltip=客户端是域中受信任的应用程序和web应用. 这些程序可以发起登录.您也可以定义应用的角色。
+search.placeholder=搜索...
+create=创建
+import=导入
+client-id=客户端 ID
+base-url=根 URL
+actions=操作
+not-defined=未定义
+edit=编辑
+delete=删除
+no-results=无记录
+no-clients-available=无可用客户
+add-client=添加客户端
+select-file=选择文件
+view-details=查看详情
+clear-import=清除导入
+client-id.tooltip =指定在URI和令牌中引用的ID。例如“my-client”。对于SAML,这也是authn请求的预期发放者值
+client.name.tooltip =指定客户端的显示名称。例如“我的客户端”。支持本地化值的键。例如\\uff1a$ {my_client}
+client.enabled.tooltip =禁用客户端无法启动登录或获取访问令牌。
+consent-required =同意必需
+consent-required.tooltip =如果已启用的用户必须同意客户端访问。
+client-protocol =客户端协议
+client-protocol.tooltip ='OpenID connect'允许客户端基于授权服务器执行的认证来验证最终用户的身份。'SAML'启用基于Web的身份验证和授权方案,包括跨域单点登录(SSO),并使用包含断言的安全令牌传递信息。
+access-type =访问类型
+access-type.tooltip ='机密'客户端需要一个秘密启动登录协议。 “公共”客户不需要一个秘密。 “仅承载”客户端是从不启动登录的Web服务。
+standard-flow-enabled =启用标准流程
+standard-flow-enabled.tooltip =这使标准的基于OpenID Connect重定向的身份验证与授权码。根据OpenID Connect或OAuth2规范,这将支持此客户端的“授权代码流”。
+implicit-flow-enabled =启用隐式流
+implicit-flow-enabled.tooltip =这启用对无授权代码的基于OpenID Connect重定向的身份验证的支持。根据OpenID Connect或OAuth2规范,这将支持此客户端的“隐式流”。
+direct-access-grants-enabled =启用直接访问授权
+direct-access-grants-enabled.tooltip =这启用对直接访问授权的支持,这意味着客户端可以访问用户的用户名/密码,并直接与Keycloak服务器交换访问令牌。在OAuth2规范方面,这允许支持此客户端的“资源所有者密码凭据授权”。
+service-accounts-enabled =启用服务帐户
+service-accounts-enabled.tooltip =允许您向Keycloak验证此客户端并检索专用于此客户端的访问令牌。在OAuth2规范方面,这将支持此客户端的“客户端凭据授予”。
+include-authnstatement = Include AuthnStatement
+include-authnstatement.tooltip =是否应该在登录响应中包含指定方法和时间戳的语句?
+sign-documents =签署文件
+sign-documents.tooltip = SAML文档是否应该由领域签名?
+sign-documents-redirect-enable-key-info-ext =优化REDIRECT签名密钥查找
+sign-documents-redirect-enable-key-info-ext.tooltip =在由Keycloak适配器保护的SP的REDIRECT绑定中签名SAML文档时,如果签名密钥的ID包含在<Extensions>元素中的SAML协议消息中?这将优化签名的验证,因为验证方使用单个密钥,而不是尝试每个已知密钥进行验证。
+sign-assertions =符号断言
+sign-assertions.tooltip = SAML文档中的断言是否应该签名?如果文档已签署,则不需要此设置。
+signature-algorithm =签名算法
+signature-algorithm.tooltip =用于签署文档的签名算法。
+canonicalization-method =规范化方法
+canonicalization-method.tooltip = XML签名的规范化方法。
+encrypt-assertions =加密断言
+encrypt-assertions.tooltip =是否应使用AES通过客户端的公钥对SAML断言进行加密?
+client-signature-required =需要客户端签名
+client-signature-required.tooltip =客户端是否签署了saml请求和响应?他们应该验证吗?
+force-post-binding =强制POST绑定
+force-post-binding.tooltip =始终对POST响应使用POST绑定。
+front-channel-logout =前通道注销
+front-channel-logout.tooltip =当为true时,注销需要浏览器重定向到客户端。当为false时,服务器对注销执行后台调用。
+force-name-id-format =强制名称ID格式
+force-name-id-format.tooltip =忽略请求的NameID主题格式并使用管理控制台配置的。
+name-id-format =名称ID格式
+name-id-format.tooltip =要用于主题的名称ID格式。
+root-url =根URL
+root-url.tooltip =附加到相对URL的根URL
+valid-redirect-uris =有效的重定向URI
+valid-redirect-uris.tooltip =浏览器可以在成功登录或注销后重定向到的有效URI模式。允许使用简单通配符,即“http://example.com/*”。也可以指定相对路径,即/ my / relative / path / *。相对路径是相对于客户端根URL的,如果没有指定,则使用auth服务器根URL。对于SAML,如果您依赖嵌入登录请求的使用者服务URL,则必须设置有效的URI模式。
+base-url.tooltip =当auth服务器需要重定向或链接回客户端时使用的默认URL。
+admin-url =管理员网址
+admin-url.tooltip =客户端管理界面的URL。如果客户端支持适配器REST API,请设置此选项。此REST API允许auth服务器推送吊销策略和其他管理任务。通常将此设置为客户端的基本URL。
+master-saml-processing-url =主SAML处理URL
+master-saml-processing-url.tooltip =如果配置,此URL将用于每次绑定到SP的断言使用者和单一注销服务。这可以对细粒度SAML端点配置中的每个绑定和服务单独进行覆盖。
+idp-sso-url-ref = IDP发起的SSO URL名称
+idp-sso-url-ref.tooltip =当您想要进行IDP发起的SSO时,引用客户端的URL片段名称。留下此空将禁用IDP启动的SSO。您将从浏览器引用的URL为:{server-root} / realms / {realm} / protocol / saml / clients / {client-url-name}
+idp-sso-relay-state = IDP发起的SSO中继状态
+idp-sso-relay-state.tooltip =当您想要执行IDP发起的SSO时,要使用SAML请求发送的中继状态。
+web-origins = Web起源
+web-origins.tooltip =允许的CORS起点。要允许有效重定向URI的所有来源,请添加“+”。允许所有起点添加'*'。
+fine-oidc-endpoint-conf = Fine Grain OpenID连接配置
+fine-oidc-endpoint-conf.tooltip =展开此部分以配置与OpenID Connect协议相关的此客户端的高级设置
+user-info-signed-response-alg =用户信息签名的响应算法
+user-info-signed-response-alg.tooltip =用于签名的用户信息端点响应的JWA算法。如果设置为“unsigned”,则用户信息响应将不会被签名,并将以application / json格式返回。
+request-object-signature-alg =请求对象签名算法
+request-object-signature-alg.tooltip = JWA算法,客户端在发送由'request'或'request_uri'参数指定的OIDC请求对象时需要使用。如果设置为“any”,则Request对象可以由任何算法(包括“none”)签名。
+fine-saml-endpoint-conf =细粒度SAML端点配置
+fine-saml-endpoint-conf.tooltip =展开此部分以配置Assertion Consumer和单一注销服务的确切URL。
+assertion-consumer-post-binding-url =断言使用者服务POST绑定URL
+assertion-consumer-post-binding-url.tooltip = SAML POST绑定客户端断言使用者服务的URL(登录响应)。如果您没有此绑定的URL,则可以将此字段留空。
+assertion-consumer-redirect-binding-url =断言使用者服务重定向绑定URL
+assertion-consumer-redirect-binding-url.tooltip = SAML重定向客户端断言使用者服务的绑定URL(登录响应)。如果您没有此绑定的URL,则可以将此字段留空。
+logout-service-post-binding-url =注销服务POST绑定URL
+logout-service-post-binding-url.tooltip = SAML POST绑定客户端单一注销服务的URL。如果使用不同的绑定,则可以将此留空
+logout-service-redir-binding-url =注销服务重定向绑定URL
+logout-service-redir-binding-url.tooltip = SAML重定向客户端单一注销服务的绑定URL。如果使用不同的绑定,则可以将此留空。
+
+#client import
+import-client =导入客户端
+format-option =格式选项
+select-format =选择格式
+import-file =导入文件
+
+#client tabs
+settings =设置
+credentials =凭据
+saml-keys = SAML键
+roles =角色
+mappers = Mappers
+mappers.tooltip =协议映射器对令牌和文档执行转换。他们可以做一些事情,例如将用户数据映射到协议声明中,或者只是转换客户端和身份验证服务器之间的任何请求。
+scope =作用域
+scope.tooltip =作用域映射允许您限制哪些用户角色映射包含在客户端请求的访问令牌中。
+sessions.tooltip =查看此客户端的活动会话。允许您查看哪些用户处于活动状态,以及他们何时登录。
+offline-access =离线访问
+offline-access.tooltip =查看此客户端的离线会话。允许您查看哪些用户检索离线令牌以及何时检索离线令牌。要撤销客户端的所有令牌,请转到撤销选项卡,并将不早于值设置到现在。
+clustering =聚类
+installation =安装
+installation.tooltip =用于生成各种客户端适配器配置格式的帮助程序实用程序,您可以下载或剪切和粘贴以配置您的客户端。
+service-account-roles =服务帐户角色
+service-account-roles.tooltip =允许您为专用于此客户端的服务帐户验证角色映射。
+
+# client credentials
+client-authenticator =客户端认证器
+client-authenticator.tooltip =客户端身份验证器用于认证此客户端对Keycloak服务器
+certificate.tooltip =客户端发出的验证JWT的客户端证书,由客户端私钥从您的密钥库签名。
+publicKey.tooltip =由客户端发出并由客户端私钥签署的validate JWT的公钥。
+no-client-certificate-configured =未配置客户端证书
+gen-new-keys-and-cert =生成新密钥和证书
+import-certificate =导入证书
+gen-client-private-key =生成客户端私钥
+generate-private-key =生成私钥
+kid =孩子
+kid.tooltip =来自导入的JWKS的客户端公钥的KID(密钥ID)。
+use-jwks-url =使用JWKS URL
+use-jwks-url.tooltip =如果开关打开,那么将从给定的JWKS URL下载客户端公钥。这允许很大的灵活性,因为当客户端生成新的密钥对时,新密钥将总是重新下载。如果交换机关闭,则使用来自Keycloak DB的公钥(或证书),因此当客户端密钥更改时,您总是需要将新密钥(或证书)导入到Keycloak数据库。
+jwks-url = JWKS URL
+jwks-url.tooltip =存储JWK格式的客户端密钥的URL。有关更多详细信息,请参阅JWK规范。如果您使用带有“jwt”凭据的keycloak客户端适配器,那么您可以使用带有'/ k_jwks'后缀的应用程序的URL。例如“http://www.myhost.com/myapp/k_jwks”。
+archive-format =归档格式
+archive-format.tooltip = Java密钥库或PKCS12归档格式。
+key-alias =密钥别名
+key-alias.tooltip =存档您的私钥和证书的别名。
+key-password =密钥密码
+key-password.tooltip =访问存档中私钥的密码
+store-password =存储密码
+store-password.tooltip =访问归档本身的密码
+generate-and-download =生成和下载
+client-certificate-import =客户端证书导入
+import-client-certificate =导入客户端证书
+jwt-import.key-alias.tooltip =您的证书的归档别名。
+secret =秘密
+regenerate-secret =重生秘密
+registrationAccessToken =注册访问令牌
+registrationAccessToken.regenerate =重新生成注册访问令牌
+registrationAccessToken.tooltip =注册访问令牌为客户端提供对客户端注册服务的访问。
+add-role =添加角色
+role-name =角色名称
+composite = Composite
+description =描述
+no-client-roles-available =没有可用的客户端角色
+scope-param-required = Scope Param必需
+scope-param-required.tooltip =只有在授权/令牌请求期间使用具有角色名称的scope参数时,才会授予此角色。
+composite-roles =复合角色
+composite-roles.tooltip =当将此角色(un)分配给用户时,与其关联的任何角色将被隐式分配(un)。
+realm-roles = Realm角色
+available-roles =可用角色
+add-selected =添加选择
+associated-roles =关联角色
+composite.associated-realm-roles.tooltip =与此组合角色关联的领域级角色。
+composite.available-realm-roles.tooltip =您可以关联到此组合角色的领域级角色。
+remove-selected =删除所选项
+client-roles =客户端角色
+select-client-to-view-roles =选择客户端以查看客户端的角色
+available-roles.tooltip =您可以与此组合角色关联的来自此客户端的角色。
+client.associated-roles.tooltip =与此组合角色关联的客户端角色。
+add-builtin =添加内置
+category = Category
+type = Type
+no-mappers-available =没有可用的映射器
+add-builtin-protocol-mappers =添加内置协议映射器
+add-builtin-protocol-mapper =添加内置协议映射器
+scope-mappings =范围映射
+full-scope-allowed =允许的全范围
+full-scope-allowed.tooltip =允许您禁用所有限制。
+scope.available-roles.tooltip =可以分配到范围的领域级角色。
+assigned-roles =分配的角色
+assigned-roles.tooltip =分配给范围的领域级角色。
+effective-roles =有效角色
+realm.effective-roles.tooltip =可能已从组合角色继承的分配的领域级角色。
+select-client-roles.tooltip =选择客户端以查看客户端的角色
+assign.available-roles.tooltip =可分配的客户端角色。
+client.assigned-roles.tooltip =分配的客户端角色。
+client.effective-roles.tooltip =可能已从组合角色继承的分配的客户端角色。
+basic-configuration =基本配置
+node-reregistration-timeout =节点重新注册超时
+node-reregistration-timeout.tooltip =指定注册的客户端群集节点重新注册的最大时间的间隔。如果集群节点在此时间内不会向Keycloak发送重新注册请求,则它将从Keycloak注销
+registered-cluster-nodes =注册的集群节点
+register-node-manually =手动注册节点
+test-cluster-availability =测试集群可用性
+last-registration =最后一次注册
+node-host =节点主机
+no-registered-cluster-nodes =没有注册的集群节点可用
+cluster-nodes =集群节点
+add-node =添加节点
+active-sessions.tooltip =此客户端的活动用户会话的总数。
+show-sessions =显示会话
+show-sessions.tooltip =警告,这是一个潜在昂贵的操作,取决于活动会话的数量。
+user =用户
+from-ip =从IP
+session-start =会话开始
+first-page=第一页
+previous-page=上一页
+next-page =下一页
+client-revoke.not-before.tooltip =撤销此客户端在此日期之前发出的任何令牌。
+client-revoke.push.tooltip =如果为此客户端配置了管理URL,请将此策略推送到该客户端。
+select-a-format =选择格式
+download=下载
+offline-tokens =脱机令牌
+offline-tokens.tooltip =此客户端的脱机令牌的总数。
+show-offline-tokens =显示脱机令牌
+show-offline-tokens.tooltip =警告,这是一个潜在的昂贵的操作,取决于脱机令牌的数量。
+token-issued =发出的令牌
+last-access=最后访问
+last-refresh =上次刷新
+key-export =密钥导出
+key-import =密钥导入
+export-saml-key =导出SAML密钥
+import-saml-key =导入SAML密钥
+realm-certificate-alias =域证书别名
+realm-certificate-alias.tooltip = Realm证书也存储在归档中。这是它的别名。
+signing-key =签名密钥
+saml-signing-key = SAML签名密钥。
+private-key =私钥
+generate-new-keys =生成新密钥
+export =导出
+encryption-key =加密密钥
+saml-encryption-key.tooltip = SAML加密密钥。
+service-accounts =服务帐户
+service-account.available-roles.tooltip =可以分配给服务帐户的领域级角色。
+service-account.assigned-roles.tooltip =分配给服务帐户的领域级角色。
+service-account-is-not-enabled-for = {{client}}未启用服务帐户
+create-protocol-mappers =创建协议映射器
+create-protocol-mapper =创建协议映射器
+protocol =协议
+protocol.tooltip =协议...
+id = ID
+mapper.name.tooltip =映射器的名称。
+mapper.consent-required.tooltip =授予临时访问权限时,用户是否同意向客户端提供此数据?
+consent-text =同意文本
+consent-text.tooltip =在同意页面上显示的文本。
+mapper-type =映射器类型
+mapper-type.tooltip =映射程序的类型
+# realm identity providers
+identity-providers =身份提供者
+table-of-identity-providers =身份提供程序表
+add-provider.placeholder =添加提供程序...
+provider =提供程序
+gui-order = GUI顺序
+first-broker-login-flow =第一登录流
+post-broker-login-flow =登录后流程
+redirect-uri =重定向URI
+redirect-uri.tooltip =配置身份提供程序时要使用的重定向uri。
+alias =别名
+display-name =显示名称
+identity-provider.alias.tooltip =别名唯一标识身份提供者,它也用于构建重定向uri。
+identity-provider.display-name.tooltip =身份提供者的友好名称。
+identity-provider.enabled.tooltip =启用/禁用此身份提供程序。
+authenticate-by-default =默认验证
+identity-provider.authenticate-by-default.tooltip =指示在显示登录屏幕之前是否应默认尝试此提供程序进行身份验证。
+store-tokens =存储令牌
+identity-provider.store-tokens.tooltip =如果在验证用户后必须存储令牌,则启用/禁用。
+stored-tokens-readable=存储令牌可读
+identity-provider.stored-tokens-readable.tooltip =如果新用户可以读取任何存储的令牌,则启用/禁用。这将分配broker.read-token角色。
+disableUserInfo =禁用用户信息
+identity-provider.disableUserInfo.tooltip =禁用用户信息服务的使用以获取其他用户信息?默认是使用此OIDC服务。
+userIp =使用userIp参数
+identity-provider.google-userIp.tooltip =在Google的用户信息服务上调用时设置'userIp'查询参数。这将使用用户的IP地址。如果Google正在限制对用户信息服务的访问,则此选项非常有用。
+update-profile-on-first-login =首次登录时更新配置文件
+on =开
+on-missing-info =缺少信息
+off =关闭
+update-profile-on-first-login.tooltip =定义用户在首次登录期间必须更新其配置文件的条件。
+trust-email =信任电子邮件
+trust-email.tooltip =如果启用,则此提供商提供的电子邮件不会验证,即使已启用对领域的验证。
+gui-order.tooltip = GUI中提供者的定义顺序的数字(例如,在登录页面上)。
+first-broker-login-flow.tooltip =认证流的别名,在首次使用此身份提供者登录后触发。术语“首次登录”意味着尚未存在与认证身份提供商帐户链接的Keycloak帐户。
+post-broker-login-flow.tooltip =认证流的别名,在每次使用此身份提供程序登录后触发。如果您需要对通过此身份提供程序(例如OTP)验证的每个用户进行额外验证,这将非常有用。如果您不希望在使用此身份提供商登录后触发任何其他验证器,请将此空白留空。还要注意,认证者实现必须假定用户已经在ClientSession中设置为身份提供者已经设置。
+openid-connect-config = OpenID连接配置
+openid-connect-config.tooltip = OIDC SP和外部IDP配置。
+authorization-url =授权URL
+authorization-url.tooltip =授权网址。
+token-url =令牌URL
+token-url.tooltip =令牌URL。
+logout-url =注销URL
+identity-provider.logout-url.tooltip =用于从外部IDP注销用户的会话终结点。
+backchannel-logout = Backchannel注销
+backchannel-logout.tooltip =外部IDP是否支持反向通道注销?
+user-info-url =用户信息URL
+user-info-url.tooltip =用户信息网址。这是可选的。
+identity-provider.client-id.tooltip =在身份提供者中注册的客户端或客户端标识符。
+client-secret =客户端密钥
+show-secret =显示密码
+hide-secret =隐藏秘密
+client-secret.tooltip =在身份提供程序中注册的客户端或客户端机密。
+issuer =发行人
+issuer.tooltip =响应的发行者的发行者标识符。如果未提供,将不执行验证。
+default-scopes =默认范围
+identity-provider.default-scopes.tooltip =在请求授权时要发送的作用域。它可以是以空格分隔的范围列表。默认为'openid'。
+prompt =提示
+unspecified.option =未指定
+none.option = none
+consent.option =同意
+login.option = login
+select-account.option = select_account
+prompt.tooltip =指定授权服务器是否提示最终用户重新认证和同意。
+validate-signatures =验证签名
+identity-provider.validate-signatures.tooltip =启用/禁用外部IDP签名的签名验证。
+identity-provider.use-jwks-url.tooltip =如果交换机打开,那么将从给定的JWKS URL下载身份提供程序公钥。这允许很大的灵活性,因为当身份提供商生成新的密钥对时,新密钥将总是被重新下载。如果交换机关闭,则使用来自Keycloak DB的公钥(或证书),因此当身份提供商密钥更改时,您始终需要将新密钥导入到Keycloak数据库。
+identity-provider.jwks-url.tooltip =存储JWK格式的身份提供者密钥的URL。有关更多详细信息,请参阅JWK规范。如果你使用外部keycloak身份提供者,那么你可以使用像http:// broker-keycloak:8180 / auth / realms / test / protocol / openid-connect / certs这样的URL,假设你的代理keycloak是运行在http: / broker-keycloak:8180',它的境界是'test'。
+validating-public-key =验证公钥
+identity-provider.validating-public-key.tooltip =必须用于验证外部IDP签名的PEM格式的公钥。
+import-external-idp-config =导入外部IDP配置
+import-external-idp-config.tooltip =允许您从配置文件加载外部IDP元数据或从URL下载它。
+import-from-url =从URL导入
+identity-provider.import-from-url.tooltip =从远程IDP发现描述符导入元数据。
+import-from-file =从文件导入
+identity-provider.import-from-file.tooltip =从下载的IDP发现描述符导入元数据。
+saml-config = SAML配置
+identity-provider.saml-config.tooltip = SAML SP和外部IDP配置。
+single-signon-service-url =单点登录服务URL
+saml.single-signon-service-url.tooltip =必须用于发送认证请求(SAML AuthnRequest)的URL。
+single-logout-service-url =单一注销服务URL
+saml.single-logout-service-url.tooltip =必须用于发送注销请求的网址。
+nameid-policy-format = NameID策略格式
+nameid-policy-format.tooltip =指定与名称标识符格式相对应的URI引用。默认为urn:oasis:names:tc:SAML:2.0:nameid-format:persistent。
+http-post-binding-response = HTTP-POST绑定响应
+http-post-binding-response.tooltip =指示是否使用HTTP-POST绑定响应请求。如果为false,将使用HTTP-REDIRECT绑定。
+http-post-binding-for-authn-request = HTTP-POST AuthnRequest的绑定
+http-post-binding-for-authn-request.tooltip =指示是否必须使用HTTP-POST绑定发送AuthnRequest。如果为false,将使用HTTP-REDIRECT绑定。
+want-authn-requests-signed =需要AuthnRequests签名
+want-authn-requests-signed.tooltip =指示身份提供者是否期望签署AuthnRequest。
+force-authentication =强制验证
+identity-provider.force-authentication.tooltip =指示身份提供者是否必须直接认证演示者,而不是依赖以前的安全上下文。
+validate-signature =验证签名
+saml.validate-signature.tooltip =启用/禁用SAML响应的签名验证。
+validating-x509-certificate =验证X509证书
+validating-x509-certificate.tooltip =必须用于检查签名的PEM格式的证书。可以输入多个证书,用逗号(,)分隔。
+saml.import-from-url.tooltip =从远程IDP SAML实体描述符导入元数据。
+social.client-id.tooltip =向身份提供者注册的客户机标识符。
+social.client-secret.tooltip =向身份提供者注册的客户端密钥。
+social.default-scopes.tooltip =在请求授权时要发送的作用域。有关可能的值,分隔符和默认值,请参阅文档。
+key = Key
+stackoverflow.key.tooltip =从Stack Overflow客户端注册获取的密钥。
+
+# User federation
+sync-ldap-roles-to-keycloak =将LDAP角色同步到Keycloak
+sync-keycloak-roles-to-ldap =同步Keycloak到LDAP的角色
+sync-ldap-groups-to-keycloak =将LDAP组同步到Keycloak
+sync-keycloak-groups-to-ldap =同步Keycloak组到LDAP
+
+realms =领域
+realm = Realm
+
+identity-provider-mappers =身份提供者映射器
+create-identity-provider-mapper =创建身份提供者映射器
+add-identity-provider-mapper =添加身份提供者映射器
+client.description.tooltip =指定客户端的描述。例如“我的客户端的时间表”。支持本地化值的键。例如\\uff1a$ {my_client_description}
+
+expires =到期
+expiration =到期
+expiration.tooltip =指定令牌有效的时间
+count = Count
+count.tooltip =指定可以使用令牌创建多少个客户端
+remainingCount =剩余计数
+created =已创建
+back =返回
+initial-access-tokens=初始接入令牌
+add-initial-access-tokens =添加初始访问令牌
+initial-access-token=初始接入令牌
+initial-access.copyPaste.tooltip =在导航离开此页面之前复制/粘贴初始访问令牌,因为它不可能稍后检索
+continue =继续
+initial-access-token.confirm.title =复制初始访问令牌
+initial-access-token.confirm.text =在确认之前,请复制并粘贴初始访问令牌,因为以后无法检索
+no-initial-access-available =没有初始访问令牌可用
+
+client-reg-policies =客户端注册策略
+client-reg-policy.name.tooltip =显示策略的名称
+anonymous-policies =匿名访问策略
+anonymous-policies.tooltip =当客户端注册服务由未经身份验证的请求调用时,使用这些策略。这意味着请求不包含初始接入令牌或承载令牌。
+auth-policies =验证的访问策略
+auth-policies.tooltip =当通过认证请求调用客户端注册服务时使用这些策略。这意味着请求包含初始接入令牌或承载令牌。
+policy-name =策略名称
+no-client-reg-policies-configured =无客户端注册策略
+trusted-hosts.label =受信任的主机
+trusted-hosts.tooltip =主机列表,它们是受信任的,并且允许调用客户端注册服务和/或用作客户端URI的值。您可以使用主机名或IP地址。如果您在开头使用星号(例如“* .example.com”),则整个域example.com将受信任。
+host-sending-registration-request-must-match.label =主机发送客户端注册请求必须匹配
+host-sending-registration-request-must-match.tooltip =如果开启,则只要客户端注册服务是从某个受信任的主机或域发送的,就允许任何请求。
+client-uris-must-match.label =客户端URI必须匹配
+client-uris-must-match.tooltip =如果启用,则所有客户端URI(重定向URI和其他)只有在它们匹配一些受信任的主机或域时才允许。
+allowed-protocol-mappers.label =允许的协议映射器
+allowed-protocol-mappers.tooltip =允许的协议映射器提供程序的白名单。如果尝试注册客户端,其中包含一些未列入白名单的协议映射器,则注册请求将被拒绝。
+consent-required-for-all-mappers.label =需要同意Mappers
+consent-required-for-all-mappers.tooltip =如果打开,则所有新注册的协议映射器将自动具有consentRequired开启。这意味着用户将需要批准同意屏幕。注意:只有在客户端已启用consentRequired开关时,才会显示同意屏幕。所以通常很好地使用这个开关与需要同意的政策。
+allowed-client-templates.label =允许的客户端模板
+allowed-client-templates.tooltip =客户端模板的白名单,可以在新注册的客户端上使用。尝试向某个未列入白名单的客户端模板注册客户端将被拒绝。默认情况下,白名单为空,因此不允许任何客户端模板。
+max-clients.label =每个领域的最大客户端
+max-clients.tooltip =如果域中现有客户端的数量等于或大于配置的限制,将不允许注册新客户端。
+
+client-templates =客户端模板
+client-templates.tooltip =客户机模板允许您定义在多个客户机之间共享的公共配置
+
+groups =组
+
+group.add-selected.tooltip =可以分配给组的领域角色。
+group.assigned-roles.tooltip =映射到组的Realm角色
+group.effective-roles.tooltip =所有领域角色映射。这里的一些角色可能从映射组合角色继承。
+group.available-roles.tooltip =可从此客户端分配角色。
+group.assigned-roles-client.tooltip =此客户端的角色映射。
+group.effective-roles-client.tooltip =此客户端的角色映射。这里的一些角色可能从映射组合角色继承。
+
+default-roles =默认角色
+no-realm-roles-available =没有领域角色可用
+
+users =用户
+user.add-selected.tooltip =可以分配给用户的领域角色。
+user.assigned-roles.tooltip =映射到用户的Realm角色
+user.effective-roles.tooltip =所有领域角色映射。这里的一些角色可能从映射组合角色继承。
+user.available-roles.tooltip =可从此客户端分配角色。
+user.assigned-roles-client.tooltip =此客户端的角色映射。
+user.effective-roles-client.tooltip =此客户端的角色映射。这里的一些角色可能从映射组合角色继承。
+default.available-roles.tooltip =可以分配的领域级角色。
+realm-default-roles = Realm默认角色
+realm-default-roles.tooltip =分配给新用户的领域级别角色。
+default.available-roles-client.tooltip =可作为默认值分配的来自此客户端的角色。
+client-default-roles =客户端默认角色
+client-default-roles.tooltip =来自此客户端的作为默认角色分配的角色。
+composite.available-roles.tooltip =您可以关联到此组合角色的领域级角色。
+composite.associated-roles.tooltip =与此组合角色关联的领域级角色。
+composite.available-roles-client.tooltip =您可以与此组合角色关联的角色。
+composite.associated-roles-client.tooltip =与此组合角色关联的客户端角色。
+partial-import =部分导入
+partial-import.tooltip =部分导入允许您从先前导出的json文件导入用户,客户端和其他资源。
+file = File
+exported-json-file =导出的json文件
+import-from-realm =从领域导入
+import-users =导入用户
+import-groups =导入组
+import-clients =导入客户端
+import-identity-providers =导入身份提供者
+import-realm-roles =导入领域角色
+import-client-roles =导入客户端角色
+if-resource-exists =如果资源存在
+fail =失败
+skip =跳过
+overwrite =覆盖
+if-resource-exists.tooltip =指定在尝试导入已存在的资源时应该做什么。
+
+action = Action
+role-selector =角色选择器
+realm-roles.tooltip =可以选择的领域角色。
+
+select-a-role =选择角色
+select-realm-role =选择领域角色
+client-roles.tooltip =可以选择的客户端角色。
+select-client-role =选择客户端角色
+
+client-template =客户端模板
+client-template.tooltip =此客户端继承配置的客户端模板
+client-saml-endpoint =客户端SAML端点
+add-client-template =添加客户端模板
+
+manage =管理
+authentication =验证
+user-federation =用户联合
+user-storage =用户存储
+events =事件
+realm-settings =领域设置
+configure =配置
+select-realm =选择领域
+add =添加
+
+client-template.name.tooltip =客户端模板的名称。在领域中必须是唯一的
+client-template.description.tooltip =客户端模板的描述
+client-template.protocol.tooltip =此客户端模板提供的SSO协议配置
+
+add-user-federation-provider =添加用户联合提供程序
+add-user-storage-provider =添加用户存储提供程序
+required-settings =必需的设置
+provider-id =提供商ID
+console-display-name =控制台显示名称
+console-display-name.tooltip =在管理控制台中链接时显示提供程序的名称。
+priority =优先级
+priority.tooltip =执行用户查找时提供程序的优先级。最低优先。
+sync-settings =同步设置
+periodic-full-sync =周期性完全同步
+periodic-full-sync.tooltip =是否应该启用提供程序用户到Keycloak的周期性完全同步
+full-sync-period =完全同步周期
+full-sync-period.tooltip =完全同步的周期(以秒为单位)
+periodic-changed-users-sync =定期更改的用户同步
+periodic-changed-users-sync.tooltip =应该启用更改的或新创建的提供程序用户到Keycloak的周期性同步
+changed-users-sync-period =更改的用户同步期间
+changed-users-sync-period.tooltip =用于同步更改的或新创建的提供程序用户的时间段(以秒为单位)
+synchronize-changed-users =同步已更改的用户
+synchronize-all-users =同步所有用户
+kerberos-realm = Kerberos领域
+kerberos-realm.tooltip = kerberos域的名称。例如FOO.ORG
+server-principal =服务器主体
+server-principal.tooltip = HTTP服务的服务器主体的完整名称,包括服务器和域名。例如HTTP /host.foo.org@FOO.ORG
+keytab = KeyTab
+keytab.tooltip =包含服务器主体的凭据的Kerberos KeyTab文件的位置。例如/etc/krb5.keytab
+debug = Debug
+debug.tooltip =启用/禁用调试日志到Krb5LoginModule的标准输出。
+allow-password-authentication =允许密码验证
+allow-password-authentication.tooltip =启用/禁用Kerberos数据库的用户名/密码身份验证的可能性
+edit-mode =编辑模式
+edit-mode.tooltip = READ_ONLY表示不允许更新密码,用户始终使用Kerberos密码进行身份验证。 UNSYNCED表示用户可以在Keycloak数据库中更改其密码,然后将使用此密码而不是Kerberos密码
+ldap.edit-mode.tooltip = READ_ONLY是只读LDAP存储。可写意味着数据将按需同步回LDAP。 UNSYNCED表示将导入用户数据,但不会同步回LDAP。
+update-profile-first-login =更新配置文件首次登录
+update-profile-first-login.tooltip =首次登录时更新配置文件
+sync-registrations =同步注册
+ldap.sync-registrations.tooltip =是否应在LDAP存储中创建新创建的用户?选择提供程序以同步新用户的优先级效果。
+vendor =供应商
+ldap.vendor.tooltip = LDAP供应商(提供者)
+username-ldap-attribute =用户名LDAP属性
+ldap-attribute-name-for-username =用户名的LDAP属性名称
+username-ldap-attribute.tooltip = LDAP属性的名称,映射为Keycloak用户名。对于许多LDAP服务器供应商,它可以是“uid”。对于活动目录,可以是“sAMAccountName”或“cn”。应该为要从LDAP导入到Keycloak的所有LDAP用户记录填充该属性。
+rdn-ldap-attribute = RDN LDAP属性
+ldap-attribute-name-for-user-rdn =用户RDN的LDAP属性名称
+rdn-ldap-attribute.tooltip = LDAP属性的名称,用作典型用户DN的RDN(top属性)。通常它与用户名LDAP属性相同,但不是必需的。例如对于Active目录,当username属性可能是“sAMAccountName”时,通常使用“cn”作为RDN属性。
+uuid-ldap-attribute = UUID LDAP属性
+ldap-attribute-name-for-uuid = UUID的LDAP属性名称
+uuid-ldap-attribute.tooltip = LDAP属性的名称,用作LDAP中对象的唯一对象标识符(UUID)。对于许多LDAP服务器供应商,它的'entryUUID',但有些是不同的。例如对于Active目录,它应该是'objectGUID'。如果您的LDAP服务器确实不支持UUID的概念,您可以使用任何其他属性,它应该在树中的LDAP用户中是唯一的。例如“uid”或“entryDN”。
+user-object-classes =用户对象类
+ldap-user-object-classes.placeholder = LDAP用户对象类(以逗号分隔)
+
+ldap-connection-url = LDAP连接URL
+ldap-users-dn = LDAP用户DN
+ldap-bind-dn = LDAP绑定DN
+ldap-bind-credentials = LDAP绑定凭据
+ldap-filter = LDAP过滤器
+ldap.user-object-classes.tooltip = LDAP中用户的LDAP objectClass属性的所有值除以逗号。例如:'inetOrgPerson,organizationalPerson'。新创建的Keycloak用户将被写入具有所有这些对象类的LDAP,并且只要现有的LDAP用户记录包含所有这些对象类,就会找到它们。
+
+connection-url =连接URL
+ldap.connection-url.tooltip =与LDAP服务器的连接URL
+test-connection =测试连接
+users-dn =用户DN
+ldap.users-dn.tooltip =用户所在的LDAP树的完整DN。此DN是LDAP用户的父级。它可以是例如'ou = users,dc = example,dc = com',假设您的典型用户将有DN像'uid = john,ou = users,dc = example,dc = com'
+authentication-type =认证类型
+ldap.authentication-type.tooltip = LDAP认证类型。现在只有“无”(匿名LDAP身份验证)或“简单”(绑定凭据+绑定密码身份验证)机制可用
+bind-dn =绑定DN
+ldap.bind-dn.tooltip = LDAP管理员的DN,Keycloak将使用它来访问LDAP服务器
+bind-credential =绑定凭据
+ldap.bind-credential.tooltip = LDAP管理员的密码
+test-authentication =测试验证
+custom-user-ldap-filter =自定义用户LDAP过滤器
+ldap.custom-user-ldap-filter.tooltip =用于过滤搜索用户的其他LDAP过滤器。如果您不需要额外的过滤器,请留空。确保它以'('开头,以')结束'
+search-scope =搜索范围
+ldap.search-scope.tooltip =对于一个级别,我们仅在用户DN指定的DN中搜索用户。对于子树,我们搜索整个他们的子树。有关更多详细信息,请参阅LDAP文档
+use-truststore-spi =使用Truststore SPI
+ldap.use-truststore-spi.tooltip =指定LDAP连接是否将使用具有在standalone.xml / domain.xml中配置的信任库的truststore SPI。 “永远”意味着它总是使用它。 “从不”意味着它不会使用它。 '只有ldaps'意味着它将使用,如果你的连接URL使用ldaps。即使未配置standalone.xml / domain.xml,也将使用由“javax.net.ssl.trustStore”属性指定的缺省Java cacerts或证书。
+connection-pooling =连接池
+ldap.connection-pooling.tooltip = Keycloak是否应该使用连接池来访问LDAP服务器
+ldap.pagination.tooltip = LDAP服务器是否支持分页。
+kerberos-integration = Kerberos集成
+allow-kerberos-authentication =允许Kerberos身份验证
+ldap.allow-kerberos-authentication.tooltip =启用/禁用具有SPNEGO / Kerberos令牌的用户的HTTP身份验证。有关已验证用户的数据将从此LDAP服务器进行配置
+use-kerberos-for-password-authentication =使用Kerberos进行密码验证
+ldap.use-kerberos-for-password-authentication.tooltip =使用Kerberos登录模块用于针对Kerberos服务器的身份验证用户名/密码,而不是使用Directory Service API对LDAP服务器进行身份验证
+batch-size =批量大小
+ldap.batch-size.tooltip =在单个事务中要从LDAP导入到Keycloak的LDAP用户的计数。
+ldap.periodic-full-sync.tooltip =是否应该启用LDAP用户到Keycloak的周期性完全同步
+ldap.periodic-changed-users-sync.tooltip =应该启用更改的或新创建的LDAP用户到Keycloak的周期性同步
+ldap.changed-users-sync-period.tooltip =用于同步更改的或新创建的LDAP用户的时间段(以秒为单位)
+user-federation-mappers =用户联合映射器
+create-user-federation-mapper =创建用户联合映射器
+add-user-federation-mapper =添加用户联合映射器
+provider-name =提供程序名称
+no-user-federation-providers-configured =未配置用户联合提供程序
+no-user-storage-providers-configured =未配置用户存储提供程序
+add-identity-provider =添加身份提供者
+add-identity-provider-link =添加身份提供商链接
+identity-provider =身份提供者
+identity-provider-user-id =身份提供者用户ID
+identity-provider-user-id.tooltip =身份提供者端的用户的唯一ID
+identity-provider-username =身份提供者用户名
+identity-provider-username.tooltip =身份提供者端的用户名
+pagination =分页
+
+browser-flow =浏览器流
+browser-flow.tooltip =选择要用于浏览器身份验证的流。
+registration-flow =注册流程
+registration-flow.tooltip =选择要用于注册的流。
+direct-grant-flow =直接授权流
+direct-grant-flow.tooltip =选择要用于直接授予身份验证的流。
+reset-credentials =重置凭据
+reset-credentials.tooltip =选择当用户忘记其凭据时要使用的流。
+client-authentication =客户端验证
+client-authentication.tooltip =选择要用于客户端身份验证的流。
+new =新
+copy =复制
+add-execution =添加执行
+add-flow =添加流
+auth-type =认证类型
+requirement=需求
+config = Config
+no-executions-available =没有可用的执行
+authentication-flows =认证流
+create-authenticator-config =创建验证器配置
+authenticator.alias.tooltip =配置的名称
+otp-type = OTP类型
+time-based=基于时间的
+counter-based=基于计数器
+otp-type.tooltip = totp是基于时间的一次性密码。 'hotp'是一个计数器基本一次性密码,其中服务器保持一个计数器哈希。
+otp-hash-algorithm = OTP哈希算法
+otp-hash-algorithm.tooltip =应该使用什么散列算法来生成OTP。
+number-of-digits=位数
+otp.number-of-digits.tooltip = OTP有多少位?
+look-ahead-window =向前看窗口
+otp.look-ahead-window.tooltip =如果令牌生成器和服务器不在时间同步或计数器同步,服务器应该向多远前进?
+initial-counter=初始计数器
+otp.initial-counter.tooltip =初始计数器值应该是什么?
+otp-token-period = OTP令牌周期
+otp-token-period.tooltip = OTP令牌有效多少秒?默认为30秒。
+table-of-password-policies =密码策略表
+add-policy.placeholder =添加策略...
+policy-type =策略类型
+policy-value =策略值
+admin-events =管理事件
+admin-events.tooltip =显示领域的已保存管理事件。事件与管理员帐户相关,例如领域创建。要启用持久性事件,请转到配置。
+login-events =登录事件
+filter =过滤器
+update =更新
+reset =复位
+operation-types =操作类型
+resource-types =资源类型
+select-operations.placeholder =选择操作...
+select-resource-types.placeholder =选择资源类型...
+resource-path =资源路径
+resource-path.tooltip =按资源路径过滤。支持通配符'*'匹配路径的单个部分,'**'匹配多个部分。例如,“realms / * / clients / asbc”匹配任何域中具有id asbc的客户端,而“realms / master / **”匹配主域中的任何内容。
+date-(from)= Date(From)
+date-(to)=日期(To)
+authentication-details =验证详细信息
+ip-address = IP地址
+time=时间
+operation-type =操作类型
+resource-type =资源类型
+auth = Auth
+representation =表示
+register=寄存器
+required-action =必需操作
+default-action =默认操作
+auth.default-action.tooltip =如果启用,任何新用户都将分配此必需操作。
+no-required-actions-configured =未配置所需的操作
+defaults-to-id =默认为id
+flows=流量
+bindings =绑定
+required-actions =必需操作
+password-policy =密码策略
+otp-policy = OTP策略
+user-groups =用户组
+default-groups =默认组
+groups.default-groups.tooltip =新用户将自动加入的组的集合。
+cut = Cut
+paste =粘贴
+
+create-group =创建组
+create-authenticator-execution =创建Authenticator执行
+create-form-action-execution =创建表单操作执行
+create-top-level-form =创建顶级表单
+flow.alias.tooltip =指定流的显示名称。
+top-level-flow-type =顶级流类型
+flow.generic = generic
+flow.client = client
+top-level-flow-type.tooltip =什么样的顶层流是什么?类型“客户端”用于客户端(应用程序)的认证,当通用是为用户和其他
+create-execution-flow =创建执行流
+flow-type =流类型
+flow.form.type = form
+flow.generic.type = generic
+flow-type.tooltip =它是什么样的形式
+form-provider =表单提供程序
+default-groups.tooltip =新创建或注册的用户将自动添加到这些组
+select-a-type.placeholder =选择一个类型
+available-groups =可用组
+available-groups.tooltip =选择要添加为默认的组。
+value = Value
+table-of-group-members =组成员表
+last-name =姓氏
+first-name =名字
+email =电子邮件
+toggle-navigation =切换导航
+manage-account =管理帐户
+sign-out=退出
+server-info =服务器信息
+resource-not-found =资源<strong>未找到</ strong> ...
+resource-not-found.instruction =我们找不到您要找的资源。请确保您输入的网址正确无误。
+go-to-the-home-page =前往首页&raquo;
+page-not-found =页面<strong>未找到</ strong> ...
+page-not-found.instruction =我们找不到您要寻找的页面。请确保您输入的网址正确无误。
+events.tooltip =显示领域的已保存事件。事件与用户帐户相关,例如用户登录。要启用持久性事件,请转到配置。
+select-event-types.placeholder =选择事件类型...
+events-config.tooltip =显示配置选项以启用用户和管理事件的持久性。
+select-an-action.placeholder =选择操作...
+event-listeners.tooltip =配置什么侦听器接收领域的事件。
+login.save-events.tooltip =如果启用的登录事件保存到数据库,使事件可用于管理和帐户管理控制台。
+clear-events.tooltip =删除数据库中的所有事件。
+events.expiration.tooltip =设置事件的到期时间。过期事件将定期从数据库中删除。
+admin-events-settings =管理事件设置
+save-events =保存事件
+admin.save-events.tooltip =如果已启用的管理事件保存到数据库,使事件可用于管理控制台。
+saved-types.tooltip =配置保存的事件类型。
+include-representation =包含表示
+include-representation.tooltip =包含用于创建和更新请求的JSON表示。
+clear-admin-events.tooltip =删除数据库中的所有管理事件。
+server-version =服务器版本
+server-profile =服务器配置文件
+info =信息
+providers =提供者
+server-time =服务器时间
+server-uptime =服务器正常运行时间
+memory =内存
+total-memory =总内存
+free-memory =可用内存
+used-memory =使用的内存
+system = System
+current-working-directory =当前工作目录
+java-version = Java版本
+java-vendor = Java供应商
+java-runtime = Java运行时
+java-vm = Java VM
+java-vm-version = Java虚拟机版本
+java-home = Java首页
+user-name =用户名
+user-timezone =用户时区
+user-locale =用户区域设置
+system-encoding =系统编码
+operating-system =操作系统
+os-architecture = OS体系结构
+spi = SPI
+granted-roles =授予的角色
+granted-protocol-mappers =授予的协议映射器
+additional-grants =附加赠款
+consent-created-date =创建
+consent-last-updated-date =最后更新
+revoke =撤消
+new-password =新密码
+password-confirmation =密码确认
+reset-password =重置密码
+credentials.temporary.tooltip =如果启用,用户需要在下次登录时更改密码
+remove-totp =删除TOTP
+credentials.remove-totp.tooltip =为用户删除一次性密码生成器。
+reset-actions =复位操作
+credentials.reset-actions.tooltip =发送用户重置操作电子邮件时要执行的操作的集合。 “验证电子邮件”向用户发送电子邮件以验证其电子邮件地址。 “更新个人资料”要求用户输入新的个人信息。 “更新密码”要求用户输入新密码。 '配置TOTP'需要设置移动密码生成器。
+reset-actions-email =重置操作电子邮件
+send-email =发送电子邮件
+credentials.reset-actions-email.tooltip =向具有嵌入链接的用户发送电子邮件。单击链接将允许用户执行重置操作。他们不必在此之前登录。例如,将操作设置为更新密码,单击此按钮,用户将无需登录即可更改其密码。
+add-user =添加用户
+created-at =创建于
+user-enabled =用户已启用
+user-enabled.tooltip =禁用的用户无法登录。
+user-temporarily-locked =用户临时锁定
+user-temporarily-locked.tooltip =用户可能由于无法登录太多次而被锁定。
+unlock-user =解锁用户
+federation-link =联合链接
+email-verified =电子邮件验证
+email-verified.tooltip =用户的电子邮件经过验证吗?
+required-user-actions =必需的用户操作
+required-user-actions.tooltip =需要用户登录时的操作。“验证电子邮件”向用户发送电子邮件以验证其电子邮件地址。 “更新个人资料”要求用户输入新的个人信息。 “更新密码”要求用户输入新密码。 '配置TOTP'需要设置移动密码生成器。
+locale =语言环境
+select-one.placeholder =选择一个...
+impersonate =模拟
+impersonate-user =模拟用户
+impersonate-user.tooltip =以此用户身份登录。如果用户与您处于相同的领域,则在您以此用户身份登录之前,当前的登录会话将被注销。
+identity-provider-alias =身份提供者别名
+provider-user-id =提供程序用户ID
+provider-username =提供者用户名
+no-identity-provider-links-available =没有可用的身份提供程序链接
+group-membership =组成员资格
+leave =离开
+group-membership.tooltip =组用户是的成员。选择列出的组,然后单击离开按钮退出组。
+membership.available-groups.tooltip =用户可以加入的组。选择一个组,然后单击加入按钮。
+table-of-realm-users =表的Realm用户
+view-all-users =查看所有用户
+unlock-users =解锁用户
+no-users-available =没有可用的用户
+users.instruction =请输入搜索,或点击查看所有用户
+consents=同意
+started =开始
+logout-all-sessions =注销所有会话
+logout =注销
+new-name =新名称
+ok =好的
+attributes =属性
+role-mappings =角色映射
+members =成员
+details =详细
+identity-provider-links =身份提供者链接
+register-required-action =注册所需的操作
+gender =性别
+address = Address
+phone =电话
+profile-url =个人资料网址
+picture-url =图片网址
+website =网站
+import-keys-and-cert =导入密钥和证书
+import-keys-and-cert.tooltip =上传客户端的密钥对和证书。
+upload-keys =上传密钥
+download-keys-and-cert =下载密钥和证书
+no-value-assigned.placeholder =未分配值
+remove =删除
+no-group-members =没有组成员
+temporary =临时
+join =加入
+event-type =事件类型
+events-config =事件配置
+event-listeners =事件监听器
+login-events-settings =登录事件设置
+clear-events =清除事件
+saved-types =保存的类型
+clear-admin-events =清除管理事件
+clear-changes =清除更改
+error =错误
+
+# Authz
+# Authz Common
+authz-authorization =授权
+authz-owner =所有者
+authz-uri = URI
+authz-scopes =范围
+authz-resource =资源
+authz-resource-type =资源类型
+authz-resources =资源
+authz-scope =范围
+authz-authz-scopes =授权范围
+authz-policies =策略
+authz-permissions =权限
+authz-evaluate =评估
+authz-icon-uri =图标URI
+authz-icon-uri.tooltip =指向图标的URI。
+authz-select-scope =选择范围
+authz-select-resource =选择资源
+authz-associated-policies =关联策略
+authz-any-resource =任何资源
+authz-any-scope =任何作用域
+authz-any-role =任何角色
+authz-policy-evaluation =政策评估
+authz-select-client =选择客户端
+authz-select-user =选择用户
+authz-entitlements =权利
+authz-no-resources =无资源
+authz-result = Result
+authz-authorization-services-enabled =授权已启用
+authz-authorization-services-enabled.tooltip =启用/禁用客户端的细粒度授权支持
+authz-required =必需
+
+# Authz Settings
+authz-import-config.tooltip =导入包含此资源服务器的授权设置的JSON文件。
+
+authz-policy-enforcement-mode =策略强制模式
+authz-policy-enforcement-mode.tooltip =策略强制模式指示在评估授权请求时如何强制执行策略。 “Enforcing”表示即使没有与给定资源相关联的策略,也会默认拒绝请求。 “Permissive”表示即使没有与给定资源相关联的策略也允许请求。 “禁用”完全禁用策略的评估,并允许访问任何资源。
+authz-policy-enforcement-mode-enforcing =强制
+authz-policy-enforcement-mode-permissive = Permissive
+authz-policy-enforcement-mode-disabled =禁用
+
+authz-remote-resource-management =远程资源管理
+authz-remote-resource-management.tooltip =资源服务器是否应该远程管理资源?如果为false,则只能从此管理控制台管理资源。
+
+authz-export-settings =导出设置
+authz-export-settings.tooltip =导出并下载此资源服务器的所有授权设置。
+
+# Authz Resource List
+authz-no-resources-available =无可用资源。
+authz-no-scopes-assigned =未分配范围。
+authz-no-type-defined =未定义类型。
+authz-no-permission-assigned =未分配权限。
+authz-no-policy-assigned =未分配策略。
+authz-create-permission =创建权限
+
+# Authz Resource Detail
+authz-add-resource =添加资源
+authz-resource-name.tooltip =此资源的唯一名称。 该名称可用于唯一标识资源,在查询特定资源时很有用。
+authz-resource-owner.tooltip =此资源的所有者。
+authz-resource-type.tooltip =此资源的类型。 它可以用于对具有相同类型的不同资源实例进行分组。
+authz-resource-uri.tooltip =也可以用于唯一标识此资源的URI。
+authz-resource-scopes.tooltip =与此资源关联的范围。
+
+# Authz Scope List
+authz-add-scope=Add Scope
+authz-no-scopes-available=No scopes available.
+
+#Authz作用域详细信息
+authz-scope-name.tooltip =此作用域的唯一名称。该名称可用于唯一标识范围,在查询特定范围时很有用。
+
+#Authz策略列表
+authz-all-types =所有类型
+authz-create-policy =创建策略
+authz-no-policies-available =没有可用的策略。
+
+#Authz策略详细信息
+authz-policy-name.tooltip =此策略的名称。
+authz-policy-description.tooltip =此策略的描述。
+authz-policy-logic =逻辑
+authz-policy-logic-positive =肯定
+authz-policy-logic-negative = Negative
+authz-policy-logic.tooltip =逻辑决定如何进行策略决策。如果为“积极”,则在评估本政策期间获得的效果(许可或拒绝)将用于执行决策。如果为“否定”,则所得的效果将被否定,换句话说,许可证变为拒绝,反之亦然。
+authz-policy-apply-policy =应用策略
+authz-policy-apply-policy.tooltip =指定必须应用于此策略或权限定义的范围的所有策略。
+authz-policy-decision-strategy =决策策略
+authz-policy-decision-strategy.tooltip =决策策略规定如何评估与给定权限相关联的策略以及如何获得最终决策。 “肯定”意味着至少一个政策必须评估为积极的决定,以使最终决定也是积极的。 “一致”是指所有政策必须评估为一个积极的决定,以使最终决定也是积极的。 “共识”意味着积极决策的数量必须大于负面决策的数量。如果正数和负数相同,最终决定将为负数。
+authz-policy-decision-strategy-affirmative =肯定
+authz-policy-decision-strategy-unanimous =一致
+authz-policy-decision-strategy-consensus=共识
+authz-select-a-policy =选择一个策略
+
+#Authz角色策略详细信息
+authz-add-role-policy =添加角色策略
+authz-no-roles-assigned =未分配角色。
+authz-policy-role-realm-roles.tooltip =指定此策略允许的* realm *角色。
+authz-policy-role-clients.tooltip =选择客户端以过滤可应用于此策略的客户端角色。
+authz-policy-role-client-roles.tooltip =指定此策略允许的客户端角色。
+
+#Authz用户策略详细信息
+authz-add-user-policy =添加用户策略
+authz-no-users-assigned =未分配用户。
+authz-policy-user-users.tooltip =指定此策略允许哪些用户。
+
+#Authz时间策略详细信息
+authz-add-time-policy =添加时间策略
+authz-policy-time-not-before.tooltip =定义不得授予策略的时间。仅当当前日期/时间晚于或等于此值时才被授予。
+authz-policy-time-not-on-after =不开或之后
+authz-policy-time-not-on-after.tooltip =定义不能授予策略的时间。仅当当前日期/时间在此值之前或之前时才被授予。
+authz-policy-time-day-month =日期
+authz-policy-time-day-month.tooltip =定义必须授予策略的月份日期。您还可以通过填充第二个字段来提供范围。在这种情况下,只有当月的当天介于或等于您提供的两个值之后,才会授予权限。
+authz-policy-time-month = month
+authz-policy-time-month.tooltip =定义必须授予策略的月份。您还可以通过填充第二个字段来提供范围。在这种情况下,仅当当前月份介于或等于您提供的两个值之间时才会授予权限。
+authz-policy-time-year =年
+authz-policy-time-year.tooltip =定义策略必须授予的年份。您还可以通过填充第二个字段来提供范围。在这种情况下,仅当当前年份介于或等于您提供的两个值之间时才会授予权限。
+authz-policy-time-hour =小时
+authz-policy-time-hour.tooltip =定义策略必须被授予的小时。您还可以通过填充第二个字段来提供范围。在这种情况下,只有当前小时介于或等于您提供的两个值之间时才会授予权限。
+authz-policy-time-minute =分钟
+authz-policy-time-minute.tooltip =定义策略必须被授予的分钟。您还可以通过填充第二个字段来提供范围。在这种情况下,仅当当前分钟介于或等于您提供的两个值之间时才会授予权限。
+
+#Authz Drools策略详细信息
+authz-add-drools-policy =添加Drools策略
+authz-policy-drools-maven-artifact-resolve =解决
+authz-policy-drools-maven-artifact =策略Maven神器
+authz-policy-drools-maven-artifact.tooltip =指向从其中加载规则的工件的Maven GAV。一旦您提供了GAV,您可以点击* Resolve *来加载* Module *和* Session *字段。
+authz-policy-drools-module = Module
+authz-policy-drools-module.tooltip =此策略使用的模块。您必须提供一个模块,以便选择将从中加载规则的特定会话。
+authz-policy-drools-session =会话
+authz-policy-drools-session.tooltip =此策略使用的会话。会话提供处理策略时评估的所有规则。
+authz-policy-drools-update-period =更新周期
+authz-policy-drools-update-period.tooltip =指定扫描工件更新的时间间隔。
+
+#Authz JS策略详细信息
+authz-add-js-policy =添加JavaScript策略
+authz-policy-js-code =代码
+authz-policy-js-code.tooltip =提供此策略条件的JavaScript代码。
+
+
+#Authz聚合策略详细信息
+authz-aggregated=聚合
+authz-add-aggregation-policy =添加聚合策略
+
+#Authz权限列表
+authz-no-permissions-available =没有可用的权限。
+
+#Authz权限详细信息
+authz-permission-name.tooltip =此权限的名称。
+authz-permission-description.tooltip =此权限的描述。
+
+#Authz资源许可详细信息
+authz-add-resource-permission =添加资源权限
+authz-permission-resource-apply-to-resource-type =应用于资源类型
+authz-permission-resource-apply-to-resource-type.tooltip =指定是否将此权限应用于具有给定类型的所有资源。 在这种情况下,将对给定资源类型的所有实例评估此权限。
+authz-permission-resource-resource.tooltip =指定此权限必须应用于特定资源实例。
+authz-permission-resource-type.tooltip =指定此权限必须应用于给定类型的所有资源实例。
+
+#Authz Scope Permission Detail
+authz-add-scope-permission =添加范围权限
+authz-permission-scope-resource.tooltip =将范围限制为与所选资源关联的范围。 如果未选择,则所有范围都可用。
+authz-permission-scope-scope.tooltip =指定此权限必须应用于一个或多个作用域。
+
+# Authz Evaluation
+authz-evaluation-identity-information =身份信息
+authz-evaluation-identity-information.tooltip =用于配置在评估策略时将使用的身份信息的可用选项。
+authz-evaluation-client.tooltip =选择进行此授权请求的客户端。如果未提供,授权请求将根据您所在的客户端完成。
+authz-evaluation-user.tooltip =选择一个用户,其身份将被用于查询服务器的权限。
+authz-evaluation-role.tooltip =选择要与所选用户关联的角色。
+authz-evaluation-new =新评估
+authz-evaluation-re-evaluate =重新评估
+authz-evaluation-previous =以前的评估
+authz-evaluation-contextual-info =上下文信息
+authz-evaluation-contextual-info.tooltip =用于配置在评估策略时将使用的任何上下文信息的可用选项。
+authz-evaluation-contextual-attributes =上下文属性
+authz-evaluation-contextual-attributes.tooltip =由正在运行的环境或执行上下文提供的任何属性。
+authz-evaluation-permissions.tooltip =用于配置将应用策略的权限的可用选项。
+authz-evaluation-evaluate =评估
+authz-evaluation-any-resource-with-scopes =任何具有范围的资源
+authz-evaluation-no-result =无法获取给定授权请求的任何结果。检查所提供的资源或范围是否与任何策略相关联。
+authz-evaluation-no-policies-resource =未找到此资源的策略。
+authz-evaluation-result.tooltip =此权限请求的总体结果。
+authz-evaluation-scopes.tooltip =允许的作用域列表。
+authz-evaluation-policies.tooltip =有关评估哪些策略及其决策的详细信息。
+authz-evaluation-authorization-data =响应
+authz-evaluation-authorization-data.tooltip =表示由于处理授权请求而携带授权数据的令牌。这个表示基本上是Keycloak向客户端请求权限的问题。针对根据当前授权请求授予的权限,检查“授权”声明。
+authz-show-authorization-data =显示授权数据
+
+keys=秘钥
+all=所有
+status=状态
+keystore=钥匙链
+keystores= 钥匙链
+add-keystore=添加 钥匙链
+add-keystore.placeholder=添加 钥匙链...
+view=查看
+active=活跃
+
+Sunday=星期天
+Monday=星期一
+Tuesday=星期二
+Wednesday=星期三
+Thursday=星期四
+Friday=星期五
+Saturday=星期六
+
+user-storage-cache-policy=缓存设置
+userStorage.cachePolicy=缓存策略
+userStorage.cachePolicy.option.DEFAULT=默认
+userStorage.cachePolicy.option.EVICT_WEEKLY=EVICT_WEEKLY
+userStorage.cachePolicy.option.EVICT_DAILY=EVICT_DAILY
+userStorage.cachePolicy.option.MAX_LIFESPAN=MAX_LIFESPAN
+userStorage.cachePolicy.option.NO_CACHE=NO_CACHE
+userStorage.cachePolicy.tooltip=这个存储源的缓存策略. '默认' 是全局的默认缓存策略。'EVICT_DAILY'是每天特定时间缓存会失效. 'EVICT_WEEKLY'是每周第n天的特定时间缓存会失效. 'MAX-LIFESPAN' 是指缓存条目的最大生命周期
+userStorage.cachePolicy.evictionDay=Eviction Day
+userStorage.cachePolicy.evictionDay.tooltip=每周第n天缓存失效
+userStorage.cachePolicy.evictionHour=Eviction Hour
+userStorage.cachePolicy.evictionHour.tooltip=一天中几点缓存失效
+userStorage.cachePolicy.evictionMinute=Eviction Minute
+userStorage.cachePolicy.evictionMinute.tooltip=缓存失效的分钟
+userStorage.cachePolicy.maxLifespan=最大生命周期
+userStorage.cachePolicy.maxLifespan.tooltip=以微秒计数的最大生命周期
+user-origin-link=存储源
+
+disable=关闭
+disableable-credential-types=可以关闭的类型
+credentials.disableable.tooltip=可以关闭的密码类型列表
+disable-credential-types=关闭密码类型
+credentials.disable.tooltip=点击按钮关闭密码类型
+credential-types=密码类型
+manage-user-password=管理密码
+disable-credentials=关闭密码
+credential-reset-actions=重置密码
+ldap-mappers=LDAP 映射器
+create-ldap-mapper=创建 LDAP 映射
+loginWithEmailAllowed=使用电子邮件登录
+duplicateEmailsAllowed=重复的邮件
+hidden=隐藏
+
+
+
+
+
+
diff --git a/themes/src/main/resources/theme/base/admin/messages/messages_zh_CN.properties b/themes/src/main/resources/theme/base/admin/messages/messages_zh_CN.properties
new file mode 100644
index 0000000..a04cc03
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/messages/messages_zh_CN.properties
@@ -0,0 +1,25 @@
+invalidPasswordMinLengthMessage=无效的密码:最短长度 {0}.
+invalidPasswordMinLowerCaseCharsMessage=无效的密码:至少包含 {0} 小写字母
+invalidPasswordMinDigitsMessage=无效的密码:至少包含 {0} 个数字
+invalidPasswordMinUpperCaseCharsMessage=无效的密码:最短长度 {0} 大写字母
+invalidPasswordMinSpecialCharsMessage=无效的密码:最短长度 {0} 特殊字符
+invalidPasswordNotUsernameMessage=无效的密码: 不可以与用户名相同
+invalidPasswordRegexPatternMessage=无效的密码: 无法与正则表达式匹配
+invalidPasswordHistoryMessage=无效的密码:不能与最后使用的 {0} 个密码相同
+
+ldapErrorInvalidCustomFilter=定制的 LDAP过滤器不是以 "(" 开头或以 ")"结尾.
+ldapErrorConnectionTimeoutNotNumber=Connection Timeout 必须是个数字
+ldapErrorMissingClientId=当域角色映射未启用时,客户端 ID 需要指定。
+ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType=无法在使用UID成员类型的同时维护组继承属性。
+ldapErrorCantWriteOnlyForReadOnlyLdap=当LDAP提供方不是可写模式时,无法设置只写
+ldapErrorCantWriteOnlyAndReadOnly=无法同时设置只读和只写
+
+clientRedirectURIsFragmentError=重定向URL不应包含URI片段
+clientRootURLFragmentError=根URL 不应包含 URL 片段
+
+pairwiseMalformedClientRedirectURI=客户端包含一个无效的重定向URL
+pairwiseClientRedirectURIsMissingHost=客户端重定向URL需要有一个有效的主机
+pairwiseClientRedirectURIsMultipleHosts=Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.
+pairwiseMalformedSectorIdentifierURI=Malformed Sector Identifier URI.
+pairwiseFailedToGetRedirectURIs=无法从服务器获得重定向URL
+pairwiseRedirectURIsMismatch=客户端的重定向URI与服务器端获取的URI配置不匹配。
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js
index c4e870f..c650d00 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -125,6 +125,12 @@ module.config(['$translateProvider', function($translateProvider) {
$translateProvider.translations(locale, resourceBundle);
}]);
+// Change for upgrade to AngularJS 1.6
+// See https://github.com/angular/angular.js/commit/aa077e81129c740041438688dff2e8d20c3d7b52
+module.config(['$locationProvider', function($locationProvider) {
+ $locationProvider.hashPrefix('');
+}]);
+
module.config([ '$routeProvider', function($routeProvider) {
$routeProvider
.when('/create/realm', {
@@ -1709,8 +1715,8 @@ module.config([ '$routeProvider', function($routeProvider) {
flows : function(AuthenticationFlowsLoader) {
return AuthenticationFlowsLoader();
},
- serverInfo : function(ServerInfo) {
- return ServerInfo.delay;
+ serverInfo : function(ServerInfoLoader) {
+ return ServerInfoLoader();
}
},
controller : 'RealmFlowBindingCtrl'
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
index 034d595..14c9392 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
@@ -79,7 +79,72 @@ module.controller('ResourceServerDetailCtrl', function($scope, $http, $route, $l
});
});
-module.controller('ResourceServerResourceCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerResource, client) {
+var Resources = {
+ delete: function(ResourceServerResource, realm, client, $scope, AuthzDialog, $location, Notifications, $route) {
+ ResourceServerResource.permissions({
+ realm : realm,
+ client : client.id,
+ rsrid : $scope.resource._id
+ }, function (permissions) {
+ var msg = "";
+
+ if (permissions.length > 0 && !$scope.deleteConsent) {
+ msg = "<p>This resource is referenced in some permissions:</p>";
+ msg += "<ul>";
+ for (i = 0; i < permissions.length; i++) {
+ msg+= "<li><strong>" + permissions[i].name + "</strong></li>";
+ }
+ msg += "</ul>";
+ msg += "<p>If you remove this resource, the permissions above will be affected and will not be associated with this resource anymore.</p>";
+ }
+
+ AuthzDialog.confirmDeleteWithMsg($scope.resource.name, "Resource", msg, function() {
+ ResourceServerResource.delete({realm : realm, client : $scope.client.id, rsrid : $scope.resource._id}, null, function() {
+ $location.url("/realms/" + realm + "/clients/" + $scope.client.id + "/authz/resource-server/resource");
+ $route.reload();
+ Notifications.success("The resource has been deleted.");
+ });
+ });
+ });
+ }
+}
+
+var Policies = {
+ delete: function(service, realm, client, $scope, AuthzDialog, $location, Notifications, $route, isPermission) {
+ var msg = "";
+
+ service.dependentPolicies({
+ realm : realm,
+ client : client.id,
+ id : $scope.policy.id
+ }, function (dependentPolicies) {
+ if (dependentPolicies.length > 0 && !$scope.deleteConsent) {
+ msg = "<p>This policy is being used by other policies:</p>";
+ msg += "<ul>";
+ for (i = 0; i < dependentPolicies.length; i++) {
+ msg+= "<li><strong>" + dependentPolicies[i].name + "</strong></li>";
+ }
+ msg += "</ul>";
+ msg += "<p>If you remove this policy, the policies above will be affected and will not be associated with this policy anymore.</p>";
+ }
+
+ AuthzDialog.confirmDeleteWithMsg($scope.policy.name, isPermission ? "Permission" : "Policy", msg, function() {
+ service.delete({realm : realm, client : $scope.client.id, id : $scope.policy.id}, null, function() {
+ if (isPermission) {
+ $location.url("/realms/" + realm + "/clients/" + $scope.client.id + "/authz/resource-server/permission");
+ Notifications.success("The permission has been deleted.");
+ } else {
+ $location.url("/realms/" + realm + "/clients/" + $scope.client.id + "/authz/resource-server/policy");
+ Notifications.success("The policy has been deleted.");
+ }
+ $route.reload();
+ });
+ });
+ });
+ }
+}
+
+module.controller('ResourceServerResourceCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerResource, client, AuthzDialog, Notifications) {
$scope.realm = realm;
$scope.client = client;
@@ -171,6 +236,11 @@ module.controller('ResourceServerResourceCtrl', function($scope, $http, $route,
}
}
};
+
+ $scope.delete = function(resource) {
+ $scope.resource = resource;
+ Resources.delete(ResourceServerResource, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route);
+ };
});
module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $route, $location, realm, ResourceServer, client, ResourceServerResource, ResourceServerScope, AuthzDialog, Notifications) {
@@ -282,30 +352,7 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
}
$scope.remove = function() {
- ResourceServerResource.permissions({
- realm : $route.current.params.realm,
- client : client.id,
- rsrid : $scope.resource._id
- }, function (permissions) {
- var msg = "";
-
- if (permissions.length > 0 && !$scope.deleteConsent) {
- msg = "<p>This resource is referenced in some policies:</p>";
- msg += "<ul>";
- for (i = 0; i < permissions.length; i++) {
- msg+= "<li><strong>" + permissions[i].name + "</strong></li>";
- }
- msg += "</ul>";
- msg += "<p>If you remove this resource, the policies above will be affected and will not be associated with this resource anymore.</p>";
- }
-
- AuthzDialog.confirmDeleteWithMsg($scope.resource.name, "Resource", msg, function() {
- ResourceServerResource.delete({realm : realm.realm, client : $scope.client.id, rsrid : $scope.resource._id}, null, function() {
- $location.url("/realms/" + realm.realm + "/clients/" + $scope.client.id + "/authz/resource-server/resource");
- Notifications.success("The resource has been deleted.");
- });
- });
- });
+ Resources.delete(ResourceServerResource, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route);
}
$scope.reset = function() {
@@ -338,7 +385,37 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
}
});
-module.controller('ResourceServerScopeCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerScope, client) {
+var Scopes = {
+ delete: function(ResourceServerScope, realm, client, $scope, AuthzDialog, $location, Notifications, $route) {
+ ResourceServerScope.permissions({
+ realm : realm,
+ client : client.id,
+ id : $scope.scope.id
+ }, function (permissions) {
+ var msg = "";
+
+ if (permissions.length > 0 && !$scope.deleteConsent) {
+ msg = "<p>This scope is referenced in some permissions:</p>";
+ msg += "<ul>";
+ for (i = 0; i < permissions.length; i++) {
+ msg+= "<li><strong>" + permissions[i].name + "</strong></li>";
+ }
+ msg += "</ul>";
+ msg += "<p>If you remove this scope, the permissions above will be affected and will not be associated with this scope anymore.</p>";
+ }
+
+ AuthzDialog.confirmDeleteWithMsg($scope.scope.name, "Scope", msg, function() {
+ ResourceServerScope.delete({realm : realm, client : $scope.client.id, id : $scope.scope.id}, null, function() {
+ $location.url("/realms/" + realm + "/clients/" + $scope.client.id + "/authz/resource-server/scope");
+ $route.reload();
+ Notifications.success("The scope has been deleted.");
+ });
+ });
+ });
+ }
+}
+
+module.controller('ResourceServerScopeCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerScope,client, AuthzDialog, Notifications) {
$scope.realm = realm;
$scope.client = client;
@@ -430,6 +507,11 @@ module.controller('ResourceServerScopeCtrl', function($scope, $http, $route, $lo
}
}
};
+
+ $scope.delete = function(scope) {
+ $scope.scope = scope;
+ Scopes.delete(ResourceServerScope, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route);
+ };
});
module.controller('ResourceServerScopeDetailCtrl', function($scope, $http, $route, $location, realm, ResourceServer, client, ResourceServerScope, AuthzDialog, Notifications) {
@@ -499,30 +581,7 @@ module.controller('ResourceServerScopeDetailCtrl', function($scope, $http, $rout
}
$scope.remove = function() {
- ResourceServerScope.permissions({
- realm : $route.current.params.realm,
- client : client.id,
- id : $scope.scope.id
- }, function (permissions) {
- var msg = "";
-
- if (permissions.length > 0 && !$scope.deleteConsent) {
- msg = "<p>This scope is referenced in some policies:</p>";
- msg += "<ul>";
- for (i = 0; i < permissions.length; i++) {
- msg+= "<li><strong>" + permissions[i].name + "</strong></li>";
- }
- msg += "</ul>";
- msg += "<p>If you remove this scope, the policies above will be affected and will not be associated with this scope anymore.</p>";
- }
-
- AuthzDialog.confirmDeleteWithMsg($scope.scope.name, "Scope", msg, function() {
- ResourceServerScope.delete({realm : realm.realm, client : $scope.client.id, id : $scope.scope.id}, null, function() {
- $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/scope");
- Notifications.success("The scope has been deleted.");
- });
- });
- });
+ Scopes.delete(ResourceServerScope, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route);
}
$scope.reset = function() {
@@ -554,7 +613,7 @@ module.controller('ResourceServerScopeDetailCtrl', function($scope, $http, $rout
}
});
-module.controller('ResourceServerPolicyCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerPolicy, PolicyProvider, client) {
+module.controller('ResourceServerPolicyCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerPolicy, PolicyProvider, client, AuthzDialog, Notifications) {
$scope.realm = realm;
$scope.client = client;
$scope.policyProviders = [];
@@ -650,9 +709,14 @@ module.controller('ResourceServerPolicyCtrl', function($scope, $http, $route, $l
}
}
};
+
+ $scope.delete = function(policy) {
+ $scope.policy = policy;
+ Policies.delete(ResourceServerPolicy, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route, false);
+ };
});
-module.controller('ResourceServerPermissionCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerPermission, PolicyProvider, client) {
+module.controller('ResourceServerPermissionCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerPermission, PolicyProvider, client, AuthzDialog, Notifications) {
$scope.realm = realm;
$scope.client = client;
$scope.policyProviders = [];
@@ -747,6 +811,11 @@ module.controller('ResourceServerPermissionCtrl', function($scope, $http, $route
}
}
};
+
+ $scope.delete = function(policy) {
+ $scope.policy = policy;
+ Policies.delete(ResourceServerPermission, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route, true);
+ };
});
module.controller('ResourceServerPolicyDroolsDetailCtrl', function($scope, $http, $route, realm, client, PolicyController) {
@@ -766,8 +835,8 @@ module.controller('ResourceServerPolicyDroolsDetailCtrl', function($scope, $http
delete policy.config;
$http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/rules/provider/resolveModules'
- , policy).success(function(data) {
- $scope.drools.moduleNames = data;
+ , policy).then(function(response) {
+ $scope.drools.moduleNames = response.data;
$scope.resolveSessions();
});
}
@@ -776,8 +845,8 @@ module.controller('ResourceServerPolicyDroolsDetailCtrl', function($scope, $http
delete $scope.policy.config;
$http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/rules/provider/resolveSessions'
- , $scope.policy).success(function(data) {
- $scope.drools.moduleSessions = data;
+ , $scope.policy).then(function(response) {
+ $scope.drools.moduleSessions = response.data;
});
}
},
@@ -1137,27 +1206,28 @@ module.controller('ResourceServerPolicyScopeDetailCtrl', function($scope, $route
rsrid: resource[0]._id
}, function (scopes) {
$scope.resourceScopes = scopes;
- ResourceServerPolicy.scopes({
- realm : $route.current.params.realm,
- client : client.id,
- id : policy.id
- }, function(scopes) {
- $scope.selectedScopes = [];
- for (i = 0; i < scopes.length; i++) {
- scopes[i].text = scopes[i].name;
- $scope.selectedScopes.push(scopes[i].id);
- }
- var copy = angular.copy($scope.selectedScopes);
- $scope.$watch('selectedScopes', function() {
- if (!angular.equals($scope.selectedScopes, copy)) {
- $scope.changed = true;
- }
- }, true);
- });
});
});
});
}
+
+ ResourceServerPolicy.scopes({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : policy.id
+ }, function(scopes) {
+ $scope.selectedScopes = [];
+ for (i = 0; i < scopes.length; i++) {
+ scopes[i].text = scopes[i].name;
+ $scope.selectedScopes.push(scopes[i].id);
+ }
+ var copy = angular.copy($scope.selectedScopes);
+ $scope.$watch('selectedScopes', function() {
+ if (!angular.equals($scope.selectedScopes, copy)) {
+ $scope.changed = true;
+ }
+ }, true);
+ });
} else {
$scope.selectedResource = null;
var copy = angular.copy($scope.selectedResource);
@@ -2098,35 +2168,7 @@ module.service("PolicyController", function($http, $route, $location, ResourceSe
});
$scope.remove = function() {
- var msg = "";
-
- service.dependentPolicies({
- realm : $route.current.params.realm,
- client : client.id,
- id : $scope.policy.id
- }, function (dependentPolicies) {
- if (dependentPolicies.length > 0 && !$scope.deleteConsent) {
- msg = "<p>This policy is being used by other policies:</p>";
- msg += "<ul>";
- for (i = 0; i < dependentPolicies.length; i++) {
- msg+= "<li><strong>" + dependentPolicies[i].name + "</strong></li>";
- }
- msg += "</ul>";
- msg += "<p>If you remove this policy, the policies above will be affected and will not be associated with this policy anymore.</p>";
- }
-
- AuthzDialog.confirmDeleteWithMsg($scope.policy.name, "Policy", msg, function() {
- service.delete({realm : $scope.realm.realm, client : $scope.client.id, id : $scope.policy.id}, null, function() {
- if (delegate.isPermission()) {
- $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/permission");
- Notifications.success("The permission has been deleted.");
- } else {
- $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy");
- Notifications.success("The policy has been deleted.");
- }
- });
- });
- });
+ Policies.delete(ResourceServerPolicy, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route, delegate.isPermission());
}
}
});
@@ -2354,8 +2396,8 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio
}
$http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/evaluate'
- , $scope.authzRequest).success(function(data) {
- $scope.evaluationResult = data;
+ , $scope.authzRequest).then(function(response) {
+ $scope.evaluationResult = response.data;
$scope.showResultTab();
});
}
@@ -2363,8 +2405,8 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio
$scope.entitlements = function() {
$scope.authzRequest.entitlements = true;
$http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/evaluate'
- , $scope.authzRequest).success(function(data) {
- $scope.evaluationResult = data;
+ , $scope.authzRequest).then(function(response) {
+ $scope.evaluationResult = response.data;
$scope.showResultTab();
});
}
@@ -2496,16 +2538,17 @@ module.controller('RealmRolePermissionsCtrl', function($scope, $http, $route, $l
$scope.realm = realm;
RoleManagementPermissions.get({realm: realm.realm, role: role.id}, function(data) {
$scope.permissions = data;
+ $scope.$watch('permissions.enabled', function(newVal, oldVal) {
+ if (newVal != oldVal) {
+ console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
+ var param = {enabled: $scope.permissions.enabled};
+ $scope.permissions= RoleManagementPermissions.update({realm: realm.realm, role:role.id}, param);
+ }
+ }, true);
});
Client.query({realm: realm.realm, clientId: getManageClientId(realm)}, function(data) {
$scope.realmManagementClientId = data[0].id;
});
- $scope.setEnabled = function() {
- var param = { enabled: $scope.permissions.enabled};
- $scope.permissions= RoleManagementPermissions.update({realm: realm.realm, role:role.id}, param);
- };
-
-
});
module.controller('ClientRolePermissionsCtrl', function($scope, $http, $route, $location, realm, client, role, Client, RoleManagementPermissions, Client, Notifications) {
console.log('RealmRolePermissionsCtrl');
@@ -2514,33 +2557,39 @@ module.controller('ClientRolePermissionsCtrl', function($scope, $http, $route, $
$scope.realm = realm;
RoleManagementPermissions.get({realm: realm.realm, role: role.id}, function(data) {
$scope.permissions = data;
+ $scope.$watch('permissions.enabled', function(newVal, oldVal) {
+ if (newVal != oldVal) {
+ console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
+ var param = {enabled: $scope.permissions.enabled};
+ $scope.permissions = RoleManagementPermissions.update({realm: realm.realm, role:role.id}, param);
+ }
+ }, true);
});
Client.query({realm: realm.realm, clientId: getManageClientId(realm)}, function(data) {
$scope.realmManagementClientId = data[0].id;
});
- $scope.setEnabled = function() {
- console.log('perssions enabled: ' + $scope.permissions.enabled);
- var param = { enabled: $scope.permissions.enabled};
- $scope.permissions = RoleManagementPermissions.update({realm: realm.realm, role:role.id}, param);
- };
-
-
});
module.controller('UsersPermissionsCtrl', function($scope, $http, $route, $location, realm, UsersManagementPermissions, Client, Notifications) {
console.log('UsersPermissionsCtrl');
$scope.realm = realm;
+ var first = true;
UsersManagementPermissions.get({realm: realm.realm}, function(data) {
$scope.permissions = data;
+ $scope.$watch('permissions.enabled', function(newVal, oldVal) {
+ if (newVal != oldVal) {
+ console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
+ var param = {enabled: $scope.permissions.enabled};
+ $scope.permissions = UsersManagementPermissions.update({realm: realm.realm}, param);
+
+ }
+ }, true);
});
Client.query({realm: realm.realm, clientId: getManageClientId(realm)}, function(data) {
$scope.realmManagementClientId = data[0].id;
});
- $scope.changeIt = function() {
- console.log('before permissions.enabled=' + $scope.permissions.enabled);
- var param = { enabled: $scope.permissions.enabled};
- $scope.permissions = UsersManagementPermissions.update({realm: realm.realm}, param);
- };
+
+
});
@@ -2550,16 +2599,17 @@ module.controller('ClientPermissionsCtrl', function($scope, $http, $route, $loca
$scope.realm = realm;
ClientManagementPermissions.get({realm: realm.realm, client: client.id}, function(data) {
$scope.permissions = data;
+ $scope.$watch('permissions.enabled', function(newVal, oldVal) {
+ if (newVal != oldVal) {
+ console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
+ var param = {enabled: $scope.permissions.enabled};
+ $scope.permissions = ClientManagementPermissions.update({realm: realm.realm, client: client.id}, param);
+ }
+ }, true);
});
Client.query({realm: realm.realm, clientId: getManageClientId(realm)}, function(data) {
$scope.realmManagementClientId = data[0].id;
});
- $scope.setEnabled = function() {
- var param = { enabled: $scope.permissions.enabled};
- $scope.permissions = ClientManagementPermissions.update({realm: realm.realm, client: client.id}, param);
- };
-
-
});
module.controller('GroupPermissionsCtrl', function($scope, $http, $route, $location, realm, group, GroupManagementPermissions, Client, Notifications) {
@@ -2570,13 +2620,14 @@ module.controller('GroupPermissionsCtrl', function($scope, $http, $route, $locat
});
GroupManagementPermissions.get({realm: realm.realm, group: group.id}, function(data) {
$scope.permissions = data;
+ $scope.$watch('permissions.enabled', function(newVal, oldVal) {
+ if (newVal != oldVal) {
+ console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
+ var param = {enabled: $scope.permissions.enabled};
+ $scope.permissions = GroupManagementPermissions.update({realm: realm.realm, group: group.id}, param);
+ }
+ }, true);
});
- $scope.setEnabled = function() {
- var param = { enabled: $scope.permissions.enabled};
- $scope.permissions = GroupManagementPermissions.update({realm: realm.realm, group: group.id}, param);
- };
-
-
});
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 515eb99..33cb93b 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -495,8 +495,8 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht
'Content-Type': 'application/json',
'Accept': 'application/octet-stream'
}
- }).success(function(data){
- var blob = new Blob([data], {
+ }).then(function(response){
+ var blob = new Blob([response.data], {
type: 'application/octet-stream'
});
var ext = ".jks";
@@ -508,10 +508,10 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht
}
saveAs(blob, 'keystore' + ext);
- }).error(function(data) {
+ }).catch(function(response) {
var errorMsg = 'Error downloading';
try {
- var error = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(data)));
+ var error = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(response.data)));
errorMsg = error['error_description'] ? error['error_description'] : errorMsg;
} catch (err) {
}
@@ -782,16 +782,16 @@ module.controller('ClientInstallationCtrl', function($scope, realm, client, serv
method: 'GET',
responseType: 'arraybuffer',
cache: false
- }).success(function(data) {
- var installation = data;
+ }).then(function(response) {
+ var installation = response.data;
$scope.installation = installation;
}
);
} else {
- $http.get(url).success(function (data) {
- var installation = data;
+ $http.get(url).then(function (response) {
+ var installation = response.data;
if ($scope.configFormat.mediaType == 'application/json') {
- installation = angular.fromJson(data);
+ installation = angular.fromJson(response.data);
installation = angular.toJson(installation, true);
}
$scope.installation = installation;
@@ -814,7 +814,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
"bearer-only"
];
- $scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort();
+ $scope.protocols = serverInfo.listProviderIds('login-protocol');
$scope.templates = [ {name:'NONE'}];
for (var i = 0; i < templates.length; i++) {
@@ -1240,7 +1240,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
});
module.controller('CreateClientCtrl', function($scope, realm, client, templates, $route, serverInfo, Client, ClientDescriptionConverter, $location, $modal, Dialog, Notifications) {
- $scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort();
+ $scope.protocols = serverInfo.listProviderIds('login-protocol');
$scope.create = true;
$scope.templates = [ {name:'NONE'}];
var templateNameMap = new Object();
@@ -1428,7 +1428,7 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien
var roles = $scope.selectedRealmRoles;
$scope.selectedRealmRoles = [];
$http.post(authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/scope-mappings/realm',
- roles).success(function() {
+ roles).then(function() {
updateRealmRoles();
Notifications.success("Scope mappings updated.");
});
@@ -1438,7 +1438,7 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien
var roles = $scope.selectedRealmMappings;
$scope.selectedRealmMappings = [];
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/scope-mappings/realm',
- {data : roles, headers : {"content-type" : "application/json"}}).success(function () {
+ {data : roles, headers : {"content-type" : "application/json"}}).then(function () {
updateRealmRoles();
Notifications.success("Scope mappings updated.");
});
@@ -1448,7 +1448,7 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien
var roles = $scope.selectedClientRoles;
$scope.selectedClientRoles = [];
$http.post(authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/scope-mappings/clients/' + $scope.targetClient.id,
- roles).success(function () {
+ roles).then(function () {
updateClientRoles();
Notifications.success("Scope mappings updated.");
});
@@ -1458,7 +1458,7 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien
var roles = $scope.selectedClientMappings;
$scope.selectedClientMappings = [];
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/scope-mappings/clients/' + $scope.targetClient.id,
- {data : roles, headers : {"content-type" : "application/json"}}).success(function () {
+ {data : roles, headers : {"content-type" : "application/json"}}).then(function () {
updateClientRoles();
Notifications.success("Scope mappings updated.");
});
@@ -1688,10 +1688,10 @@ module.controller('AddBuiltinProtocolMapperCtrl', function($scope, realm, client
}
}
$http.post(authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/protocol-mappers/add-models',
- toAdd).success(function() {
+ toAdd).then(function() {
Notifications.success("Mappers added");
$location.url('/realms/' + realm.realm + '/clients/' + client.id + '/mappers');
- }).error(function() {
+ }).catch(function() {
Notifications.error("Error adding mappers");
$location.url('/realms/' + realm.realm + '/clients/' + client.id + '/mappers');
});
@@ -1915,7 +1915,7 @@ module.controller('ClientTemplateListCtrl', function($scope, realm, templates, C
});
module.controller('ClientTemplateDetailCtrl', function($scope, realm, template, $route, serverInfo, ClientTemplate, $location, $modal, Dialog, Notifications) {
- $scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort();
+ $scope.protocols = serverInfo.listProviderIds('login-protocol');
$scope.realm = realm;
$scope.create = !template.name;
@@ -2202,10 +2202,10 @@ module.controller('ClientTemplateAddBuiltinProtocolMapperCtrl', function($scope,
}
}
$http.post(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/protocol-mappers/add-models',
- toAdd).success(function() {
+ toAdd).then(function() {
Notifications.success("Mappers added");
$location.url('/realms/' + realm.realm + '/client-templates/' + template.id + '/mappers');
- }).error(function() {
+ }).catch(function() {
Notifications.error("Error adding mappers");
$location.url('/realms/' + realm.realm + '/client-templates/' + template.id + '/mappers');
});
@@ -2273,7 +2273,7 @@ module.controller('ClientTemplateScopeMappingCtrl', function($scope, $http, real
var roles = $scope.selectedRealmRoles;
$scope.selectedRealmRoles = [];
$http.post(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/scope-mappings/realm',
- roles).success(function() {
+ roles).then(function() {
updateTemplateRealmRoles();
Notifications.success("Scope mappings updated.");
});
@@ -2283,7 +2283,7 @@ module.controller('ClientTemplateScopeMappingCtrl', function($scope, $http, real
var roles = $scope.selectedRealmMappings;
$scope.selectedRealmMappings = [];
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/scope-mappings/realm',
- {data : roles, headers : {"content-type" : "application/json"}}).success(function () {
+ {data : roles, headers : {"content-type" : "application/json"}}).then(function () {
updateTemplateRealmRoles();
Notifications.success("Scope mappings updated.");
});
@@ -2293,7 +2293,7 @@ module.controller('ClientTemplateScopeMappingCtrl', function($scope, $http, real
var roles = $scope.selectedClientRoles;
$scope.selectedClientRoles = [];
$http.post(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/scope-mappings/clients/' + $scope.targetClient.id,
- roles).success(function () {
+ roles).then(function () {
updateTemplateClientRoles();
Notifications.success("Scope mappings updated.");
});
@@ -2303,7 +2303,7 @@ module.controller('ClientTemplateScopeMappingCtrl', function($scope, $http, real
var roles = $scope.selectedClientMappings;
$scope.selectedClientMappings = [];
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/scope-mappings/clients/' + $scope.targetClient.id,
- {data : roles, headers : {"content-type" : "application/json"}}).success(function () {
+ {data : roles, headers : {"content-type" : "application/json"}}).then(function () {
updateTemplateClientRoles();
Notifications.success("Scope mappings updated.");
});
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js
index 00c8e93..aa0cfad 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js
@@ -236,7 +236,7 @@ module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group,
var roles = $scope.selectedRealmRoles;
$scope.selectedRealmRoles = [];
$http.post(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/realm',
- roles).success(function() {
+ roles).then(function() {
$scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
@@ -257,7 +257,7 @@ module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group,
$scope.deleteRealmRole = function() {
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/realm',
- {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).success(function() {
+ {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).then(function() {
$scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
@@ -277,7 +277,7 @@ module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group,
$scope.addClientRole = function() {
$http.post(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/clients/' + $scope.targetClient.id,
- $scope.selectedClientRoles).success(function() {
+ $scope.selectedClientRoles).then(function() {
$scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
@@ -291,7 +291,7 @@ module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group,
$scope.deleteClientRole = function() {
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/clients/' + $scope.targetClient.id,
- {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).success(function() {
+ {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).then(function() {
$scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index ab4e72a..77dc9cd 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -406,16 +406,20 @@ module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serv
$scope.supportedLocalesOptions = {
'multiple' : true,
- 'simple_tags' : true
+ 'simple_tags' : true,
+ 'tags' : []
};
-
+
+ updateSupported();
+
function localeForTheme(type, name) {
name = name || 'base';
for (var i = 0; i < serverInfo.themes[type].length; i++) {
if (serverInfo.themes[type][i].name == name) {
- return serverInfo.themes[type][i].locales;
+ return serverInfo.themes[type][i].locales || [];
}
}
+ return [];
}
function updateSupported() {
@@ -923,11 +927,11 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
//formDataAppender: function(formData, key, val){}
}).progress(function(evt) {
console.log('percent: ' + parseInt(100.0 * evt.loaded / evt.total));
- }).success(function(data, status, headers) {
- setConfig(data);
+ }).then(function(response) {
+ setConfig(response.data);
$scope.clearFileSelect();
Notifications.success("The IDP metadata has been loaded from file.");
- }).error(function() {
+ }).catch(function() {
Notifications.error("The file can not be uploaded. Please verify the file.");
});
}
@@ -943,12 +947,12 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
providerId: providerFactory.id
}
$http.post(authUrl + '/admin/realms/' + realm.realm + '/identity-provider/import-config', input)
- .success(function(data, status, headers) {
- setConfig(data);
+ .then(function(response) {
+ setConfig(response.data);
$scope.fromUrl.data = '';
$scope.importUrl = false;
Notifications.success("Imported config information from url.");
- }).error(function() {
+ }).catch(function() {
Notifications.error("Config can not be imported. Please verify the url.");
});
};
@@ -1047,9 +1051,9 @@ module.controller('RealmIdentityProviderExportCtrl', function(realm, identityPro
$scope.exportedType = "";
var url = IdentityProviderExport.url({realm: realm.realm, alias: identityProvider.alias}) ;
- $http.get(url).success(function(data, status, headers, config) {
- $scope.exportedType = headers('Content-Type');
- $scope.exported = data;
+ $http.get(url).then(function(response) {
+ $scope.exportedType = response.headers('Content-Type');
+ $scope.exported = response.data;
});
$scope.download = function() {
@@ -1449,7 +1453,7 @@ module.controller('RoleDetailCtrl', function($scope, realm, role, roles, clients
$http, $location, Notifications, Dialog);
});
-module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, realm, $http, $location, Dialog, Notifications) {
+module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, realm, $http, $location, Dialog, Notifications, RealmSMTPConnectionTester) {
console.log('RealmSMTPSettingsCtrl');
var booleanSmtpAtts = ["auth","ssl","starttls"];
@@ -1484,6 +1488,25 @@ module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, real
$scope.changed = false;
};
+ var initSMTPTest = function() {
+ return {
+ realm: $scope.realm.realm,
+ config: JSON.stringify(realm.smtpServer)
+ };
+ };
+
+ $scope.testConnection = function() {
+ RealmSMTPConnectionTester.send(initSMTPTest(), function() {
+ Notifications.success("SMTP connection successful. E-mail was sent!");
+ }, function(errorResponse) {
+ if (error.data.errorMessage) {
+ Notifications.error(error.data.errorMessage);
+ } else {
+ Notifications.error('Unexpected error during SMTP validation');
+ }
+ });
+ };
+
/* Convert string attributes containing a boolean to actual boolean type + convert an integer string (port) to integer. */
function typeObject(obj){
for (var att in obj){
@@ -1526,9 +1549,15 @@ module.controller('RealmEventsConfigCtrl', function($scope, eventsConfig, RealmE
$scope.eventsConfig.expirationUnit = TimeUnit.autoUnit(eventsConfig.eventsExpiration);
$scope.eventsConfig.eventsExpiration = TimeUnit.toUnit(eventsConfig.eventsExpiration, $scope.eventsConfig.expirationUnit);
-
+
$scope.eventListeners = Object.keys(serverInfo.providers.eventsListener.providers);
-
+
+ $scope.eventsConfigSelectOptions = {
+ 'multiple': true,
+ 'simple_tags': true,
+ 'tags': $scope.eventListeners
+ };
+
$scope.eventSelectOptions = {
'multiple': true,
'simple_tags': true,
@@ -1916,6 +1945,8 @@ module.controller('RealmFlowBindingCtrl', function($scope, flows, Current, Realm
}
}
+ $scope.profileInfo = serverInfo.profileInfo;
+
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/flow-bindings");
});
@@ -2110,6 +2141,9 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo
} else if (realm.clientAuthenticationFlow == $scope.flow.alias) {
Notifications.error("Cannot remove flow, it is currently being used as the client authentication flow.");
+ } else if (realm.dockerAuthenticationFlow == $scope.flow.alias) {
+ Notifications.error("Cannot remove flow, it is currently being used as the docker authentication flow.");
+
} else {
AuthenticationFlows.remove({realm: realm.realm, flow: $scope.flow.id}, function () {
$location.url("/realms/" + realm.realm + '/authentication/flows/' + flows[0].alias);
@@ -2770,11 +2804,11 @@ module.controller('RealmExportCtrl', function($scope, realm, $http,
exportUrl += '?' + $httpParamSerializer(params);
}
$http.post(exportUrl)
- .success(function(data, status, headers) {
- var download = angular.fromJson(data);
+ .then(function(response) {
+ var download = angular.fromJson(response.data);
download = angular.toJson(download, true);
saveAs(new Blob([download], { type: 'application/json' }), 'realm-export.json');
- }).error(function() {
+ }).catch(function() {
Notifications.error("Sorry, something went wrong.");
});
}
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index 580d661..549f621 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -23,7 +23,7 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl
var roles = $scope.selectedRealmRoles;
$scope.selectedRealmRoles = [];
$http.post(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/realm',
- roles).success(function() {
+ roles).then(function() {
$scope.realmMappings = RealmRoleMapping.query({realm : realm.realm, userId : user.id});
$scope.realmRoles = AvailableRealmRoleMapping.query({realm : realm.realm, userId : user.id});
$scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id});
@@ -44,7 +44,7 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl
$scope.deleteRealmRole = function() {
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/realm',
- {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).success(function() {
+ {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).then(function() {
$scope.realmMappings = RealmRoleMapping.query({realm : realm.realm, userId : user.id});
$scope.realmRoles = AvailableRealmRoleMapping.query({realm : realm.realm, userId : user.id});
$scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id});
@@ -64,7 +64,7 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl
$scope.addClientRole = function() {
$http.post(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.targetClient.id,
- $scope.selectedClientRoles).success(function() {
+ $scope.selectedClientRoles).then(function() {
$scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
$scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
$scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
@@ -78,7 +78,7 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl
$scope.deleteClientRole = function() {
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.targetClient.id,
- {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).success(function() {
+ {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).then(function() {
$scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
$scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
$scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
@@ -402,7 +402,11 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
$scope.userReqActionList.push(item);
}
}
-
+ console.log("---------------------");
+ console.log("ng-model: user.requiredActions=" + JSON.stringify($scope.user.requiredActions));
+ console.log("---------------------");
+ console.log("ng-repeat: userReqActionList=" + JSON.stringify($scope.userReqActionList));
+ console.log("---------------------");
});
$scope.$watch('user', function() {
if (!angular.equals($scope.user, user)) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index e850b3b..fca7b33 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -320,12 +320,57 @@ module.factory('RealmLDAPConnectionTester', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection');
});
+module.factory('RealmSMTPConnectionTester', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/testSMTPConnection/:config', {
+ realm : '@realm',
+ config : '@config'
+ }, {
+ send: {
+ method: 'POST'
+ }
+ });
+});
+
module.service('ServerInfo', function($resource, $q, $http) {
var info = {};
var delay = $q.defer();
- $http.get(authUrl + '/admin/serverinfo').success(function(data) {
+ function copyInfo(data, info) {
angular.copy(data, info);
+
+ info.listProviderIds = function(spi) {
+ var providers = info.providers[spi].providers;
+ var ids = Object.keys(providers);
+ ids.sort(function(a, b) {
+ var s1;
+ var s2;
+
+ if (providers[a].order != providers[b].order) {
+ s1 = providers[b].order;
+ s2 = providers[a].order;
+ } else {
+ s1 = a;
+ s2 = b;
+ }
+
+ if (s1 < s2) {
+ return -1;
+ } else if (s1 > s2) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+ return ids;
+ }
+
+ info.featureEnabled = function(provider) {
+ return info.profileInfo.disabledFeatures.indexOf(provider) == -1;
+ }
+ }
+
+ $http.get(authUrl + '/admin/serverinfo').then(function(response) {
+ copyInfo(response.data, info);
delay.resolve(info);
});
@@ -334,8 +379,8 @@ module.service('ServerInfo', function($resource, $q, $http) {
return info;
},
reload: function() {
- $http.get(authUrl + '/admin/serverinfo').success(function(data) {
- angular.copy(data, info);
+ $http.get(authUrl + '/admin/serverinfo').then(function(response) {
+ copyInfo(response.data, info);
});
},
promise: delay.promise
@@ -732,7 +777,7 @@ function roleControl($scope, realm, role, roles, clients,
$scope.addRealmRole = function() {
$scope.compositeSwitchDisabled=true;
$http.post(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
- $scope.selectedRealmRoles).success(function() {
+ $scope.selectedRealmRoles).then(function() {
for (var i = 0; i < $scope.selectedRealmRoles.length; i++) {
var role = $scope.selectedRealmRoles[i];
var idx = $scope.realmRoles.indexOf($scope.selectedRealmRoles[i]);
@@ -749,7 +794,7 @@ function roleControl($scope, realm, role, roles, clients,
$scope.deleteRealmRole = function() {
$scope.compositeSwitchDisabled=true;
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
- {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).success(function() {
+ {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).then(function() {
for (var i = 0; i < $scope.selectedRealmMappings.length; i++) {
var role = $scope.selectedRealmMappings[i];
var idx = $scope.realmMappings.indexOf($scope.selectedRealmMappings[i]);
@@ -766,7 +811,7 @@ function roleControl($scope, realm, role, roles, clients,
$scope.addClientRole = function() {
$scope.compositeSwitchDisabled=true;
$http.post(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
- $scope.selectedClientRoles).success(function() {
+ $scope.selectedClientRoles).then(function() {
for (var i = 0; i < $scope.selectedClientRoles.length; i++) {
var role = $scope.selectedClientRoles[i];
var idx = $scope.clientRoles.indexOf($scope.selectedClientRoles[i]);
@@ -782,7 +827,7 @@ function roleControl($scope, realm, role, roles, clients,
$scope.deleteClientRole = function() {
$scope.compositeSwitchDisabled=true;
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
- {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).success(function() {
+ {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).then(function() {
for (var i = 0; i < $scope.selectedClientMappings.length; i++) {
var role = $scope.selectedClientMappings[i];
var idx = $scope.clientMappings.indexOf($scope.selectedClientMappings[i]);
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html b/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html
index 8a9d0e1..6bf39f3 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html
@@ -47,7 +47,7 @@
</div>
<div class="form-group">
- <label for="resetCredentials" class="col-md-2 control-label">{{:: 'client-authentication' | translate}}</label>
+ <label for="clientAuthentication" class="col-md-2 control-label">{{:: 'client-authentication' | translate}}</label>
<div class="col-md-2">
<div>
<select id="clientAuthentication" ng-model="realm.clientAuthenticationFlow" class="form-control" ng-options="flow.alias as flow.alias for flow in clientFlows">
@@ -57,6 +57,18 @@
<kc-tooltip>{{:: 'client-authentication.tooltip' | translate}}</kc-tooltip>
</div>
+
+ <div class="form-group" data-ng-show="serverInfo.featureEnabled('DOCKER')">
+ <label for="dockerAuth" class="col-md-2 control-label">{{:: 'docker-auth' | translate}}</label>
+ <div class="col-md-2">
+ <div>
+ <select id="dockerAuth" ng-model="realm.dockerAuthenticationFlow" class="form-control" ng-options="flow.alias as flow.alias for flow in flows">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'docker-auth.tooltip' | translate}}</kc-tooltip>
+ </div>
+
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-permissions.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-permissions.html
index abc21a4..7f29fd7 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-permissions.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-permissions.html
@@ -11,7 +11,7 @@
<div class="form-group">
<label class="col-md-2 control-label" for="permissionsEnabled">{{:: 'permissions-enabled-role' | translate}}</label>
<div class="col-md-6">
- <input ng-model="permissions.enabled" ng-click="setEnabled()" name="permissionsEnabled" id="permissionsEnabled" ng-disabled="!access.manageAuthorization" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+ <input ng-model="permissions.enabled" name="permissionsEnabled" id="permissionsEnabled" ng-disabled="!access.manageAuthorization" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<kc-tooltip>{{:: 'permissions-enabled-role.tooltip' | translate}}</kc-tooltip>
</div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-role-permissions.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-role-permissions.html
index c5f37ea..c76ecec 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-role-permissions.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-role-permissions.html
@@ -12,7 +12,7 @@
<div class="form-group">
<label class="col-md-2 control-label" for="permissionsEnabled">{{:: 'permissions-enabled-role' | translate}}</label>
<div class="col-md-6">
- <input ng-model="permissions.enabled" ng-click="setEnabled()" name="permissionsEnabled" id="permissionsEnabled" ng-disabled="!access.manageAuthorization" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+ <input ng-model="permissions.enabled" name="permissionsEnabled" id="permissionsEnabled" ng-disabled="!access.manageAuthorization" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<kc-tooltip>{{:: 'permissions-enabled-role.tooltip' | translate}}</kc-tooltip>
</div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/group-permissions.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/group-permissions.html
index 897a0ed..f2be6d9 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/group-permissions.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/group-permissions.html
@@ -11,7 +11,7 @@
<div class="form-group">
<label class="col-md-2 control-label" for="permissionsEnabled">{{:: 'permissions-enabled-role' | translate}}</label>
<div class="col-md-6">
- <input ng-model="permissions.enabled" ng-click="setEnabled()" name="permissionsEnabled" id="permissionsEnabled" ng-disabled="!access.manageAuthorization" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+ <input ng-model="permissions.enabled" name="permissionsEnabled" id="permissionsEnabled" ng-disabled="!access.manageAuthorization" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<kc-tooltip>{{:: 'permissions-enabled-role.tooltip' | translate}}</kc-tooltip>
</div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/realm-role-permissions.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/realm-role-permissions.html
index 9c03333..e21ee63 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/realm-role-permissions.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/realm-role-permissions.html
@@ -11,7 +11,7 @@
<div class="form-group">
<label class="col-md-2 control-label" for="permissionsEnabled">{{:: 'permissions-enabled-role' | translate}}</label>
<div class="col-md-6">
- <input ng-model="permissions.enabled" ng-click="setEnabled()" name="permissionsEnabled" id="permissionsEnabled" ng-disabled="!access.manageAuthorization" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+ <input ng-model="permissions.enabled" name="permissionsEnabled" id="permissionsEnabled" ng-disabled="!access.manageAuthorization" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<kc-tooltip>{{:: 'permissions-enabled-role.tooltip' | translate}}</kc-tooltip>
</div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/users-permissions.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/users-permissions.html
index 4a5661f..2665bba 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/users-permissions.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/users-permissions.html
@@ -7,7 +7,7 @@
<div class="form-group">
<label class="col-md-2 control-label" for="permissionsEnabled">{{:: 'permissions-enabled-users' | translate}}</label>
<div class="col-md-6">
- <input ng-model="permissions.enabled" ng-click="changeIt()" name="permissionsEnabled" id="permissionsEnabled" ng-disabled="!access.manageAuthorization" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+ <input ng-model="permissions.enabled" name="permissionsEnabled" id="permissionsEnabled" ng-disabled="!access.manageAuthorization" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<kc-tooltip>{{:: 'permissions-enabled-users.tooltip' | translate}}</kc-tooltip>
</div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html
index 79cec9a..df4377f 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html
@@ -38,7 +38,6 @@
</div>
<div class="form-group clearfix" data-ng-show="selectedResource">
<label class="col-md-2 control-label" for="resourceScopes">{{:: 'authz-scopes' | translate}} <span class="required">*</span></label>
-
<div class="col-md-6">
<select ui-select2 id="resourceScopes"
data-ng-model="selectedScopes"
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index cd6e271..88d765a 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -37,7 +37,7 @@
</div>
<kc-tooltip>{{:: 'client.enabled.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group clearfix block">
+ <div class="form-group clearfix block" data-ng-show="protocol != 'docker-v2'">
<label class="col-md-2 control-label" for="consentRequired">{{:: 'consent-required' | translate}}</label>
<div class="col-sm-6">
<input ng-model="clientEdit.consentRequired" name="consentRequired" id="consentRequired" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
@@ -110,7 +110,7 @@
<input ng-model="clientEdit.serviceAccountsEnabled" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
</div>
- <div class="form-group" data-ng-show="serverInfo.profileInfo.disabledFeatures.indexOf('AUTHORIZATION') == -1 && protocol == 'openid-connect'">
+ <div class="form-group" data-ng-show="serverInfo.featureEnabled('AUTHORIZATION') && protocol == 'openid-connect'">
<label class="col-md-2 control-label" for="authorizationServicesEnabled">{{:: 'authz-authorization-services-enabled' | translate}}</label>
<kc-tooltip>{{:: 'authz-authorization-services-enabled.tooltip' | translate}}</kc-tooltip>
<div class="col-md-6">
@@ -239,7 +239,7 @@
<kc-tooltip>{{:: 'name-id-format.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group" data-ng-show="!clientEdit.bearerOnly">
+ <div class="form-group" data-ng-show="!clientEdit.bearerOnly && protocol != 'docker-v2'">
<label class="col-md-2 control-label" for="rootUrl">{{:: 'root-url' | translate}}</label>
<div class="col-sm-6">
<input class="form-control" type="text" name="rootUrl" id="rootUrl" data-ng-model="clientEdit.rootUrl">
@@ -247,7 +247,7 @@
<kc-tooltip>{{:: 'root-url.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group clearfix block" data-ng-hide="clientEdit.bearerOnly || (!clientEdit.standardFlowEnabled && !clientEdit.implicitFlowEnabled)">
+ <div class="form-group clearfix block" data-ng-hide="clientEdit.bearerOnly || (!clientEdit.standardFlowEnabled && !clientEdit.implicitFlowEnabled) || protocol == 'docker-v2'">
<label class="col-md-2 control-label" for="newRedirectUri"><span class="required" data-ng-show="protocol != 'saml'">*</span> {{:: 'valid-redirect-uris' | translate}}</label>
<div class="col-sm-6">
@@ -269,14 +269,14 @@
<kc-tooltip>{{:: 'valid-redirect-uris.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group" data-ng-show="!clientEdit.bearerOnly">
+ <div class="form-group" data-ng-show="!clientEdit.bearerOnly && protocol != 'docker-v2'">
<label class="col-md-2 control-label" for="baseUrl">{{:: 'base-url' | translate}}</label>
<div class="col-sm-6">
<input class="form-control" type="text" name="baseUrl" id="baseUrl" data-ng-model="clientEdit.baseUrl">
</div>
<kc-tooltip>{{:: 'base-url.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group" data-ng-hide="protocol == 'saml'">
+ <div class="form-group" data-ng-hide="protocol == 'saml' || protocol == 'docker-v2'">
<label class="col-md-2 control-label" for="adminUrl">{{:: 'admin-url' | translate}}</label>
<div class="col-sm-6">
<input class="form-control" type="text" name="adminUrl" id="adminUrl"
@@ -306,7 +306,7 @@
</div>
<kc-tooltip>{{:: 'idp-sso-relay-state.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group" data-ng-show="!clientEdit.bearerOnly && protocol == 'openid-connect' && (clientEdit.standardFlowEnabled || clientEdit.implicitFlowEnabled)">
+ <div class="form-group" data-ng-show="(!clientEdit.bearerOnly && protocol == 'openid-connect') && (clientEdit.standardFlowEnabled || clientEdit.directAccessGrantsEnabled || clientEdit.implicitFlowEnabled)">
<label class="col-md-2 control-label" for="newWebOrigin">{{:: 'web-origins' | translate}}</label>
<div class="col-sm-6">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html
index a304784..c610f68 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html
@@ -19,9 +19,7 @@
<label class="col-md-2 control-label" for="eventsListeners" class="control-label">{{:: 'event-listeners' | translate}}</label>
<kc-tooltip>{{:: 'event-listeners.tooltip' | translate}}</kc-tooltip>
<div class="col-md-6">
- <select ui-select2 ng-model="eventsConfig.eventsListeners" data-placeholder="{{:: 'select-an-action.placeholder' | translate}}" multiple>
- <option ng-repeat="listener in eventListeners" value="{{listener}}">{{listener}}</option>
- </select>
+ <input ui-select2="eventsConfigSelectOptions" id="eventsListeners" ng-model="eventsConfig.eventsListeners" data-placeholder="{{:: 'select-an-action.placeholder' | translate}}"/>
</div>
</div>
</fieldset>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-bitbucket.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-bitbucket.html
new file mode 100755
index 0000000..90d5c1f
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-bitbucket.html
@@ -0,0 +1,130 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
+ <li data-ng-hide="newIdentityProvider">{{provider.name}}</li>
+ <li data-ng-show="newIdentityProvider">{{:: 'add-identity-provider' | translate}}</li>
+ </ol>
+
+ <kc-tabs-identity-provider></kc-tabs-identity-provider>
+
+ <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageIdentityProviders">
+ <input type="text" readonly value="this is not a login form" style="display: none;">
+ <input type="password" readonly value="this is not a login form" style="display: none;">
+
+ <fieldset>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="redirectUri">{{:: 'redirect-uri' | translate}}</label>
+ <div class="col-sm-6">
+ <input class="form-control" id="redirectUri" type="text" value="{{callbackUrl}}{{identityProvider.alias}}/endpoint" readonly kc-select-action="click">
+ </div>
+ <kc-tooltip>{{:: 'redirect-uri.tooltip' | translate}}</kc-tooltip>
+ </div>
+ </fieldset>
+ <fieldset>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="clientId"><span class="required">*</span> {{:: 'bitbucket-consumer-key' | translate}}</label>
+ <div class="col-md-6">
+ <input class="form-control" id="clientId" type="text" ng-model="identityProvider.config.clientId" required>
+ </div>
+ <kc-tooltip>{{:: 'bitbucket.key.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="clientSecret"><span class="required">*</span> {{:: 'bitbucket-consumer-secret' | translate}}</label>
+ <div class="col-md-6">
+ <input class="form-control" id="clientSecret" type="password" ng-model="identityProvider.config.clientSecret" required>
+ </div>
+ <kc-tooltip>{{:: 'bitbucket.secret.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="defaultScope">{{:: 'default-scopes' | translate}} </label>
+ <div class="col-md-6">
+ <input class="form-control" id="defaultScope" type="text" ng-model="identityProvider.config.defaultScope">
+ </div>
+ <kc-tooltip>{{:: 'bitbucket.default-scopes.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="enabled">{{:: 'store-tokens' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.storeToken" id="storeToken" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'identity-provider.store-tokens.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="storedTokensReadable">{{:: 'stored-tokens-readable' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.addReadTokenRoleOnCreate" id="storedTokensReadable" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'identity-provider.stored-tokens-readable.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="enabled">{{:: 'enabled' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.enabled" id="enabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'identity-provider.enabled.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="trustEmail">{{:: 'trust-email' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.trustEmail" name="identityProvider.trustEmail" id="trustEmail" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'trust-email.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="linkOnly">{{:: 'link-only' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.linkOnly" name="identityProvider.trustEmail" id="linkOnly" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'link-only.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="hideOnLoginPage">{{:: 'hide-on-login-page' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.config.hideOnLoginPage" name="identityProvider.config.hideOnLoginPage" id="hideOnLoginPage" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'hide-on-login-page.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="guiOrder">{{:: 'gui-order' | translate}}</label>
+ <div class="col-md-6">
+ <input class="form-control" id="guiOrder" type="text" ng-model="identityProvider.config.guiOrder">
+ </div>
+ <kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="firstBrokerLoginFlowAlias">{{:: 'first-broker-login-flow' | translate}}</label>
+ <div class="col-md-6">
+ <div>
+ <select class="form-control" id="firstBrokerLoginFlowAlias"
+ ng-model="identityProvider.firstBrokerLoginFlowAlias"
+ ng-options="flow.alias as flow.alias for flow in authFlows"
+ required>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'first-broker-login-flow.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="postBrokerLoginFlowAlias">{{:: 'post-broker-login-flow' | translate}}</label>
+ <div class="col-md-6">
+ <div>
+ <select class="form-control" id="postBrokerLoginFlowAlias"
+ ng-model="identityProvider.postBrokerLoginFlowAlias"
+ ng-options="flow.alias as flow.alias for flow in postBrokerAuthFlows">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'post-broker-login-flow.tooltip' | translate}}</kc-tooltip>
+ </div>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2">
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-cancel data-ng-click="cancel()" data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-gitlab.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-gitlab.html
new file mode 100755
index 0000000..152d1f1
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-gitlab.html
@@ -0,0 +1,130 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
+ <li data-ng-hide="newIdentityProvider">{{provider.name}}</li>
+ <li data-ng-show="newIdentityProvider">{{:: 'add-identity-provider' | translate}}</li>
+ </ol>
+
+ <kc-tabs-identity-provider></kc-tabs-identity-provider>
+
+ <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageIdentityProviders">
+ <input type="text" readonly value="this is not a login form" style="display: none;">
+ <input type="password" readonly value="this is not a login form" style="display: none;">
+
+ <fieldset>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="redirectUri">{{:: 'redirect-uri' | translate}}</label>
+ <div class="col-sm-6">
+ <input class="form-control" id="redirectUri" type="text" value="{{callbackUrl}}{{identityProvider.alias}}/endpoint" readonly kc-select-action="click">
+ </div>
+ <kc-tooltip>{{:: 'redirect-uri.tooltip' | translate}}</kc-tooltip>
+ </div>
+ </fieldset>
+ <fieldset>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="clientId"><span class="required">*</span> {{:: 'gitlab-application-id' | translate}}</label>
+ <div class="col-md-6">
+ <input class="form-control" id="clientId" type="text" ng-model="identityProvider.config.clientId" required>
+ </div>
+ <kc-tooltip>{{:: 'gitlab.application-id.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="clientSecret"><span class="required">*</span> {{:: 'gitlab-application-secret' | translate}}</label>
+ <div class="col-md-6">
+ <input class="form-control" id="clientSecret" type="password" ng-model="identityProvider.config.clientSecret" required>
+ </div>
+ <kc-tooltip>{{:: 'gitlab.application-secret.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="defaultScope">{{:: 'default-scopes' | translate}} </label>
+ <div class="col-md-6">
+ <input class="form-control" id="defaultScope" type="text" ng-model="identityProvider.config.defaultScope">
+ </div>
+ <kc-tooltip>{{:: 'gitlab.default-scopes.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="enabled">{{:: 'store-tokens' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.storeToken" id="storeToken" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'identity-provider.store-tokens.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="storedTokensReadable">{{:: 'stored-tokens-readable' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.addReadTokenRoleOnCreate" id="storedTokensReadable" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'identity-provider.stored-tokens-readable.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="enabled">{{:: 'enabled' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.enabled" id="enabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'identity-provider.enabled.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="trustEmail">{{:: 'trust-email' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.trustEmail" name="identityProvider.trustEmail" id="trustEmail" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'trust-email.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="linkOnly">{{:: 'link-only' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.linkOnly" name="identityProvider.trustEmail" id="linkOnly" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'link-only.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="hideOnLoginPage">{{:: 'hide-on-login-page' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.config.hideOnLoginPage" name="identityProvider.config.hideOnLoginPage" id="hideOnLoginPage" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'hide-on-login-page.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="guiOrder">{{:: 'gui-order' | translate}}</label>
+ <div class="col-md-6">
+ <input class="form-control" id="guiOrder" type="text" ng-model="identityProvider.config.guiOrder">
+ </div>
+ <kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="firstBrokerLoginFlowAlias">{{:: 'first-broker-login-flow' | translate}}</label>
+ <div class="col-md-6">
+ <div>
+ <select class="form-control" id="firstBrokerLoginFlowAlias"
+ ng-model="identityProvider.firstBrokerLoginFlowAlias"
+ ng-options="flow.alias as flow.alias for flow in authFlows"
+ required>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'first-broker-login-flow.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="postBrokerLoginFlowAlias">{{:: 'post-broker-login-flow' | translate}}</label>
+ <div class="col-md-6">
+ <div>
+ <select class="form-control" id="postBrokerLoginFlowAlias"
+ ng-model="identityProvider.postBrokerLoginFlowAlias"
+ ng-options="flow.alias as flow.alias for flow in postBrokerAuthFlows">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'post-broker-login-flow.tooltip' | translate}}</kc-tooltip>
+ </div>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2">
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-cancel data-ng-click="cancel()" data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
index 5d3c68e..43df761 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
@@ -10,6 +10,9 @@
<div class="col-md-6">
<input class="form-control" id="smtpHost" type="text" ng-model="realm.smtpServer.host" placeholder="{{:: 'smtp-host' | translate}}" required>
</div>
+ <div class="col-sm-4">
+ <a class="btn btn-primary" data-ng-click="testConnection()">{{:: 'test-connection' | translate}}</a>
+ </div>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="smtpPort">{{:: 'port' | translate}}</label>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html
index ab1d1e2..be74a2a 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html
@@ -65,9 +65,7 @@
<label class="col-md-2 control-label" for="supportedLocales" class="control-label two-lines">{{:: 'supported-locales' | translate}}</label>
<div class="col-md-6">
- <select ui-select2 id="supportedLocales" ng-model="realm.supportedLocales" data-placeholder="{{:: 'supported-locales.placeholder' | translate}}" ng-required="realm.internationalizationEnabled" multiple>
- <option ng-repeat="option in supportedLocalesOptions.tags" value="{{option}}">{{option}}</option>
- </select>
+ <input ui-select2="supportedLocalesOptions" id="supportedLocales" ng-model="realm.supportedLocales" data-placeholder="{{:: 'supported-locales.placeholder' | translate}}"/>
</div>
</div>
<div class="form-group" data-ng-show="realm.internationalizationEnabled">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html b/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html
index 45a5be6..17dcc05 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html
@@ -21,7 +21,7 @@
<tr ng-repeat="requiredAction in requiredActions | orderBy : 'name'" data-ng-show="requiredActions.length > 0">
<td>{{requiredAction.name}}</td>
<td><input type="checkbox" ng-model="requiredAction.enabled" ng-change="updateRequiredAction(requiredAction)" id="{{requiredAction.alias}}.enabled"></td>
- <td><input type="checkbox" ng-model="requiredAction.defaultAction" ng-change="updateRequiredAction(requiredAction)" id="{{requiredAction.alias}}.defaultAction"></td>
+ <td><input type="checkbox" ng-model="requiredAction.defaultAction" ng-change="updateRequiredAction(requiredAction)" ng-disabled="!requiredAction.enabled" ng-checked="requiredAction.enabled && requiredAction.defaultAction" id="{{requiredAction.alias}}.defaultAction"></td>
</tr>
<tr data-ng-show="requiredActions.length == 0">
<td>{{:: 'no-required-actions-configured' | translate}}</td>
@@ -31,4 +31,4 @@
</div>
-<kc-menu></kc-menu>
\ No newline at end of file
+<kc-menu></kc-menu>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
index 569e1fc..1fc707c 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
@@ -2,7 +2,7 @@
<kc-tabs-users></kc-tabs-users>
- <table class="table table-striped table-bordered">
+ <table class="table table-striped table-bordered" id="user-table">
<caption data-ng-show="users" class="hidden">{{:: 'table-of-realm-users' | translate}}</caption>
<thead>
<tr>
@@ -32,7 +32,7 @@
<th class="w-15">{{:: 'email' | translate}}</th>
<th class="w-15">{{:: 'last-name' | translate}}</th>
<th class="w-15">{{:: 'first-name' | translate}}</th>
- <th colspan="{{serverInfo.profileInfo.disabledFeatures.indexOf('IMPERSONATION') == -1 && access.impersonation == true ? '3' : '2'}}">{{:: 'actions' | translate}}</th>
+ <th colspan="{{serverInfo.featureEnabled('IMPERSONATION') && access.impersonation == true ? '3' : '2'}}">{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tfoot data-ng-show="users && (users.length >= query.max || query.first > 0)">
@@ -54,7 +54,7 @@
<td class="clip">{{user.lastName}}</td>
<td class="clip">{{user.firstName}}</td>
<td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/users/{{user.id}}">{{:: 'edit' | translate}}</td>
- <td data-ng-show="serverInfo.profileInfo.disabledFeatures.indexOf('IMPERSONATION') == -1 && access.impersonation" class="kc-action-cell" data-ng-click="impersonate(user.id)">{{:: 'impersonate' | translate}}</td>
+ <td data-ng-show="serverInfo.featureEnabled('IMPERSONATION') && access.impersonation" class="kc-action-cell" data-ng-click="impersonate(user.id)">{{:: 'impersonate' | translate}}</td>
<td data-ng-show="user.access.manage" class="kc-action-cell" data-ng-click="removeUser(user)">{{:: 'delete' | translate}}</td>
</tr>
<tr data-ng-show="!users || users.length == 0">
@@ -66,4 +66,4 @@
</table>
</div>
-<kc-menu></kc-menu>
\ No newline at end of file
+<kc-menu></kc-menu>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html
index 53b0a3d..cea1692 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html
@@ -50,7 +50,7 @@
<ul class="nav nav-pills nav-stacked">
<li data-ng-show="access.queryGroups" data-ng-class="(path[2] == 'groups'
|| path[2] == 'default-groups') && 'active'"><a href="#/realms/{{realm.realm}}/groups"><span class="pficon pficon-users"></span> {{:: 'groups' | translate}}</a></li>
- <li data-ng-show="access.queryUsers" data-ng-class="(path[2] == 'users') && 'active'"><a href="#/realms/{{realm.realm}}/users"><span class="pficon pficon-user"></span> {{:: 'users' | translate}}</a></li>
+ <li data-ng-show="access.queryUsers" data-ng-class="(path[2] == 'users' || path[2] == 'users-permissions') && 'active'"><a href="#/realms/{{realm.realm}}/users"><span class="pficon pficon-user"></span> {{:: 'users' | translate}}</a></li>
<li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'sessions') && 'active'"><a href="#/realms/{{realm.realm}}/sessions/realm"><i class="fa fa-clock-o"></i> {{:: 'sessions' | translate}}</a></li>
<li data-ng-show="access.viewEvents" data-ng-class="(path[2] == 'events'
|| path[2] == 'events-settings'
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
index e5b7c21..0c532a6 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
@@ -8,7 +8,10 @@
<ul class="nav nav-tabs" data-ng-hide="create && !path[4]">
<li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{:: 'settings' | translate}}</a></li>
- <li ng-class="{active: path[4] == 'credentials'}" data-ng-show="!disableCredentialsTab && !client.publicClient && client.protocol != 'saml'"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">{{:: 'credentials' | translate}}</a></li>
+ <li ng-class="{active: path[4] == 'credentials'}"
+ data-ng-show="!client.publicClient && client.protocol == 'openid-connect'">
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">{{:: 'credentials' | translate}}</a>
+ </li>
<li ng-class="{active: path[4] == 'saml'}" data-ng-show="client.protocol == 'saml' && (client.attributes['saml.client.signature'] == 'true' || client.attributes['saml.encrypt'] == 'true')"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/saml/keys">{{:: 'saml-keys' | translate}}</a></li>
<li ng-class="{active: path[4] == 'roles'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">{{:: 'roles' | translate}}</a></li>
<li ng-class="{active: path[4] == 'mappers'}" data-ng-show="!client.bearerOnly">
@@ -19,8 +22,13 @@
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/scope-mappings">{{:: 'scope' | translate}}</a>
<kc-tooltip>{{:: 'scope.tooltip' | translate}}</kc-tooltip>
</li>
- <li ng-class="{active: path[4] == 'authz'}" data-ng-show="serverInfo.profileInfo.disabledFeatures.indexOf('AUTHORIZATION') == -1 && !disableAuthorizationTab && client.authorizationServicesEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' | translate}}</a></li>
- <li ng-class="{active: path[4] == 'revocation'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">{{:: 'revocation' | translate}}</a></li>
+ <li ng-class="{active: path[4] == 'authz'}"
+ data-ng-show="serverInfo.featureEnabled('AUTHORIZATION') && !disableAuthorizationTab && client.authorizationServicesEnabled">
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' |
+ translate}}</a></li>
+ <li ng-class="{active: path[4] == 'revocation'}" data-ng-show="client.protocol != 'docker-v2'"><a
+ href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">{{:: 'revocation' | translate}}</a>
+ </li>
<!-- <li ng-class="{active: path[4] == 'identity-provider'}" data-ng-show="realm.identityFederationEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/identity-provider">Identity Provider</a></li> -->
<li ng-class="{active: path[4] == 'sessions'}" data-ng-show="!client.bearerOnly">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/sessions">{{:: 'sessions' | translate}}</a>
@@ -39,7 +47,7 @@
<kc-tooltip>{{:: 'installation.tooltip' | translate}}</kc-tooltip>
</li>
- <li ng-class="{active: path[4] == 'service-account-roles'}" data-ng-show="!disableServiceAccountRolesTab && client.serviceAccountsEnabled && !(client.bearerOnly || client.publicClient)">
+ <li ng-class="{active: path[4] == 'service-account-roles'}" data-ng-show="client.serviceAccountsEnabled && !(client.bearerOnly || client.publicClient)">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/service-account-roles">{{:: 'service-account-roles' | translate}}</a>
<kc-tooltip>{{:: 'service-account-roles.tooltip' | translate}}</kc-tooltip>
</li>
@@ -48,4 +56,4 @@
<kc-tooltip>{{:: 'manage-permissions-client.tooltip' | translate}}</kc-tooltip>
</li>
</ul>
-</div>
\ No newline at end of file
+</div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html
index 7bba535..6328c4e 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html
@@ -12,7 +12,7 @@
<a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/mappers">{{:: 'mappers' | translate}}</a>
<kc-tooltip>{{:: 'mappers.tooltip' | translate}}</kc-tooltip>
</li>
- <li ng-class="{active: path[4] == 'scope-mappings'}" >
+ <li ng-class="{active: path[4] == 'scope-mappings'}" data-ng-show="client.protocol != 'docker-v2'">
<a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/scope-mappings">{{:: 'scope' | translate}}</a>
<kc-tooltip>{{:: 'scope.tooltip' | translate}}</kc-tooltip>
</li>
diff --git a/themes/src/main/resources/theme/base/admin/theme.properties b/themes/src/main/resources/theme/base/admin/theme.properties
new file mode 100644
index 0000000..4bd8da4
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/theme.properties
@@ -0,0 +1,2 @@
+import=common/keycloak
+locales=ca,en,es,fr,it,ja,lt,no,pt-BR,ru,zh-CN
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/email/html/email-test.ftl b/themes/src/main/resources/theme/base/email/html/email-test.ftl
new file mode 100644
index 0000000..604415d
--- /dev/null
+++ b/themes/src/main/resources/theme/base/email/html/email-test.ftl
@@ -0,0 +1,5 @@
+<html>
+<body>
+${msg("emailTestBodyHtml",realmName)}
+</body>
+</html>
diff --git a/themes/src/main/resources/theme/base/email/html/executeActions.ftl b/themes/src/main/resources/theme/base/email/html/executeActions.ftl
index f75e10f..3af8d55 100755
--- a/themes/src/main/resources/theme/base/email/html/executeActions.ftl
+++ b/themes/src/main/resources/theme/base/email/html/executeActions.ftl
@@ -1,5 +1,8 @@
+<#assign requiredActionsText>
+<#if requiredActions??><#list requiredActions><b><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, </#items></b></#list><#else></#if>
+</#assign>
<html>
<body>
-${msg("executeActionsBodyHtml",link, linkExpiration, realmName)}
+${msg("executeActionsBodyHtml",link, linkExpiration, realmName, requiredActionsText)}
</body>
</html>
diff --git a/themes/src/main/resources/theme/base/email/messages/messages_en.properties b/themes/src/main/resources/theme/base/email/messages/messages_en.properties
index 9281bb7..5cb1b6e 100755
--- a/themes/src/main/resources/theme/base/email/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/email/messages/messages_en.properties
@@ -1,6 +1,9 @@
emailVerificationSubject=Verify email
emailVerificationBody=Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you didn''t create this account, just ignore this message.
emailVerificationBodyHtml=<p>Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address</p><p><a href="{0}">Link to e-mail address verification</a></p><p>This link will expire within {1} minutes.</p><p>If you didn''t create this account, just ignore this message.</p>
+emailTestSubject=[KEYCLOAK] - SMTP test message
+emailTestBody=This is a test message
+emailTestBodyHtml=<p>This is a test message</p>
identityProviderLinkSubject=Link {0}
identityProviderLinkBody=Someone wants to link your "{1}" account with "{0}" account of user {2} . If this was you, click the link below to link accounts\n\n{3}\n\nThis link will expire within {4} minutes.\n\nIf you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.
identityProviderLinkBodyHtml=<p>Someone wants to link your <b>{1}</b> account with <b>{0}</b> account of user {2} . If this was you, click the link below to link accounts</p><p><a href="{3}">Link to confirm account linking</a></p><p>This link will expire within {4} minutes.</p><p>If you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.</p>
@@ -8,8 +11,8 @@ passwordResetSubject=Reset password
passwordResetBody=Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.\n\n{0}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your credentials, just ignore this message and nothing will be changed.
passwordResetBodyHtml=<p>Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.</p><p><a href="{0}">Link to reset credentials</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your credentials, just ignore this message and nothing will be changed.</p>
executeActionsSubject=Update Your Account
-executeActionsBody=Your administrator has just requested that you update your {2} account. Click on the link below to start this process.\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you are unaware that your admin has requested this, just ignore this message and nothing will be changed.
-executeActionsBodyHtml=<p>Your administrator has just requested that you update your {2} account. Click on the link below to start this process.</p><p><a href="{0}">Link to account update</a></p><p>This link will expire within {1} minutes.</p><p>If you are unaware that your admin has requested this, just ignore this message and nothing will be changed.</p>
+executeActionsBody=Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you are unaware that your admin has requested this, just ignore this message and nothing will be changed.
+executeActionsBodyHtml=<p>Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.</p><p><a href="{0}">Link to account update</a></p><p>This link will expire within {1} minutes.</p><p>If you are unaware that your admin has requested this, just ignore this message and nothing will be changed.</p>
eventLoginErrorSubject=Login error
eventLoginErrorBody=A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an admin.
eventLoginErrorBodyHtml=<p>A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an admin.</p>
@@ -22,3 +25,9 @@ eventUpdatePasswordBodyHtml=<p>Your password was changed on {0} from {1}. If thi
eventUpdateTotpSubject=Update TOTP
eventUpdateTotpBody=TOTP was updated for your account on {0} from {1}. If this was not you, please contact an admin.
eventUpdateTotpBodyHtml=<p>TOTP was updated for your account on {0} from {1}. If this was not you, please contact an admin.</p>
+
+requiredAction.CONFIGURE_TOTP=Configure OTP
+requiredAction.terms_and_conditions=Terms and Conditions
+requiredAction.UPDATE_PASSWORD=Update Password
+requiredAction.UPDATE_PROFILE=Update Profile
+requiredAction.VERIFY_EMAIL=Verify Email
diff --git a/themes/src/main/resources/theme/base/email/messages/messages_zh_CN.properties b/themes/src/main/resources/theme/base/email/messages/messages_zh_CN.properties
new file mode 100644
index 0000000..e27ccda
--- /dev/null
+++ b/themes/src/main/resources/theme/base/email/messages/messages_zh_CN.properties
@@ -0,0 +1,24 @@
+emailVerificationSubject=验证电子邮件
+emailVerificationBody=用户使用当前电子邮件注册 {2} 账户。如是本人操作,请点击以下链接完成邮箱验证\n\n{0}\n\n这个链接会在 {1} 分钟后过期.\n\n如果您没有注册用户,请忽略这条消息。
+emailVerificationBodyHtml=<p>用户使用当前电子邮件注册 {2} 账户。如是本人操作,请点击以下链接完成邮箱验证</p><p><a href="{0}">{0}</a></p><p>这个链接会在 {1} 分钟后过期.</p><p>如果您没有注册用户,请忽略这条消息。</p>
+identityProviderLinkSubject=链接 {0}
+identityProviderLinkBody=有用户想要将账户 "{1}" 与用户{2}的账户"{0}" 做链接 . 如果是本人操作,请点击以下链接完成链接请求\n\n{3}\n\n这个链接会在 {4} 分钟后过期.\n\n如非本人操作,请忽略这条消息。如果您链接账户,您将可以通过{0}登录账户 {1}.
+identityProviderLinkBodyHtml=<p>有用户想要将账户 <b>{1}</b> 与用户{2} 的账户<b>{0}</b> 做链接 . 如果是本人操作,请点击以下链接完成链接请求</p><p><a href="{3}">{3}</a></p><p>这个链接会在 {4} 分钟后过期。</p><p>如非本人操作,请忽略这条消息。如果您链接账户,您将可以通过{0}登录账户 {1}.</p>
+passwordResetSubject=重置密码
+passwordResetBody=有用户要求修改账户 {2} 的密码.如是本人操作,请点击下面链接进行重置.\n\n{0}\n\n这个链接会在 {1} 分钟后过期.\n\n如果您不想重置您的密码,请忽略这条消息,密码不会改变。
+passwordResetBodyHtml=<p>有用户要求修改账户 {2} 的密码如是本人操作,请点击下面链接进行重置.</p><p><a href="{0}">{0}</a></p><p>这个链接会在 {1} 分钟后过期</p><p>如果您不想重置您的密码,请忽略这条消息,密码不会改变。</p>
+executeActionsSubject=更新您的账户
+executeActionsBody=您的管理员要求您更新账户 {2}. 点击以下链接开始更新\n\n{0}\n\n这个链接会在 {1} 分钟后失效.\n\n如果您不知道管理员要求更新账户信息,请忽略这条消息。账户信息不会修改。
+executeActionsBodyHtml=<p>您的管理员要求您更新账户{2}. 点击以下链接开始更新.</p><p><a href="{0}">{0}</a></p><p>这个链接会在 {1} 分钟后失效.</p><p>如果您不知道管理员要求更新账户信息,请忽略这条消息。账户信息不会修改。</p>
+eventLoginErrorSubject=登录错误
+eventLoginErrorBody=在{0} 由 {1}使用您的账户登录失败. 如果这不是您本人操作,请联系管理员.
+eventLoginErrorBodyHtml=<p>在{0} 由 {1}使用您的账户登录失败. 如果这不是您本人操作,请联系管理员.</p>
+eventRemoveTotpSubject=删除 TOTP
+eventRemoveTotpBody=TOTP在 {0} 由{1} 从您的账户中删除.如果这不是您本人操作,请联系管理员
+eventRemoveTotpBodyHtml=<p>TOTP在 {0} 由{1} 从您的账户中删除.如果这不是您本人操作,请联系管理员。</p>
+eventUpdatePasswordSubject=更新密码
+eventUpdatePasswordBody=您的密码在{0} 由 {1}更改. 如非本人操作,请联系管理员
+eventUpdatePasswordBodyHtml=<p>您的密码在{0} 由 {1}更改. 如非本人操作,请联系管理员</p>
+eventUpdateTotpSubject=更新 TOTP
+eventUpdateTotpBody=您账户的TOTP 配置在{0} 由 {1}更改. 如非本人操作,请联系管理员。
+eventUpdateTotpBodyHtml=<p>您账户的TOTP 配置在{0} 由 {1}更改. 如非本人操作,请联系管理员。</p>
diff --git a/themes/src/main/resources/theme/base/email/text/email-test.ftl b/themes/src/main/resources/theme/base/email/text/email-test.ftl
new file mode 100644
index 0000000..19942c7
--- /dev/null
+++ b/themes/src/main/resources/theme/base/email/text/email-test.ftl
@@ -0,0 +1 @@
+${msg("emailTestBody", realmName)}
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/email/text/executeActions.ftl b/themes/src/main/resources/theme/base/email/text/executeActions.ftl
index a33758f..39ce047 100755
--- a/themes/src/main/resources/theme/base/email/text/executeActions.ftl
+++ b/themes/src/main/resources/theme/base/email/text/executeActions.ftl
@@ -1 +1,4 @@
-${msg("executeActionsBody",link, linkExpiration, realmName)}
\ No newline at end of file
+<#assign requiredActionsText>
+<#if requiredActions??><#list requiredActions><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, </#items></#list><#else></#if>
+</#assign>
+${msg("executeActionsBody",link, linkExpiration, realmName, requiredActionsText)}
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/email/theme.properties b/themes/src/main/resources/theme/base/email/theme.properties
new file mode 100644
index 0000000..b9c3990
--- /dev/null
+++ b/themes/src/main/resources/theme/base/email/theme.properties
@@ -0,0 +1 @@
+locales=ca,de,en,es,fr,it,ja,lt,no,pt-BR,ru,zh-CN
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/login/info.ftl b/themes/src/main/resources/theme/base/login/info.ftl
index cb228d2..c9e197b 100755
--- a/themes/src/main/resources/theme/base/login/info.ftl
+++ b/themes/src/main/resources/theme/base/login/info.ftl
@@ -6,11 +6,13 @@
${message.summary}
<#elseif section = "form">
<div id="kc-info-message">
- <p class="instruction">${message.summary}</p>
+ <p class="instruction">${message.summary}<#if requiredActions??><#list requiredActions>: <b><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, </#items></b></#list><#else></#if></p>
<#if skipLink??>
<#else>
<#if pageRedirectUri??>
<p><a href="${pageRedirectUri}">${msg("backToApplication")}</a></p>
+ <#elseif actionUri??>
+ <p><a href="${actionUri}">${msg("proceedWithAction")}</a></p>
<#elseif client.baseUrl??>
<p><a href="${client.baseUrl}">${msg("backToApplication")}</a></p>
</#if>
diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
index cf26236..dbd0a3c 100755
--- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -130,6 +130,8 @@ accountDisabledMessage=Account is disabled, contact admin.
accountTemporarilyDisabledMessage=Account is temporarily disabled, contact admin or try again later.
expiredCodeMessage=Login timeout. Please login again.
expiredActionMessage=Action expired. Please continue with login now.
+expiredActionTokenNoSessionMessage=Action expired.
+expiredActionTokenSessionExistsMessage=Action expired. Please start again.
missingFirstNameMessage=Please specify first name.
missingLastNameMessage=Please specify last name.
@@ -217,6 +219,9 @@ identityProviderNotUniqueMessage=Realm supports multiple identity providers. Cou
emailVerifiedMessage=Your email address has been verified.
staleEmailVerificationLink=The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email?
identityProviderAlreadyLinkedMessage=Federated identity returned by {0} is already linked to another user.
+confirmAccountLinking=Confirm linking the account {0} of identity provider {1} with your account.
+confirmEmailAddressVerification=Confirm validity of e-mail address {0}.
+confirmExecutionOfActions=Perform the following action(s)
locale_ca=Catal\u00E0
locale_de=Deutsch
@@ -230,6 +235,8 @@ locale_pt_BR=Portugu\u00EAs (Brasil)
locale_pt-BR=Portugu\u00EAs (Brasil)
locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439
locale_lt=Lietuvi\u0173
+locale_zh-CN=\u4e2d\u6587\u7b80\u4f53
+locale_sv=Svenska
backToApplication=« Back to Application
missingParameterMessage=Missing parameters\: {0}
@@ -239,5 +246,12 @@ invalidParameterMessage=Invalid parameter\: {0}
alreadyLoggedIn=You are already logged in.
differentUserAuthenticated=You are already authenticated as different user ''{0}'' in this session. Please logout first.
brokerLinkingSessionExpired=Requested broker account linking, but current session is no longer valid.
+proceedWithAction=» Click here to proceed
+
+requiredAction.CONFIGURE_TOTP=Configure OTP
+requiredAction.terms_and_conditions=Terms and Conditions
+requiredAction.UPDATE_PASSWORD=Update Password
+requiredAction.UPDATE_PROFILE=Update Profile
+requiredAction.VERIFY_EMAIL=Verify Email
p3pPolicy=CP="This is not a P3P policy!"
diff --git a/themes/src/main/resources/theme/base/login/messages/messages_zh_CN.properties b/themes/src/main/resources/theme/base/login/messages/messages_zh_CN.properties
new file mode 100644
index 0000000..7a1a072
--- /dev/null
+++ b/themes/src/main/resources/theme/base/login/messages/messages_zh_CN.properties
@@ -0,0 +1,232 @@
+doLogIn=登录
+doRegister=注册
+doCancel=取消
+doSubmit=提交
+doYes=是
+doNo=否
+doContinue=继续
+doAccept=接受
+doDecline=拒绝
+doForgotPassword=忘记密码?
+doClickHere=点击这里
+doImpersonate=模拟
+kerberosNotConfigured=Kerberos 没有配置
+kerberosNotConfiguredTitle=Kerberos 没有配置
+bypassKerberosDetail=您没有通过Kerberos登录 或者您的浏览器没有设置Kerberos登录. 请点击继续通过其他途径登录。
+kerberosNotSetUp=Kerberos没有配置,您不可以登录
+registerWithTitle=用 {0} 注册
+registerWithTitleHtml={0}
+loginTitle=登录到 {0}
+loginTitleHtml={0}
+impersonateTitle={0} 模拟用户
+impersonateTitleHtml=<strong>{0}</strong>模拟用户</strong>
+realmChoice=域
+unknownUser=未知用户
+loginTotpTitle=手机验证者配置
+loginProfileTitle=更新账户信息
+loginTimeout=登录超时,请重新开始登录
+oauthGrantTitle=授权
+oauthGrantTitleHtml={0}
+errorTitle=很抱歉...
+errorTitleHtml=我们<strong>很抱歉</strong> ...
+emailVerifyTitle=验证电子邮件地址
+emailForgotTitle=忘记密码?
+updatePasswordTitle=更新密码
+codeSuccessTitle=成功码
+codeErrorTitle=错误码\: {0}
+
+termsTitle=条款
+termsTitleHtml=条款
+termsText=<p>需要确定的条款</p>
+
+recaptchaFailed=无效的验证码
+recaptchaNotConfigured=需要验证码,但是没有配置
+consentDenied=许可被拒绝。
+
+noAccount=新用户?
+username=用户名
+usernameOrEmail=用户名 或 电子邮箱地址
+firstName=名
+givenName=姓
+fullName=全名
+lastName=姓
+familyName=姓
+email=Email
+password=密码
+passwordConfirm=确认密码
+passwordNew=新密码
+passwordNewConfirm=新密码确认
+rememberMe=记住我
+authenticatorCode=一次性验证码
+address=地址
+street=街道
+locality=市
+region=省,自治区,直辖市
+postal_code=邮政编码
+country=国家
+emailVerified=电子邮件已验证
+gssDelegationCredential=GSS Delegation Credential
+
+loginTotpStep1=在手机安装 <a href="https://fedorahosted.org/freeotp/" target="_blank">FreeOTP</a> 或 Google Authenticator. 这两个应用可以在 <a href="https://play.google.com">Google Play</a> 和 Apple App Store找到.
+loginTotpStep2=打开应用扫描二维码或者输入一次性码
+loginTotpStep3=输入应用提供的一次性码点击提交完成设置
+loginTotpOneTime=一次性验证码
+
+oauthGrantRequest=您是否想要授予下列权限?
+inResource=in
+
+emailVerifyInstruction1=一封包含验证邮箱具体步骤的邮件已经发送到您的邮箱。
+emailVerifyInstruction2=邮箱没有收到验证码?
+emailVerifyInstruction3=重新发送电子邮件
+
+emailLinkIdpTitle=链接 {0}
+emailLinkIdp1=一封包含链接账户 {0} 和账户 {1} 到账户 {2} 的邮件已经发送到您的邮箱。
+emailLinkIdp2=邮箱没有收到验证码邮件?
+emailLinkIdp3=重新发送电子邮件
+
+backToLogin=« 回到登录
+
+emailInstruction=输入您的用户名和邮箱,我们会发送一封带有设置新密码步骤的邮件到您的邮箱。
+
+copyCodeInstruction=请复制这段验证码并粘贴到应用:
+
+personalInfo=个人信息\:
+role_admin=管理员
+role_realm-admin=域管理员
+role_create-realm=创建域
+role_create-client=创建客户
+role_view-realm=查看域
+role_view-users=查看用户
+role_view-applications=查看应用
+role_view-clients=查看客户
+role_view-events=查看时间
+role_view-identity-providers=查看身份提供者
+role_manage-realm=管理域
+role_manage-users=管理用户
+role_manage-applications=管理应用
+role_manage-identity-providers=管理身份提供者
+role_manage-clients=管理客户
+role_manage-events=管理事件
+role_view-profile=查看用户信息
+role_manage-account=管理账户
+role_read-token=读取 token
+role_offline-access=离线访问
+client_account=账户
+client_security-admin-console=安全管理控制台
+client_admin-cli=管理命令行工具
+client_realm-management=域管理
+client_broker=代理
+
+invalidUserMessage=无效的用户名或密码。
+invalidEmailMessage=无效的电子邮件地址
+accountDisabledMessage=账户被禁用,请联系管理员。
+accountTemporarilyDisabledMessage=账户被暂时禁用,请稍后再试或联系管理员。
+expiredCodeMessage=登录超时,请重新登陆。
+
+missingFirstNameMessage=请输入名
+missingLastNameMessage=请输入姓
+missingEmailMessage=请输入email.
+missingUsernameMessage=请输入用户名
+missingPasswordMessage=请输入密码
+missingTotpMessage=请输入验证码
+notMatchPasswordMessage=密码不匹配。
+
+invalidPasswordExistingMessage=无效的旧密码
+invalidPasswordConfirmMessage=确认密码不相同
+invalidTotpMessage=无效的验证码
+
+usernameExistsMessage=用户名已被占用
+emailExistsMessage=电子邮件已存在。
+
+federatedIdentityExistsMessage=用户 {0} {1} 已存在. 请登录账户管理界面链接账户.
+
+confirmLinkIdpTitle=账户已存在
+federatedIdentityConfirmLinkMessage=用户{0} {1} 已存在. 怎么继续?
+federatedIdentityConfirmReauthenticateMessage=以 {0} 登录来将 {1} 连接到您的账户
+confirmLinkIdpReviewProfile=审查您的信息
+confirmLinkIdpContinue=添加到已知账户
+
+configureTotpMessage=您需要设置验证码模块来激活您的账户
+updateProfileMessage=您需要更新您的简介来激活您的账户
+updatePasswordMessage=您需要更新您的密码来激活您的账户
+verifyEmailMessage=您需要验证您的电子邮箱来激活您的账户
+linkIdpMessage=您需要验证您的电子邮箱来连接到账户{0}.
+
+emailSentMessage=您很快会收到一封关于接下来操作的邮件。
+emailSendErrorMessage=无法发送邮件,请稍后再试
+
+accountUpdatedMessage=您的账户已经更新。
+accountPasswordUpdatedMessage=您的密码已经更新
+
+noAccessMessage=无权限
+
+invalidPasswordMinLengthMessage=无效的密码:最短长度 {0}.
+invalidPasswordMinDigitsMessage=无效的密码: 至少包含{0} 个数字
+invalidPasswordMinLowerCaseCharsMessage=无效的密码:至少包含 {0} 小写字母.
+invalidPasswordMinUpperCaseCharsMessage=无效的密码:至少包含 {0} 大写字母.
+invalidPasswordMinSpecialCharsMessage=无效的密码:至少包含 {0} 特殊字符.
+invalidPasswordNotUsernameMessage=无效的密码: 不能与用户名相同.
+invalidPasswordRegexPatternMessage=无效的密码: 无法与正则表达式匹配.
+invalidPasswordHistoryMessage=无效的密码: 不能与前 {0} 个旧密码相同.
+
+failedToProcessResponseMessage=无法处理回复
+httpsRequiredMessage=需要HTTPS
+realmNotEnabledMessage=域未启用
+invalidRequestMessage=非法的请求
+failedLogout=无法登出
+unknownLoginRequesterMessage=未知的登录请求发起方
+loginRequesterNotEnabledMessage=登录请求发起方为启用
+bearerOnlyMessage=Bearer-only 的应用允许通过浏览器登录
+standardFlowDisabledMessage=客户程序不允许发起指定返回类型的浏览器登录. 标准的登录流程已禁用。
+implicitFlowDisabledMessage=客户程序不允许发起指定返回类型的浏览器登录. 隐式的登录流程已禁用。
+invalidRedirectUriMessage=无效的跳转链接
+unsupportedNameIdFormatMessage=不支持的 nameID格式
+invalidRequesterMessage=无效的发起者
+registrationNotAllowedMessage=注册不允许
+resetCredentialNotAllowedMessage=不允许重置密码
+
+permissionNotApprovedMessage=许可没有批准
+noRelayStateInResponseMessage=身份提供者没有返回中继状态信息
+insufficientPermissionMessage=权限不足以链接新的身份
+couldNotProceedWithAuthenticationRequestMessage=无法与身份提供者处理认证请求
+couldNotObtainTokenMessage=未从身份提供者获得token
+unexpectedErrorRetrievingTokenMessage=从身份提供者获得Token时遇到未知错误
+unexpectedErrorHandlingResponseMessage=从身份提供者获得回复时遇到未知错误
+identityProviderAuthenticationFailedMessage=认证失败,无法通过身份提供者认证
+identityProviderDifferentUserMessage=认证为 {0}, 但期望认证为 {1}
+couldNotSendAuthenticationRequestMessage=无法向身份提供方发送认证请求
+unexpectedErrorHandlingRequestMessage=在处理发向认证提供方的请求时,出现未知错误。
+invalidAccessCodeMessage=无效的验证码
+sessionNotActiveMessage=会话不在活动状态
+invalidCodeMessage=发生错误,请重新通过应用登录
+identityProviderUnexpectedErrorMessage=在与认证提供者认证过程中发生未知错误
+identityProviderNotFoundMessage=无法找到认证提供方
+identityProviderLinkSuccess=您的账户已经将账户{0} 与账户 {1} 链接.
+staleCodeMessage=当前页面已无效,请到登录界面重新登录
+realmSupportsNoCredentialsMessage=域不支持特定类型密码
+identityProviderNotUniqueMessage=域支持通过多个身份提供者登录,不知道应用哪一种方式登录
+emailVerifiedMessage=您的电子邮箱已经验证。
+staleEmailVerificationLink=您点击的链接已无效。可能您已经验证过您的电子邮箱?
+
+locale_ca=Català
+locale_de=Deutsch
+locale_en=English
+locale_es=Español
+locale_fr=Français
+locale_it=Italian
+locale_ja=日本語
+locale_no=Norsk
+locale_pt_BR=Português (Brasil)
+locale_pt-BR=Português (Brasil)
+locale_ru=Русский
+locale_lt=Lietuvių
+locale_zh-CN=中文简体
+
+backToApplication=« 回到应用
+missingParameterMessage=缺少参数 \: {0}
+clientNotFoundMessage=客户端未找到
+clientDisabledMessage=客户端已禁用
+invalidParameterMessage=无效的参数 \: {0}
+alreadyLoggedIn=您已经登录
+
+p3pPolicy="This is not a P3P policy!"
diff --git a/themes/src/main/resources/theme/base/login/theme.properties b/themes/src/main/resources/theme/base/login/theme.properties
new file mode 100644
index 0000000..b9c3990
--- /dev/null
+++ b/themes/src/main/resources/theme/base/login/theme.properties
@@ -0,0 +1 @@
+locales=ca,de,en,es,fr,it,ja,lt,no,pt-BR,ru,zh-CN
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/keycloak/admin/theme.properties b/themes/src/main/resources/theme/keycloak/admin/theme.properties
index c930785..8519d89 100755
--- a/themes/src/main/resources/theme/keycloak/admin/theme.properties
+++ b/themes/src/main/resources/theme/keycloak/admin/theme.properties
@@ -1,3 +1,3 @@
parent=base
import=common/keycloak
-styles=lib/patternfly/css/patternfly.css lib/select2-3.4.1/select2.css css/styles.css lib/angular/treeview/css/angular.treeview.css
\ No newline at end of file
+styles=lib/patternfly/css/patternfly.css node_modules/select2/select2.css css/styles.css lib/angular/treeview/css/angular.treeview.css
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/keycloak/common/resources/package.json b/themes/src/main/resources/theme/keycloak/common/resources/package.json
new file mode 100644
index 0000000..3f5fa9e
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak/common/resources/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "keycloak-admin-console",
+ "version": "1.0.0",
+ "description": "Keycloak Admin Console",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "angular": "^1.6.4",
+ "angular-cookies": "^1.6.4",
+ "angular-loader": "^1.6.4",
+ "angular-resource": "^1.6.4",
+ "angular-route": "^1.6.4",
+ "angular-sanitize": "^1.6.4",
+ "angular-translate": "^2.15.1",
+ "angular-translate-loader-url": "^2.15.1",
+ "angular-treeview": "^0.1.5",
+ "angular-ui-select2": "^0.0.5",
+ "autofill-event": "^0.0.1",
+ "bootstrap": "^3.3.7",
+ "filesaver": "^0.0.13",
+ "font-awesome": "^4.7.0",
+ "jquery": "^3.2.1",
+ "ng-file-upload": "^12.2.13",
+ "select2": "3.5.1"
+ }
+}
diff --git a/themes/src/main/resources/theme/keycloak/common/resources/README.md b/themes/src/main/resources/theme/keycloak/common/resources/README.md
new file mode 100644
index 0000000..a5ef852
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak/common/resources/README.md
@@ -0,0 +1,24 @@
+Management of javascript libraries
+===================================================
+
+Javascript libraries under the *./lib* directory are not managed. These
+libraries are not available in the public npm repo and are thus checked into
+GitHub.
+
+Javascript libraries under *./node_modules* directory are managed with yarn.
+THEY SHOULD NOT BE CHECKED INTO GITHUB!
+
+Adding or Removing javascript libraries
+---------------------------------------
+To add/remove/update javascript libraries you should always use yarn so that
+the yarn.lock file will be updated. Then, just check in the modified version
+of package.json and yarn.lock. To do this, you should locally install
+nodejs/npm and yarn.
+
+Do not use *npm install --save*. If you try to update a dependency using
+package.json and fail to update yarn.lock, then the next build will fail.
+
+To locally install nodejs/npm and yarn, see:
+
+* [Install nodejs and npm](https://www.npmjs.com/get-npm)
+* [Install yarn](https://yarnpkg.com/lang/en/docs/install/)
diff --git a/themes/src/main/resources/theme/keycloak/common/resources/yarn.lock b/themes/src/main/resources/theme/keycloak/common/resources/yarn.lock
new file mode 100644
index 0000000..9158a5b
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak/common/resources/yarn.lock
@@ -0,0 +1,92 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+angular-cookies@^1.6.4:
+ version "1.6.4"
+ resolved "https://registry.yarnpkg.com/angular-cookies/-/angular-cookies-1.6.4.tgz#c28f3f6aac7a9826c1e45f1d6807240036e5b26d"
+
+angular-loader@^1.6.4:
+ version "1.6.4"
+ resolved "https://registry.yarnpkg.com/angular-loader/-/angular-loader-1.6.4.tgz#c202b9dd233b11e66c802f7716c5d82ad249bb42"
+
+angular-resource@^1.6.4:
+ version "1.6.4"
+ resolved "https://registry.yarnpkg.com/angular-resource/-/angular-resource-1.6.4.tgz#bcb83688b0a7d3402fde58dc7f4881383a6c0ebb"
+
+angular-route@^1.6.4:
+ version "1.6.4"
+ resolved "https://registry.yarnpkg.com/angular-route/-/angular-route-1.6.4.tgz#7bb216fcda746a1b8c452054b05900a7074ecc62"
+
+angular-sanitize@^1.6.4:
+ version "1.6.4"
+ resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.6.4.tgz#60a37ea96fb0d4a322a3ccb64ee4a5cf3b154f0c"
+
+angular-translate-loader-url@^2.15.1:
+ version "2.15.1"
+ resolved "https://registry.yarnpkg.com/angular-translate-loader-url/-/angular-translate-loader-url-2.15.1.tgz#31d6785a59d813fe7d8a1990d8be16864ff59e24"
+ dependencies:
+ angular-translate "~2.15.1"
+
+angular-translate@^2.15.1, angular-translate@~2.15.1:
+ version "2.15.1"
+ resolved "https://registry.yarnpkg.com/angular-translate/-/angular-translate-2.15.1.tgz#920f7d2b877819e1c0fa881781b9b675f36480ce"
+ dependencies:
+ angular ">=1.2.26 <=1.6"
+
+angular-treeview@^0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/angular-treeview/-/angular-treeview-0.1.5.tgz#ec797d4d001b20172c983e65d855ebcd8152b4fa"
+
+angular-ui-select2@^0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/angular-ui-select2/-/angular-ui-select2-0.0.5.tgz#15e7643afd69ca9063d405eb3be2f95dd5ec87f5"
+
+"angular@>=1.2.26 <=1.6", angular@^1.6.4:
+ version "1.6.4"
+ resolved "https://registry.yarnpkg.com/angular/-/angular-1.6.4.tgz#03b7b15c01a0802d7e2cf593240e604054dc77fb"
+
+autofill-event@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/autofill-event/-/autofill-event-0.0.1.tgz#c382cf989b21b61ff4a12b3597e1943471d3cf7a"
+
+bootstrap@^3.3.7:
+ version "3.3.7"
+ resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.3.7.tgz#5a389394549f23330875a3b150656574f8a9eb71"
+
+filesaver@^0.0.13:
+ version "0.0.13"
+ resolved "https://registry.yarnpkg.com/filesaver/-/filesaver-0.0.13.tgz#fa9b2ac1371d436fe5edc9285ed998d1e2782bee"
+ dependencies:
+ mkdirp "^0.5.0"
+ safename "0.0.4"
+
+font-awesome@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133"
+
+jquery@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+mkdirp@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
+ng-file-upload@^12.2.13:
+ version "12.2.13"
+ resolved "https://registry.yarnpkg.com/ng-file-upload/-/ng-file-upload-12.2.13.tgz#01800f3872e526f95310f8477e99e4f12d0d8d14"
+
+safename@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/safename/-/safename-0.0.4.tgz#b82c3b6db70d943a0582f9052fbfbfebbb589af5"
+
+select2@3.5.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/select2/-/select2-3.5.1.tgz#f2819489bbc65fd6d328be72bbe2b95dd7e87cfe"
diff --git a/themes/src/main/resources-community/theme/base/account/messages/messages_no.properties b/themes/src/main/resources-community/theme/base/account/messages/messages_no.properties
index 0bc2c19..948ff6c 100644
--- a/themes/src/main/resources-community/theme/base/account/messages/messages_no.properties
+++ b/themes/src/main/resources-community/theme/base/account/messages/messages_no.properties
@@ -161,3 +161,4 @@ locale_ja=\u65E5\u672C\u8A9E
locale_no=Norsk
locale_pt-BR=Portugu\u00EAs (Brasil)
locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439
+locale_zh-CN=\u4e2d\u6587\u7b80\u4f53
diff --git a/themes/src/main/resources-community/theme/base/account/messages/messages_sv.properties b/themes/src/main/resources-community/theme/base/account/messages/messages_sv.properties
index 8c83abb..cc134cd 100755
--- a/themes/src/main/resources-community/theme/base/account/messages/messages_sv.properties
+++ b/themes/src/main/resources-community/theme/base/account/messages/messages_sv.properties
@@ -2,14 +2,14 @@
doSave=Spara
doCancel=Avbryt
doLogOutAllSessions=Logga ut från samtliga sessioner
-doRemove=Ta Bort
-doAdd=Lägg Till
-doSignOut=Logga Ut
+doRemove=Ta bort
+doAdd=Lägg till
+doSignOut=Logga ut
-editAccountHtmlTitle=Redigera Konto
+editAccountHtmlTitle=Redigera konto
federatedIdentitiesHtmlTitle=Federerade identiteter
-accountLogHtmlTitle=Kontoslogg
-changePasswordHtmlTitle=Byt Lösenord
+accountLogHtmlTitle=Kontologg
+changePasswordHtmlTitle=Byt lösenord
sessionsHtmlTitle=Sessioner
accountManagementTitle=Kontohantering för Keycloak
authenticatorTitle=Autentiserare
@@ -21,40 +21,40 @@ firstName=Förnamn
lastName=Efternamn
password=Lösenord
passwordConfirm=Bekräftelse
-passwordNew=Nytt Lösenord
+passwordNew=Nytt lösenord
username=Användarnamn
address=Adress
street=Gata
locality=Postort
-region=Stat, Provins, eller Region
+region=Stat, Provins eller Region
postal_code=Postnummer
country=Land
emailVerified=E-post verifierad
gssDelegationCredential=GSS Delegation Credential
role_admin=Administratör
-role_realm-admin=Realm Administratör
+role_realm-admin=Realm-administratör
role_create-realm=Skapa realm
role_view-realm=Visa realm
role_view-users=Visa användare
role_view-applications=Visa applikationer
role_view-clients=Visa klienter
role_view-events=Visa event
-role_view-identity-providers=Visa identity providers
+role_view-identity-providers=Visa identitetsleverantörer
role_manage-realm=Hantera realm
role_manage-users=Hantera användare
role_manage-applications=Hantera applikationer
-role_manage-identity-providers=Hantera identity providers
+role_manage-identity-providers=Hantera identitetsleverantörer
role_manage-clients=Hantera klienter
role_manage-events=Hantera event
role_view-profile=Visa profil
role_manage-account=Hantera konto
role_read-token=Läs element
-role_offline-access=Åtkomst Offline
+role_offline-access=Åtkomst offline
role_uma_authorization=Erhåll tillstånd
client_account=Konto
client_security-admin-console=Säkerhetsadministratörskonsol
-client_admin-cli=Administratörs CLI
+client_admin-cli=Administratörs-CLI
client_realm-management=Realmhantering
@@ -71,7 +71,7 @@ client=Klient
clients=Klienter
details=Detaljer
started=Startade
-lastAccess=Senast Åtkomst
+lastAccess=Senast åtkomst
expires=Upphör
applications=Applikationer
@@ -82,20 +82,20 @@ sessions=Sessioner
log=Logg
application=Applikation
-availablePermissions=Tillgängliga Tillstånd
-grantedPermissions=Beviljade Tillstånd
-grantedPersonalInfo=Medgiven Personlig Information
-additionalGrants=Ytterligare Medgivanden
+availablePermissions=Tillgängliga rättigheter
+grantedPermissions=Beviljade rättigheter
+grantedPersonalInfo=Medgiven personlig information
+additionalGrants=Ytterligare medgivanden
action=Åtgärd
-inResource=in
-fullAccess=Fullständig Åtkomst
-offlineToken=Offline Token
-revoke=Upphäv Tillstånd
+inResource=i
+fullAccess=Fullständig åtkomst
+offlineToken=Offline token
+revoke=Upphäv rättighet
-configureAuthenticators=Ändrade Autentiserare
+configureAuthenticators=Konfigurerade autentiserare
mobile=Mobil
totpStep1=Installera <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> eller Google Authenticator på din enhet. Båda applikationerna finns tillgängliga på <a href="https://play.google.com">Google Play</a> och Apple App Store.
-totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeLn.
+totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln.
totpStep3=Fyll i engångskoden som tillhandahålls av applikationen och klicka på Spara för att avsluta inställningarna.
missingUsernameMessage=Vänligen ange användarnamn.
@@ -106,10 +106,10 @@ missingEmailMessage=Vänligen ange e-post.
missingPasswordMessage=Vänligen ange lösenord.
notMatchPasswordMessage=Lösenorden matchar inte.
-missingTotpMessage=Vänligen ange autentiserarekoden.
+missingTotpMessage=Vänligen ange autentiseringskoden.
invalidPasswordExistingMessage=Det nuvarande lösenordet är ogiltigt.
invalidPasswordConfirmMessage=Lösenordsbekräftelsen matchar inte.
-invalidTotpMessage=Autentiserarekoden är ogiltig.
+invalidTotpMessage=Autentiseringskoden är ogiltig.
usernameExistsMessage=Användarnamnet finns redan.
emailExistsMessage=E-posten finns redan.
@@ -120,20 +120,20 @@ readOnlyPasswordMessage=Du kan inte uppdatera ditt lösenord eftersom ditt konto
successTotpMessage=Mobilautentiseraren är inställd.
successTotpRemovedMessage=Mobilautentiseraren är borttagen.
-successGrantRevokedMessage=Upphävandet av tillståndet lyckades.
+successGrantRevokedMessage=Upphävandet av rättigheten lyckades.
accountUpdatedMessage=Ditt konto har uppdaterats.
accountPasswordUpdatedMessage=Ditt lösenord har uppdaterats.
-missingIdentityProviderMessage=Identity provider är inte angiven.
+missingIdentityProviderMessage=Identitetsleverantör är inte angiven.
invalidFederatedIdentityActionMessage=Åtgärden är ogiltig eller saknas.
-identityProviderNotFoundMessage=Angiven identity provider hittas inte.
+identityProviderNotFoundMessage=Angiven identitetsleverantör hittas inte.
federatedIdentityLinkNotActiveMessage=Den här identiteten är inte längre aktiv.
-federatedIdentityRemovingLastProviderMessage=Du kan inte ta bort senaste federerade identiteten eftersom du inte har lösenordet.
-identityProviderRedirectErrorMessage=Misslyckades med att omdirigera till identity provider.
-identityProviderRemovedMessage=Borttaginingen av Identity provider lyckades.
+federatedIdentityRemovingLastProviderMessage=Du kan inte ta bort senaste federerade identiteten eftersom du inte har ett lösenord.
+identityProviderRedirectErrorMessage=Misslyckades med att omdirigera till identitetsleverantör.
+identityProviderRemovedMessage=Borttagningen av identitetsleverantören lyckades.
identityProviderAlreadyLinkedMessage=Den federerade identiteten som returnerades av {0} är redan länkad till en annan användare.
-staleCodeAccountMessage=Sidan har redan upphört. Vänligen försök igen.
+staleCodeAccountMessage=Sidan har upphört att gälla. Vänligen försök igen.
consentDenied=Samtycket förnekades.
accountDisabledMessage=Kontot är inaktiverat, kontakta administratör.
@@ -145,6 +145,6 @@ invalidPasswordMinDigitsMessage=Ogiltigt lösenord: måste innehålla minst {0}
invalidPasswordMinUpperCaseCharsMessage=Ogiltigt lösenord: måste innehålla minst {0} stora bokstäver.
invalidPasswordMinSpecialCharsMessage=Ogiltigt lösenord: måste innehålla minst {0} specialtecken.
invalidPasswordNotUsernameMessage=Ogiltigt lösenord: Får inte vara samma som användarnamnet.
-invalidPasswordRegexPatternMessage=Ogiltigt lösenord: matchar inte regex mönstret(en).
+invalidPasswordRegexPatternMessage=Ogiltigt lösenord: matchar inte kravet för lösenordsmönster.
invalidPasswordHistoryMessage=Ogiltigt lösenord: Får inte vara samma som de senaste {0} lösenorden.
invalidPasswordGenericMessage=Ogiltigt lösenord: Det nya lösenordet stämmer inte med lösenordspolicyn.
\ No newline at end of file
diff --git a/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_es.properties b/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_es.properties
index fb17ff6..f5cab83 100755
--- a/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_es.properties
+++ b/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_es.properties
@@ -464,3 +464,4 @@ identity-provider-mappers=Asignadores de proveedores de identidad (IDP)
create-identity-provider-mapper=Crear asignador de proveedor de identidad (IDP)
add-identity-provider-mapper=A\u00F1adir asignador de proveedor de identidad
client.description.tooltip=Indica la descripci\u00F3n del cliente. Por ejemplo ''My Client for TimeSheets''. Tambi\u00E9n soporta claves para valores localizados. Por ejemplo: ${my_client_description}
+content-type-options=
diff --git a/themes/src/main/resources-community/theme/base/admin/theme.properties b/themes/src/main/resources-community/theme/base/admin/theme.properties
index 46cee85..4bd8da4 100644
--- a/themes/src/main/resources-community/theme/base/admin/theme.properties
+++ b/themes/src/main/resources-community/theme/base/admin/theme.properties
@@ -1,2 +1,2 @@
import=common/keycloak
-locales=ca,de,en,es,fr,it,ja,lt,no,pt-BR,ru
\ No newline at end of file
+locales=ca,en,es,fr,it,ja,lt,no,pt-BR,ru,zh-CN
\ No newline at end of file
diff --git a/themes/src/main/resources-community/theme/base/email/messages/messages_de.properties b/themes/src/main/resources-community/theme/base/email/messages/messages_de.properties
index f8c7145..9068321 100755
--- a/themes/src/main/resources-community/theme/base/email/messages/messages_de.properties
+++ b/themes/src/main/resources-community/theme/base/email/messages/messages_de.properties
@@ -1,7 +1,15 @@
emailVerificationSubject=E-Mail verifizieren
-passwordResetSubject=Passwort zur\u00FCcksetzen
emailVerificationBody=Jemand hat ein {2} Konto mit dieser E-Mail Adresse erstellt. Falls Sie das waren, dann klicken Sie auf den Link, um die E-Mail Adresse zu verifizieren.\n\n{0}\n\nDieser Link wird in {1} Minuten ablaufen.\n\nFalls Sie dieses Konto nicht erstellt haben, dann k\u00F6nnen sie diese Nachricht ignorieren.
emailVerificationBodyHtml=<p>Jemand hat ein {2} Konto mit dieser E-Mail Adresse erstellt. Falls das Sie waren, klicken Sie auf den Link, um die E-Mail Adresse zu verifizieren.</p><p><a href="{0}">{0}</a></p><p>Dieser Link wird in {1} Minuten ablaufen.</p><p>Falls Sie dieses Konto nicht erstellt haben, dann k\u00F6nnen sie diese Nachricht ignorieren.</p>
+identityProviderLinkSubject=Link {0}
+identityProviderLinkBody=Es wurde beantragt Ihren Account {1} mit dem Account {0} von Benutzer {2} zu verlinken. Sollten Sie dies beantragt haben, klicken Sie auf den unten stehenden Link.\n\n{3}\n\n Die G\u00FCltigkeit des Links wird in {4} Minuten verfallen.\n\nSollten Sie Ihren Account nicht verlinken wollen, ignorieren Sie diese Nachricht. Wenn Sie die Accounts verlinken wird ein Login auf {1} \u00FCber {0} erm\u00F6glicht.
+identityProviderLinkBodyHtml=<p>Es wurde beantragt Ihren Account {1} mit dem Account {0} von Benutzer {2} zu verlinken. Sollten Sie dies beantragt haben, klicken Sie auf den unten stehenden Link.</p><p><a href="{3}">Link zur Best\u00E4tigung der Kontoverkn\u00FCpfung</a></p><p>Die G\u00FCltigkeit des Links wird in {4} Minuten verfallen.</p><p>Sollten Sie Ihren Account nicht verlinken wollen, ignorieren Sie diese Nachricht. Wenn Sie die Accounts verlinken wird ein Login auf {1} \u00FCber {0} erm\u00F6glicht.</p>
+passwordResetSubject=Passwort zur\u00FCcksetzen
+passwordResetBody=Es wurde eine \u00C4nderung der Anmeldeinformationen f\u00FCr Ihren Account {2} angefordert. Wenn Sie diese \u00C4nderung beantragt haben, klicken Sie auf den unten stehenden Link.\n\n{0}\n\nDie G\u00FCltigkeit des Links wird in {1} Minuten verfallen.\n\nSollten Sie keine \u00C4nderung vollziehen wollen k\u00F6nnen Sie diese Nachricht ignorieren und an Ihrem Account wird nichts ge\u00E4ndert.
+passwordResetBodyHtml=<p>Es wurde eine \u00C4nderung der Anmeldeinformationen f\u00FCr Ihren Account {2} angefordert. Wenn Sie diese \u00C4nderung beantragt haben, klicken Sie auf den unten stehenden Link.</p><p><a href="{0}">Link zum Zur\u00FCcksetzen von Anmeldeinformationen</a></p><p>Die G\u00FCltigkeit des Links wird in {1} Minuten verfallen.</p><p>Sollten Sie keine \u00C4nderung vollziehen wollen k\u00F6nnen Sie diese Nachricht ignorieren und an Ihrem Account wird nichts ge\u00E4ndert.</p>
+executeActionsSubject=Aktualisieren Sie Ihr Konto
+executeActionsBody=Ihr Administrator hat Sie aufgefordert Ihren Account {2} zu aktualisieren. Klicken Sie auf den unten stehenden Link um den Prozess zu starten.\n\n{0}\n\nDie G\u00FCltigkeit des Links wird in {1} Minuten verfallen.\n\nSollten Sie sich dieser Aufforderung nicht bewusst sein, ignorieren Sie diese Nachricht und Ihr Account bleibt unver\u00E4ndert.
+executeActionsBodyHtml=<p>Ihr Administrator hat Sie aufgefordert Ihren Account {2} zu aktualisieren. Klicken Sie auf den unten stehenden Link um den Prozess zu starten.</p><p><a href="{0}">Link zum Account-Update</a></p><p>Die G\u00FCltigkeit des Links wird in {1} Minuten verfallen.</p><p>Sollten Sie sich dieser Aufforderung nicht bewusst sein, ignorieren Sie diese Nachricht und Ihr Account bleibt unver\u00E4ndert.</p>
eventLoginErrorSubject=Fehlgeschlagene Anmeldung
eventLoginErrorBody=Jemand hat um {0} von {1} versucht, sich mit ihrem Konto anzumelden. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.
eventLoginErrorBodyHtml=<p>Jemand hat um {0} von {1} versucht, sich mit ihrem Konto anzumelden. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>
diff --git a/themes/src/main/resources-community/theme/base/email/messages/messages_ja.properties b/themes/src/main/resources-community/theme/base/email/messages/messages_ja.properties
index 087170a..a60ffe3 100644
--- a/themes/src/main/resources-community/theme/base/email/messages/messages_ja.properties
+++ b/themes/src/main/resources-community/theme/base/email/messages/messages_ja.properties
@@ -1,16 +1,16 @@
# encoding: utf-8
emailVerificationSubject=Eメールの確認
emailVerificationBody=このメールアドレスで {2} アカウントが作成されたました。以下のリンクをクリックしてメールアドレスの確認を完了してください。\n\n{0}\n\nこのリンクは {1} 分間だけ有効です。\n\nもしこのアカウントの作成に心当たりがない場合は、このメールを無視してください。
-emailVerificationBodyHtml=<p>このメールアドレスで {2} アカウントが作成されました。以下のリンクをクリックしてメールアドレスの確認を完了してください。</p><p><a href="{0}">{0}</a></p><p>このリンクは {1} 分間だけ有効です。</p><p>もしこのアカウントの作成に心当たりがない場合は、このメールを無視してください。</p>
+emailVerificationBodyHtml=<p>このメールアドレスで {2} アカウントが作成されました。以下のリンクをクリックしてメールアドレスの確認を完了してください。</p><p><a href="{0}">メールアドレスの確認</a></p><p>このリンクは {1} 分間だけ有効です。</p><p>もしこのアカウントの作成に心当たりがない場合は、このメールを無視してください。</p>
identityProviderLinkSubject=リンク {0}
identityProviderLinkBody=あなたの "{1}" アカウントと {2} ユーザーの "{0}" アカウントのリンクが要求されました。以下のリンクをクリックしてアカウントのリンクを行ってください。\n\n{3}\n\nこのリンクは {4} 分間だけ有効です。\n\nもしアカウントのリンクを行わない場合は、このメッセージを無視してください。アカウントのリンクを行うことで、{0} 経由で {1} にログインすることができるようになります。
-identityProviderLinkBodyHtml=<p>あなたの <b>{1}</b> アカウントと {2} ユーザーの <b>{0}</b> アカウントのリンクが要求されました。以下のリンクをクリックしてアカウントのリンクを行ってください。</p><p><a href="{3}">{3}</a></p><p>このリンクは {4} 分間だけ有効です。</p><p>もしアカウントのリンクを行わない場合は、このメッセージを無視してください。アカウントのリンクを行うことで、{0} 経由で {1} にログインすることができるようになります。</p>
+identityProviderLinkBodyHtml=<p>あなたの <b>{1}</b> アカウントと {2} ユーザーの <b>{0}</b> アカウントのリンクが要求されました。以下のリンクをクリックしてアカウントのリンクを行ってください。</p><p><a href="{3}">アカウントリンクの確認</a></p><p>このリンクは {4} 分間だけ有効です。</p><p>もしアカウントのリンクを行わない場合は、このメッセージを無視してください。アカウントのリンクを行うことで、{0} 経由で {1} にログインすることができるようになります。</p>
passwordResetSubject=パスワードのリセット
-passwordResetBody=あなたの {2} アカウントのクレデンシャルの変更が要求されています。以下のリンクをクリックしてクレデンシャルのリセットを行ってください。\n\n{0}\n\nこのリンクとコードは {1} 分間だけ有効です。\n\nもしクレデンシャルのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。
-passwordResetBodyHtml=<p>あなたの {2} アカウントのクレデンシャルの変更が要求されています。以下のリンクをクリックしてクレデンシャルのリセットを行ってください。</p><p><a href="{0}">{0}</a></p><p>このリンクとコードは {1} 分間だけ有効です。</p><p>もしクレデンシャルのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。</p>
+passwordResetBody=あなたの {2} アカウントのパスワードの変更が要求されています。以下のリンクをクリックしてパスワードのリセットを行ってください。\n\n{0}\n\nこのリンクは {1} 分間だけ有効です。\n\nもしパスワードのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。
+passwordResetBodyHtml=<p>あなたの {2} アカウントのパスワードの変更が要求されています。以下のリンクをクリックしてパスワードのリセットを行ってください。</p><p><a href="{0}">パスワードのリセット</a></p><p>このリンクは {1} 分間だけ有効です。</p><p>もしパスワードのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。</p>
executeActionsSubject=アカウントの更新
executeActionsBody=管理者よりあなたの {2} アカウントの更新が要求されています。以下のリンクをクリックしてこのプロセスを開始してください。\n\n{0}\n\nこのリンクは {1} 分間だけ有効です。\n\n管理者からのこの変更要求についてご存知ない場合は、このメッセージを無視してください。何も変更されません。
-executeActionsBodyHtml=<p>管理者よりあなたの {2} アカウントの更新が要求されています。以下のリンクをクリックしてこのプロセスを開始してください。</p><p><a href="{0}">{0}</a></p><p>このリンクは {1} 分間だけ有効です。</p><p>管理者からのこの変更要求についてご存知ない場合は、このメッセージを無視してください。何も変更されません。</p>
+executeActionsBodyHtml=<p>管理者よりあなたの {2} アカウントの更新が要求されています。以下のリンクをクリックしてこのプロセスを開始してください。</p><p><a href="{0}">アカウントの更新</a></p><p>このリンクは {1} 分間だけ有効です。</p><p>管理者からのこの変更要求についてご存知ない場合は、このメッセージを無視してください。何も変更されません。</p>
eventLoginErrorSubject=ログインエラー
eventLoginErrorBody={0} に {1} からのログイン失敗があなたのアカウントで検出されました。心当たりがない場合は、管理者に連絡してください。
eventLoginErrorBodyHtml=<p>{0} に {1} からのログイン失敗があなたのアカウントで検出されました。心当たりがない場合は管理者に連絡してください。</p>
diff --git a/themes/src/main/resources-community/theme/base/email/messages/messages_sv.properties b/themes/src/main/resources-community/theme/base/email/messages/messages_sv.properties
index 5b5ac6d..a5ffbf4 100755
--- a/themes/src/main/resources-community/theme/base/email/messages/messages_sv.properties
+++ b/themes/src/main/resources-community/theme/base/email/messages/messages_sv.properties
@@ -6,9 +6,9 @@ identityProviderLinkSubject=Länk {0}
identityProviderLinkBody=Någon vill länka ditt "{1}" konto med "{0}" kontot tillhörande användaren {2} . Om det var du, klicka då på länken nedan för att länka kontona\n\n{3}\n\nDen här länken kommer att upphöra inom {4} minuter.\n\nOm du inte vill länka kontot, ignorera i så fall det här meddelandet. Om du länkar kontona, så kan du logga in till {1} genom {0}.
identityProviderLinkBodyHtml=<p>Någon vill länka ditt <b>{1}</b> konto med <b>{0}</b> kontot tillhörande användaren {2} . Om det var du, klicka då på länken nedan för att länka kontona</p><p><a href="{3}">{3}</a></p><p>Den här länken kommer att upphöra inom {4} minuter.</p><p>Om du inte vill länka kontot, ignorera i så fall det här meddelandet. Om du länkar kontona, så kan du logga in till {1} genom {0}.</p>
passwordResetSubject=Återställ lösenord
-passwordResetBody=Någon har precis bett om att ändra ditt {2} kontos användaruppgifter. Om det var du, klicka då på länken nedan för att återställa dem.\n\n{0}\n\nDen här länken och koden kommer att upphöra inom {1} minuter.\n\nOm du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.
-passwordResetBodyHtml=<p>Någon har precis bett om att ändra ditt {2} kontos användaruppgifter. Om det var du, klicka då på länken nedan för att återställa dem.</p><p><a href="{0}">{0}</a></p><p>Den här länken och koden kommer att upphöra inom {1} minuter.</p><p>Om du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.</p>
-executeActionsSubject=Uppdatera Ditt Konto
+passwordResetBody=Någon har precis bett om att ändra användaruppgifter för ditt konto {2}. Om det var du, klicka då på länken nedan för att återställa dem.\n\n{0}\n\nDen här länken och koden kommer att upphöra inom {1} minuter.\n\nOm du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.
+passwordResetBodyHtml=<p>Någon har precis bett om att ändra användaruppgifter för ditt konto {2}. Om det var du, klicka då på länken nedan för att återställa dem.</p><p><a href="{0}">{0}</a></p><p>Den här länken och koden kommer att upphöra inom {1} minuter.</p><p>Om du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.</p>
+executeActionsSubject=Uppdatera ditt konto
executeActionsBody=Din administratör har precis bett om att du skall uppdatera ditt {2} konto. Klicka på länken för att påbörja processen.\n\n{0}\n\nDen här länken kommer att upphöra inom {1} minuter.\n\nOm du är omedveten om att din administratör har bett om detta, ignorera i så fall det här meddelandet så kommer inget att ändras.
executeActionsBodyHtml=<p>Din administratör har precis bett om att du skall uppdatera ditt {2} konto. Klicka på länken för att påbörja processen.</p><p><a href="{0}">{0}</a></p><p>Den här länken kommer att upphöra inom {1} minuter.</p><p>Om du är omedveten om att din administratör har bett om detta, ignorera i så fall det här meddelandet så kommer inget att ändras.</p>
eventLoginErrorSubject=Inloggningsfel
diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_no.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_no.properties
index efa23a3..1f1a9d9 100644
--- a/themes/src/main/resources-community/theme/base/login/messages/messages_no.properties
+++ b/themes/src/main/resources-community/theme/base/login/messages/messages_no.properties
@@ -218,6 +218,7 @@ locale_no=Norsk
locale_pt_BR=Portugu\u00EAs (Brasil)
locale_pt-BR=Portugu\u00EAs (Brasil)
locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439
+locale_zh-CN=\u4e2d\u6587\u7b80\u4f53
backToApplication=« Tilbake til applikasjonen
missingParameterMessage=Manglende parameter\: {0}
diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_sv.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_sv.properties
index c671ee3..c383703 100755
--- a/themes/src/main/resources-community/theme/base/login/messages/messages_sv.properties
+++ b/themes/src/main/resources-community/theme/base/login/messages/messages_sv.properties
@@ -1,5 +1,5 @@
# encoding: utf-8
-doLogIn=Logga In
+doLogIn=Logga in
doRegister=Registrera
doCancel=Avbryt
doSubmit=Skicka
@@ -8,30 +8,30 @@ doNo=Nej
doContinue=Fortsätt
doAccept=Acceptera
doDecline=Avböj
-doForgotPassword=Glömt Lösenord?
+doForgotPassword=Glömt lösenord?
doClickHere=Klicka här
doImpersonate=Imitera
-kerberosNotConfigured=Kerberos är Inte Konfigurerad
-kerberosNotConfiguredTitle=Kerberos är Inte Konfigurerad
-bypassKerberosDetail=Antingen så är du inte inloggad via Kerberos eller så är inte din webläsare inställd för Kerberosinloggning. Vänligen klicka på fortsätt för att logga in på annat sätt.
+kerberosNotConfigured=Kerberos är inte konfigurerat
+kerberosNotConfiguredTitle=Kerberos är inte konfigurerat
+bypassKerberosDetail=Antingen så är du inte inloggad via Kerberos eller så är inte din webbläsare inställd för Kerberosinloggning. Vänligen klicka på fortsätt för att logga in på annat sätt.
kerberosNotSetUp=Kerberos är inte inställt. Du kan inte logga in.
registerWithTitle=Registrera med {0}
registerWithTitleHtml={0}
loginTitle=Logga in till {0}
loginTitleHtml={0}
-impersonateTitle={0} Imitera Användare
-impersonateTitleHtml=<strong>{0}</strong> Imitera Användare</strong>
+impersonateTitle={0} Imitera användare
+impersonateTitleHtml=<strong>{0}</strong> Imitera användare</strong>
realmChoice=Realm
unknownUser=Okänd användare
-loginTotpTitle=Inställning av Mobilautentiserare
-loginProfileTitle=Uppdatera Kontoinformation
-loginTimeout=Du tog för lång tid för att logga in. Inloggningsprocessen börjar om.
-oauthGrantTitle=Bevilja Åtkomst
+loginTotpTitle=Inställning av mobilautentiserare
+loginProfileTitle=Uppdatera kontoinformation
+loginTimeout=Det tog för lång tid att logga in. Inloggningsprocessen börjar om.
+oauthGrantTitle=Bevilja åtkomst
oauthGrantTitleHtml={0}
errorTitle=Vi ber om ursäkt...
errorTitleHtml=Vi ber om <strong>ursäkt</strong> ...
-emailVerifyTitle=E-postsverifikation
-emailForgotTitle=Glömt Ditt Lösenord?
+emailVerifyTitle=E-postverifiering
+emailForgotTitle=Glömt ditt lösenord?
updatePasswordTitle=Uppdatera lösenord
codeSuccessTitle=Rätt kod
codeErrorTitle=Felkod\: {0}
@@ -51,15 +51,15 @@ firstName=Förnamn
lastName=Efternamn
email=E-post
password=Lösenord
-passwordConfirm=Bekräfta Lösenord
-passwordNew=Nytt Lösenord
-passwordNewConfirm=Bekräftelse av Nytt Lösenord
+passwordConfirm=Bekräfta lösenord
+passwordNew=Nytt lösenord
+passwordNewConfirm=Bekräftelse av nytt lösenord
rememberMe=Kom ihåg mig
authenticatorCode=Engångskod
address=Adress
street=Gata
locality=Postort
-region=Stat, Provins, eller Region
+region=Stat, Provins eller Region
postal_code=Postnummer
country=Land
emailVerified=E-post verifierad
@@ -70,8 +70,8 @@ loginTotpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln
loginTotpStep3=Fyll i engångskoden som tillhandahålls av applikationen och klicka på Spara för att avsluta inställningarna
loginTotpOneTime=Engångskod
-oauthGrantRequest=Godkänner du dessa åtkomstförmånen?
-inResource=in
+oauthGrantRequest=Godkänner du tillgång till de här rättigheterna?
+inResource=i
emailVerifyInstruction1=Ett e-postmeddelande med instruktioner om hur du verifierar din e-postadress har skickats till dig.
emailVerifyInstruction2=Har du inte fått en verifikationskod i din e-post?
@@ -88,9 +88,9 @@ emailInstruction=Fyll i ditt användarnamn eller din e-postadress, så kommer vi
copyCodeInstruction=Vänligen kopiera den här koden och klistra in den i din applikation:
-personalInfo=Personlig Information:
+personalInfo=Personlig information:
role_admin=Administratör
-role_realm-admin=Realm Administratör
+role_realm-admin=Realm-administratör
role_create-realm=Skapa realm
role_create-client=Skapa klient
role_view-realm=Visa realm
@@ -98,39 +98,39 @@ role_view-users=Visa användare
role_view-applications=Visa applikationer
role_view-clients=Visa klienter
role_view-events=Visa event
-role_view-identity-providers=Visa identity providers
+role_view-identity-providers=Visa identitetsleverantörer
role_manage-realm=Hantera realm
role_manage-users=Hantera användare
role_manage-applications=Hantera applikationer
-role_manage-identity-providers=Hantera identity providers
+role_manage-identity-providers=Hantera identitetsleverantörer
role_manage-clients=Hantera klienter
role_manage-events=Hantera event
role_view-profile=Visa profil
role_manage-account=Hantera konto
role_read-token=Läs element
-role_offline-access=Åtkomst Offline
+role_offline-access=Åtkomst offline
client_account=Konto
client_security-admin-console=Säkerhetsadministratörskonsol
-client_admin-cli=Administratörs CLI
+client_admin-cli=Administratörs-CLI
client_realm-management=Realmhantering
invalidUserMessage=Ogiltigt användarnamn eller lösenord.
invalidEmailMessage=Ogiltig e-postadress.
accountDisabledMessage=Kontot är inaktiverat, kontakta administratör.
accountTemporarilyDisabledMessage=Kontot är tillfälligt inaktiverat, kontakta administratör eller försök igen senare.
-expiredCodeMessage=Inloggnings time-out. Vänligen försök igen.
+expiredCodeMessage=Inloggningen nådde en maxtidsgräns. Vänligen försök igen.
missingFirstNameMessage=Vänligen ange förnamn.
missingLastNameMessage=Vänligen ange efternamn.
missingEmailMessage=Vänligen ange e-post.
missingUsernameMessage=Vänligen ange användarnamn.
missingPasswordMessage=Vänligen ange lösenord.
-missingTotpMessage=Vänligen ange autentiserarekod.
+missingTotpMessage=Vänligen ange autentiseringskod.
notMatchPasswordMessage=Lösenorden matchar inte.
invalidPasswordExistingMessage=Det nuvarande lösenordet är ogiltigt.
invalidPasswordConfirmMessage=Lösenordsbekräftelsen matchar inte.
-invalidTotpMessage=Autentiserarekoden är ogiltig.
+invalidTotpMessage=Autentiseringskoden är ogiltig.
usernameExistsMessage=Användarnamnet finns redan.
emailExistsMessage=E-postadressen finns redan.
@@ -169,42 +169,42 @@ invalidPasswordGenericMessage=Ogiltigt lösenord: Det nya lösenordet stämmer i
failedToProcessResponseMessage=Misslyckades med att behandla svaret
httpsRequiredMessage=HTTPS krävs
-realmNotEnabledMessage=Realm är inte aktiverat
-invalidRequestMessage=Ogiltig Förfrågan
+realmNotEnabledMessage=Realm är inte aktiverad
+invalidRequestMessage=Ogiltig förfrågan
failedLogout=Utloggning misslyckades
unknownLoginRequesterMessage=Okänd inloggningsförfrågan
loginRequesterNotEnabledMessage=Inloggningsförfrågaren är inte aktiverad
-bearerOnlyMessage=Bearer-only applikationer tillåts inte att initiera inloggning genom webbläsare
+bearerOnlyMessage=Bearer-only-applikationer tillåts inte att initiera inloggning genom webbläsare
standardFlowDisabledMessage=Klienten tillåts inte att initiera inloggning genom webbläsare med det givna response_type. Standardflödet är inaktiverat för klienten.
implicitFlowDisabledMessage=Klienten tillåts inte att initiera inloggning genom webbläsare med det givna response_type. Villkorslöst flöde är inaktiverat för klienten.
-invalidRedirectUriMessage=Ogiltig omdirigerad uri
+invalidRedirectUriMessage=Ogiltig omdirigeringsadress
unsupportedNameIdFormatMessage=NameIDFormat stöds ej
invalidRequesterMessage=Ogiltig förfrågare
registrationNotAllowedMessage=Registrering tillåts ej
resetCredentialNotAllowedMessage=Återställning av uppgifter tillåts ej
-permissionNotApprovedMessage=Tillståndet ej godkänt.
-noRelayStateInResponseMessage=Inget vidarebefordrat tillstånd i svaret från identity provider.
+permissionNotApprovedMessage=Rättigheten ej godkänd.
+noRelayStateInResponseMessage=Inget vidarebefordrat tillstånd i svaret från identitetsleverantör.
insufficientPermissionMessage=Otillräckliga tillstånd för att länka identiteter.
-couldNotProceedWithAuthenticationRequestMessage=Kunde inte fortsätta med autentiseringsförfrågan till identity provider.
-couldNotObtainTokenMessage=Kunde inte motta element från identity provider.
-unexpectedErrorRetrievingTokenMessage=Oväntat fel när element hämtas från identity provider.
-unexpectedErrorHandlingResponseMessage=Oväntat fel under hantering av svar från från identity provider.
-identityProviderAuthenticationFailedMessage=Autentiseringen misslyckades. Kunde inte autentisera med identity provider.
+couldNotProceedWithAuthenticationRequestMessage=Kunde inte fortsätta med autentiseringsförfrågan till identitetsleverantör.
+couldNotObtainTokenMessage=Kunde inte motta element från identitetsleverantör.
+unexpectedErrorRetrievingTokenMessage=Oväntat fel när element hämtas från identitetsleverantör.
+unexpectedErrorHandlingResponseMessage=Oväntat fel under hantering av svar från från identitetsleverantör.
+identityProviderAuthenticationFailedMessage=Autentiseringen misslyckades. Kunde inte autentisera med identitetsleverantör.
identityProviderDifferentUserMessage=Autentiserad som {0}, men väntades att vara autentiserad som {1}
-couldNotSendAuthenticationRequestMessage=Kunde inte skicka autentiseringsförfrågan till identity provider.
-unexpectedErrorHandlingRequestMessage=Oväntat fel under hantering av autentiseringsförfrågan till identity provider.
+couldNotSendAuthenticationRequestMessage=Kunde inte skicka autentiseringsförfrågan till identitetsleverantör.
+unexpectedErrorHandlingRequestMessage=Oväntat fel under hantering av autentiseringsförfrågan till identitetsleverantör.
invalidAccessCodeMessage=Ogiltig tillträdeskod.
sessionNotActiveMessage=Sessionen ej aktiv.
invalidCodeMessage=Ett fel uppstod, vänligen logga in igen genom din applikation.
-identityProviderUnexpectedErrorMessage=Oväntat fel under autentiseringen med identity provider
-identityProviderNotFoundMessage=Kunde inte hitta en identity provider med identifikatorn.
-identityProviderLinkSuccess=Ditt konto lyckades med att länka {0} med kontot {1} .
+identityProviderUnexpectedErrorMessage=Oväntat fel under autentiseringen med identitetsleverantör
+identityProviderNotFoundMessage=Kunde inte hitta en identitetsleverantör med identifikatorn.
+identityProviderLinkSuccess=Ditt konto lyckades med att länka {0} med kontot {1}.
staleCodeMessage=Den här sidan är inte längre giltig, vänligen gå tillbaka till din applikation och logga in igen
-realmSupportsNoCredentialsMessage=Realm:et stödjer inga inloggningstyper.
-identityProviderNotUniqueMessage=Realm:et stödjer flera identity providers. Kunde inte avgöra vilken identity provider som skall användas för autentisering.
+realmSupportsNoCredentialsMessage=Realmen stödjer inga inloggningstyper.
+identityProviderNotUniqueMessage=Realmen stödjer flera identitetsleverantör. Kunde inte avgöra vilken identitetsleverantör som skall användas för autentisering.
emailVerifiedMessage=Din e-postadress har blivit verifierad.
-staleEmailVerificationLink=Länken du klickade på är en gammal inaktuell länk som inte längre är giltig. Kanske har du redan verifierat din e-post?
+staleEmailVerificationLink=Länken du klickade på är en gammal, inaktuell länk som inte längre är giltig. Kanske har du redan verifierat din e-post?
backToApplication=« Tillbaka till applikationen
missingParameterMessage=Parametrar som saknas\: {0}
diff --git a/themes/src/main/resources-product/theme/rh-sso/admin/theme.properties b/themes/src/main/resources-product/theme/rh-sso/admin/theme.properties
index 009f980..5b489c1 100755
--- a/themes/src/main/resources-product/theme/rh-sso/admin/theme.properties
+++ b/themes/src/main/resources-product/theme/rh-sso/admin/theme.properties
@@ -1,3 +1,3 @@
parent=keycloak
import=common/rh-sso
-styles=lib/rcue/css/rcue.min.css lib/rcue/css/rcue-additions.min.css lib/select2-3.4.1/select2.css css/styles.css lib/angular/treeview/css/angular.treeview.css
+styles=lib/rcue/css/rcue.min.css lib/rcue/css/rcue-additions.min.css node_modules/select2/select2.css css/styles.css lib/angular/treeview/css/angular.treeview.css
travis-run-tests.sh 52(+25 -27)
diff --git a/travis-run-tests.sh b/travis-run-tests.sh
index fd5e5e3..4d4f905 100755
--- a/travis-run-tests.sh
+++ b/travis-run-tests.sh
@@ -1,42 +1,40 @@
#!/bin/bash -e
-mvn install --no-snapshot-updates -DskipTests=true -f testsuite
+function run-server-tests {
+ cd testsuite/integration-arquillian
+ mvn install -B -nsu -Pauth-server-wildfly -DskipTests
-if [ $1 == "old" ]; then
- mvn test -B --no-snapshot-updates -f testsuite/integration
- mvn test -B --no-snapshot-updates -f testsuite/jetty
- mvn test -B --no-snapshot-updates -f testsuite/tomcat6
- mvn test -B --no-snapshot-updates -f testsuite/tomcat7
- mvn test -B --no-snapshot-updates -f testsuite/tomcat8
-fi
+ cd tests/base
+ mvn test -B -nsu -Pauth-server-wildfly -Dtest=$1 2>&1 | java -cp ../../../utils/target/classes org.keycloak.testsuite.LogTrimmer
+ exit ${PIPESTATUS[0]}
+}
-if [ $1 == "group1" ]; then
- cd testsuite/integration-arquillian/tests/base
- mvn test -B --no-snapshot-updates -Dtest=org.keycloak.testsuite.ad*.**.*Test
-fi
+mvn install -B -nsu -Pdistribution -DskipTests -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
-if [ $1 == "group2" ]; then
- cd testsuite/integration-arquillian/tests/base
- mvn test -B --no-snapshot-updates -Dtest=org.keycloak.testsuite.ac*.**.*Test,org.keycloak.testsuite.b*.**.*Test,org.keycloak.testsuite.cli*.**.*Test,org.keycloak.testsuite.co*.**.*Test
+if [ $1 == "old" ]; then
+ cd testsuite
+ mvn test -B -nsu -f integration
+ mvn test -B -nsu -f jetty
+ mvn test -B -nsu -f tomcat7
+ mvn test -B -nsu -f tomcat8
fi
-if [ $1 == "group3" ]; then
- cd testsuite/integration-arquillian/tests/base
- mvn test -B --no-snapshot-updates -Dtest=org.keycloak.testsuite.au*.**.*Test,org.keycloak.testsuite.d*.**.*Test,org.keycloak.testsuite.e*.**.*Test,org.keycloak.testsuite.f*.**.*Test,org.keycloak.testsuite.i*.**.*Test
+if [ $1 == "unit" ]; then
+ mvn -B test -DskipTestsuite
fi
-if [ $1 == "group4" ]; then
- cd testsuite/integration-arquillian/tests/base
- mvn test -B --no-snapshot-updates -Dtest=org.keycloak.testsuite.k*.**.*Test,org.keycloak.testsuite.m*.**.*Test,org.keycloak.testsuite.o*.**.*Test,org.keycloak.testsuite.s*.**.*Test
+if [ $1 == "server-group1" ]; then
+ run-server-tests org.keycloak.testsuite.ad*.**.*Test,!**/adapter/undertow/**/*Test
fi
-if [ $1 == "adapter" ]; then
- cd testsuite/integration-arquillian/tests/other/adapters
- mvn test -B --no-snapshot-updates
+if [ $1 == "server-group2" ]; then
+ run-server-tests org.keycloak.testsuite.ac*.**.*Test,org.keycloak.testsuite.b*.**.*Test,org.keycloak.testsuite.cli*.**.*Test,org.keycloak.testsuite.co*.**.*Test
fi
-if [ $1 == "console" ]; then
- cd testsuite/integration-arquillian/tests/other/console
- mvn test -B --no-snapshot-updates
+if [ $1 == "server-group3" ]; then
+ run-server-tests org.keycloak.testsuite.au*.**.*Test,org.keycloak.testsuite.d*.**.*Test,org.keycloak.testsuite.e*.**.*Test,org.keycloak.testsuite.f*.**.*Test,org.keycloak.testsuite.i*.**.*Test
fi
+if [ $1 == "server-group4" ]; then
+ run-server-tests org.keycloak.testsuite.k*.**.*Test,org.keycloak.testsuite.m*.**.*Test,org.keycloak.testsuite.o*.**.*Test,org.keycloak.testsuite.s*.**.*Test
+fi
util/embedded-ldap/pom.xml 2(+1 -1)
diff --git a/util/embedded-ldap/pom.xml b/util/embedded-ldap/pom.xml
index 2ea8d90..b5d3172 100644
--- a/util/embedded-ldap/pom.xml
+++ b/util/embedded-ldap/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
util/pom.xml 2(+1 -1)
diff --git a/util/pom.xml b/util/pom.xml
index 25781bb..296413b 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
wildfly/adduser/pom.xml 2(+1 -1)
diff --git a/wildfly/adduser/pom.xml b/wildfly/adduser/pom.xml
index 4ae847c..cbb127b 100755
--- a/wildfly/adduser/pom.xml
+++ b/wildfly/adduser/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-wildfly-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-wildfly-adduser</artifactId>
wildfly/extensions/pom.xml 2(+1 -1)
diff --git a/wildfly/extensions/pom.xml b/wildfly/extensions/pom.xml
index 4a042d6..05733c8 100755
--- a/wildfly/extensions/pom.xml
+++ b/wildfly/extensions/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-wildfly-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-wildfly-extensions</artifactId>
wildfly/pom.xml 2(+1 -1)
diff --git a/wildfly/pom.xml b/wildfly/pom.xml
index c573b2f..fcaffad 100755
--- a/wildfly/pom.xml
+++ b/wildfly/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<name>Keycloak WildFly Integration</name>
wildfly/server-subsystem/pom.xml 2(+1 -1)
diff --git a/wildfly/server-subsystem/pom.xml b/wildfly/server-subsystem/pom.xml
index 9161e85..9f8f7e6 100755
--- a/wildfly/server-subsystem/pom.xml
+++ b/wildfly/server-subsystem/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-wildfly-parent</artifactId>
- <version>3.2.0.CR1-SNAPSHOT</version>
+ <version>3.3.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>keycloak-wildfly-server-subsystem</artifactId>